pyroll-ring-model-thermal 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyroll/ring_model_thermal/__init__.py +21 -0
- pyroll/ring_model_thermal/config.py +10 -0
- pyroll/ring_model_thermal/profile.py +52 -0
- pyroll/ring_model_thermal/report.py +129 -0
- pyroll/ring_model_thermal/roll_pass.py +207 -0
- pyroll/ring_model_thermal/transport.py +133 -0
- pyroll_ring_model_thermal-2.0.0.dist-info/METADATA +10 -0
- pyroll_ring_model_thermal-2.0.0.dist-info/RECORD +10 -0
- pyroll_ring_model_thermal-2.0.0.dist-info/WHEEL +4 -0
- pyroll_ring_model_thermal-2.0.0.dist-info/licenses/LICENSE +11 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
VERSION = "2.0.0"
|
|
2
|
+
|
|
3
|
+
from . import profile
|
|
4
|
+
from . import roll_pass
|
|
5
|
+
from . import transport
|
|
6
|
+
from .config import Config
|
|
7
|
+
|
|
8
|
+
from pyroll.core import root_hooks, Unit
|
|
9
|
+
|
|
10
|
+
root_hooks.add(Unit.OutProfile.temperature)
|
|
11
|
+
root_hooks.add(Unit.OutProfile.ring_temperatures)
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
|
|
15
|
+
REPORT_INSTALLED = bool(importlib.util.find_spec("pyroll.report"))
|
|
16
|
+
|
|
17
|
+
if REPORT_INSTALLED:
|
|
18
|
+
from . import report
|
|
19
|
+
import pyroll.report
|
|
20
|
+
|
|
21
|
+
pyroll.report.plugin_manager.register(report)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from pyroll.core import config
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@config("PYROLL_RING_MODEL_THERMAL")
|
|
5
|
+
class Config:
|
|
6
|
+
RADIATION_COEFFICIENT = 5.670374419e-8
|
|
7
|
+
"""The Stefan-Boltzmann radiation constant. Only to change if not working with SI-units."""
|
|
8
|
+
|
|
9
|
+
ROLL_PASS_ATMOSPHERE_TRANSFER = True
|
|
10
|
+
"""Whether to include the heat transfer to atmosphere at free surfaces in roll passes into the calculation."""
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pyroll.core import Profile, Unit
|
|
3
|
+
from pyroll.core.hooks import Hook
|
|
4
|
+
from pyroll.ring_model import RingProfile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@Profile.extension_class
|
|
8
|
+
class Profile(RingProfile):
|
|
9
|
+
ring_temperatures = Hook[np.ndarray]()
|
|
10
|
+
"""Array temperature values from core to surface."""
|
|
11
|
+
|
|
12
|
+
relative_radiation_coefficient = Hook[float]()
|
|
13
|
+
"""Heat transfer coefficient by convection to atmosphere."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@Profile.relative_radiation_coefficient
|
|
17
|
+
def relative_radiation_coefficient(self: Profile):
|
|
18
|
+
return 0.8
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@Profile.ring_temperatures
|
|
22
|
+
def homogeneous_profile(self: Profile):
|
|
23
|
+
if self.has_set_or_cached("temperature"):
|
|
24
|
+
return np.full_like(self.rings, self.temperature)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@Unit.OutProfile.ring_temperatures
|
|
28
|
+
def out_ring_temperatures_from_in(self: Unit.OutProfile):
|
|
29
|
+
if self.unit.in_profile.has_set_or_cached("ring_temperatures"):
|
|
30
|
+
return np.copy(self.unit.in_profile.ring_temperatures)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@Profile.temperature
|
|
34
|
+
def mean_temperature(self: Profile):
|
|
35
|
+
return np.sum(self.ring_temperatures * [s.area for s in self.ring_sections]) / np.sum(
|
|
36
|
+
[s.area for s in self.ring_sections])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@Profile.surface_temperature
|
|
40
|
+
def surface_temperature(self: Profile):
|
|
41
|
+
return self.ring_temperatures[-1]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@Profile.core_temperature
|
|
45
|
+
def core_temperature(self: Profile):
|
|
46
|
+
return self.ring_temperatures[0]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@Unit.OutProfile.ring_temperatures
|
|
50
|
+
def ring_temperatures_from_disks(self: Unit.OutProfile):
|
|
51
|
+
if self.unit.subunits:
|
|
52
|
+
return self.unit.subunits[-1].out_profile.ring_temperatures
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pyroll.report import hookimpl
|
|
3
|
+
from pyroll.core import Unit, PassSequence
|
|
4
|
+
from pyroll.core import DiskElementUnit
|
|
5
|
+
import matplotlib as mpl
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import matplotlib.collections as mcol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@hookimpl(specname="unit_plot")
|
|
11
|
+
def disked_unit_temperature_plot(unit: Unit):
|
|
12
|
+
if isinstance(unit, DiskElementUnit):
|
|
13
|
+
fig: plt.Figure = plt.figure()
|
|
14
|
+
ax: plt.Axes = fig.subplots()
|
|
15
|
+
|
|
16
|
+
count = len(unit.subunits) + 2
|
|
17
|
+
cmap = mpl.colormaps['coolwarm_r']
|
|
18
|
+
colors = cmap(np.linspace(0, 1, count))
|
|
19
|
+
|
|
20
|
+
def prepare_data(p):
|
|
21
|
+
return (
|
|
22
|
+
np.append(p.rings, p.equivalent_radius) / p.equivalent_radius,
|
|
23
|
+
np.append(p.ring_temperatures, p.surface_temperature)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ip = ax.plot(*prepare_data(unit.in_profile), c=colors[0], label="incoming profile")[0]
|
|
27
|
+
|
|
28
|
+
for i in range(1, count - 1):
|
|
29
|
+
u = unit.subunits[i - 1]
|
|
30
|
+
ax.plot(*prepare_data(u.out_profile), c=colors[i])
|
|
31
|
+
|
|
32
|
+
op = ax.plot(*prepare_data(unit.out_profile), c=colors[-1], label="outgoing profile")[0]
|
|
33
|
+
|
|
34
|
+
ax.set_title("Radial Temperature Profile Evolution")
|
|
35
|
+
ax.set_xlabel("Relative Distance from Core")
|
|
36
|
+
ax.set_ylabel("Temperature")
|
|
37
|
+
|
|
38
|
+
lc = mcol.LineCollection(
|
|
39
|
+
5 * [[(0, 0)]],
|
|
40
|
+
colors=cmap(np.linspace(0, 1, 7)[1:-1]),
|
|
41
|
+
label="intermediate stages",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
ax.legend(handles=[ip, lc, op], handler_map={type(lc): HandlerDashedLines()})
|
|
45
|
+
|
|
46
|
+
return fig
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@hookimpl(specname="unit_plot")
|
|
50
|
+
def pass_sequence_temperature_plot(unit: Unit):
|
|
51
|
+
if isinstance(unit, PassSequence):
|
|
52
|
+
fig: plt.Figure = plt.figure()
|
|
53
|
+
ax: plt.Axes = fig.subplots()
|
|
54
|
+
|
|
55
|
+
def yield_data(u: Unit, name):
|
|
56
|
+
yield getattr(u.in_profile, name, None)
|
|
57
|
+
if u.subunits:
|
|
58
|
+
for su in u.subunits:
|
|
59
|
+
yield from yield_data(su, name)
|
|
60
|
+
yield getattr(u.out_profile, name, None)
|
|
61
|
+
|
|
62
|
+
t = np.array(list(yield_data(unit, "t")))
|
|
63
|
+
core = np.array(list(yield_data(unit, "core_temperature")))
|
|
64
|
+
surface = np.array(list(yield_data(unit, "surface_temperature")))
|
|
65
|
+
mean = np.array(list(yield_data(unit, "temperature")))
|
|
66
|
+
|
|
67
|
+
ax.plot(t, core, label="core")
|
|
68
|
+
ax.plot(t, surface, label="surface")
|
|
69
|
+
ax.plot(t, mean, label="mean")
|
|
70
|
+
|
|
71
|
+
ax.legend()
|
|
72
|
+
|
|
73
|
+
ax.set_title("Temperature Evolution")
|
|
74
|
+
ax.set_xlabel("Cumulative Process Time $t$")
|
|
75
|
+
ax.set_ylabel("Temperature $T$")
|
|
76
|
+
|
|
77
|
+
return fig
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# from https://matplotlib.org/stable/gallery/text_labels_and_annotations/legend_demo.html#sphx-glr-gallery-text-labels-and-annotations-legend-demo-py
|
|
81
|
+
from matplotlib.legend_handler import HandlerLineCollection
|
|
82
|
+
from matplotlib.lines import Line2D
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class HandlerDashedLines(HandlerLineCollection):
|
|
86
|
+
"""
|
|
87
|
+
Custom Handler for LineCollection instances.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def create_artists(
|
|
91
|
+
self, legend, orig_handle,
|
|
92
|
+
xdescent, ydescent, width, height, fontsize, trans
|
|
93
|
+
):
|
|
94
|
+
# figure out how many lines there are
|
|
95
|
+
numlines = len(orig_handle.get_segments())
|
|
96
|
+
xdata, xdata_marker = self.get_xdata(
|
|
97
|
+
legend, xdescent, ydescent,
|
|
98
|
+
width, height, fontsize
|
|
99
|
+
)
|
|
100
|
+
leglines = []
|
|
101
|
+
# divide the vertical space where the lines will go
|
|
102
|
+
# into equal parts based on the number of lines
|
|
103
|
+
ydata = np.full_like(xdata, height / (numlines + 1))
|
|
104
|
+
# for each line, create the line at the proper location
|
|
105
|
+
# and set the dash pattern
|
|
106
|
+
for i in range(numlines):
|
|
107
|
+
legline = Line2D(xdata, ydata * (numlines - i) - ydescent)
|
|
108
|
+
self.update_prop(legline, orig_handle, legend)
|
|
109
|
+
# set color, dash pattern, and linewidth to that
|
|
110
|
+
# of the lines in linecollection
|
|
111
|
+
try:
|
|
112
|
+
color = orig_handle.get_colors()[i]
|
|
113
|
+
except IndexError:
|
|
114
|
+
color = orig_handle.get_colors()[0]
|
|
115
|
+
try:
|
|
116
|
+
dashes = orig_handle.get_dashes()[i]
|
|
117
|
+
except IndexError:
|
|
118
|
+
dashes = orig_handle.get_dashes()[0]
|
|
119
|
+
try:
|
|
120
|
+
lw = orig_handle.get_linewidths()[i]
|
|
121
|
+
except IndexError:
|
|
122
|
+
lw = orig_handle.get_linewidths()[0]
|
|
123
|
+
if dashes[1] is not None:
|
|
124
|
+
legline.set_dashes(dashes[1])
|
|
125
|
+
legline.set_color(color)
|
|
126
|
+
legline.set_transform(trans)
|
|
127
|
+
legline.set_linewidth(lw)
|
|
128
|
+
leglines.append(legline)
|
|
129
|
+
return leglines
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from typing import Union, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .config import Config
|
|
6
|
+
from .profile import Profile
|
|
7
|
+
from pyroll.core import RollPass, Unit, Hook, DeformationUnit
|
|
8
|
+
import scipy.optimize as scopt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@RollPass.Roll.extension_class
|
|
12
|
+
class RollExt(RollPass.Roll):
|
|
13
|
+
heat_transfer_coefficient = Hook[float]()
|
|
14
|
+
"""Heat transfer coefficient by contact to the roll."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@RollPass.extension_class
|
|
18
|
+
class RollPassExt(RollPass):
|
|
19
|
+
heat_transfer_coefficient = Hook[float]()
|
|
20
|
+
"""Heat transfer coefficient by convection to atmosphere."""
|
|
21
|
+
|
|
22
|
+
deformation_heat_efficiency = Hook[float]()
|
|
23
|
+
"""Efficiency of heat generation by deformation power."""
|
|
24
|
+
|
|
25
|
+
environment_temperature = Hook[float]()
|
|
26
|
+
"""Temperature of the surrounding atmosphere."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@RollPassExt.deformation_heat_efficiency
|
|
30
|
+
def deformation_heat_efficiency(self: RollPass):
|
|
31
|
+
return 0.95
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@RollPassExt.environment_temperature
|
|
35
|
+
def environment_temperature(self: RollPassExt):
|
|
36
|
+
return self.roll.temperature
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@RollExt.heat_transfer_coefficient
|
|
40
|
+
def heat_transfer_coefficient(self: RollPass):
|
|
41
|
+
return 6000
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@RollPassExt.heat_transfer_coefficient
|
|
45
|
+
def heat_transfer_coefficient(self: RollPass):
|
|
46
|
+
return 150
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_increments(unit: DeformationUnit, roll_pass: RollPassExt, ring_temperatures) -> np.ndarray:
|
|
50
|
+
p: Profile = unit.in_profile
|
|
51
|
+
|
|
52
|
+
increments = np.zeros_like(ring_temperatures)
|
|
53
|
+
|
|
54
|
+
deformation_resistance = (
|
|
55
|
+
unit.deformation_resistance
|
|
56
|
+
if unit.has_value("deformation_resistance")
|
|
57
|
+
else (unit.in_profile.flow_stress + 2 * unit.out_profile.flow_stress) / 3
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
source_density = roll_pass.deformation_heat_efficiency * deformation_resistance * unit.strain_rate
|
|
61
|
+
|
|
62
|
+
cross_section = p.ring_sections[0].area
|
|
63
|
+
increments[0] = unit.duration / (p.density * p.thermal_capacity * cross_section) * (
|
|
64
|
+
(ring_temperatures[1] - ring_temperatures[0]) * p.ring_contours[1].length
|
|
65
|
+
/ p.rings[1] * p.thermal_conductivity
|
|
66
|
+
+ source_density * cross_section
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
free_surface_ratio = unit.free_surface_area / unit.surface_area
|
|
71
|
+
except AttributeError:
|
|
72
|
+
free_surface_ratio = 0
|
|
73
|
+
|
|
74
|
+
cross_section = p.ring_sections[-1].area
|
|
75
|
+
|
|
76
|
+
roll_contact_transfer = (
|
|
77
|
+
roll_pass.roll.heat_transfer_coefficient * (roll_pass.roll.temperature - p.surface_temperature)
|
|
78
|
+
* p.ring_contours[-1].length * (1 - free_surface_ratio)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if Config.ROLL_PASS_ATMOSPHERE_TRANSFER:
|
|
82
|
+
atmosphere_transfer = (
|
|
83
|
+
(
|
|
84
|
+
roll_pass.heat_transfer_coefficient
|
|
85
|
+
* (roll_pass.environment_temperature - p.surface_temperature)
|
|
86
|
+
+ Config.RADIATION_COEFFICIENT * p.relative_radiation_coefficient
|
|
87
|
+
* (roll_pass.environment_temperature ** 4 - p.surface_temperature ** 4)
|
|
88
|
+
)
|
|
89
|
+
* p.ring_contours[-1].length * free_surface_ratio
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
atmosphere_transfer = 0
|
|
93
|
+
|
|
94
|
+
increments[-1] = unit.duration / (p.density * p.thermal_capacity * cross_section) * (
|
|
95
|
+
roll_contact_transfer + atmosphere_transfer
|
|
96
|
+
- p.thermal_conductivity * (ring_temperatures[-1] - ring_temperatures[-2])
|
|
97
|
+
/ (p.rings[-1] - p.rings[-2])
|
|
98
|
+
* p.ring_contours[-2].length
|
|
99
|
+
+ source_density * cross_section
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
for i in range(1, len(increments) - 1):
|
|
103
|
+
cross_section = p.ring_sections[i].area
|
|
104
|
+
increments[i] = unit.duration / (p.density * p.thermal_capacity * cross_section) * (
|
|
105
|
+
p.thermal_conductivity
|
|
106
|
+
* (
|
|
107
|
+
(ring_temperatures[i + 1] - ring_temperatures[i]) * p.ring_contours[i + 1].length
|
|
108
|
+
/ (p.rings[i + 1] - p.rings[i])
|
|
109
|
+
- (ring_temperatures[i] - ring_temperatures[i - 1]) * p.ring_contours[i].length
|
|
110
|
+
/ (p.rings[i] - p.rings[i - 1])
|
|
111
|
+
)
|
|
112
|
+
+ source_density * cross_section
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return increments
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _solve_step(unit, roll_pass, in_ring_temperatures):
|
|
119
|
+
x0 = get_increments(unit, roll_pass, in_ring_temperatures)
|
|
120
|
+
|
|
121
|
+
def f(x):
|
|
122
|
+
out_ring_temperatures = in_ring_temperatures + x
|
|
123
|
+
return get_increments(unit, roll_pass, out_ring_temperatures) - x
|
|
124
|
+
|
|
125
|
+
sol = scopt.root(f, x0=x0)
|
|
126
|
+
|
|
127
|
+
if not sol.success:
|
|
128
|
+
raise RuntimeError(f"Numerical procedure did not succeed: {sol.message}.")
|
|
129
|
+
|
|
130
|
+
return in_ring_temperatures + sol.x
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@RollPass.OutProfile.ring_temperatures
|
|
134
|
+
def ring_temperatures_disk(self: Union[RollPass.OutProfile, Profile]):
|
|
135
|
+
if not self.roll_pass.disk_elements:
|
|
136
|
+
roll_pass = self.roll_pass
|
|
137
|
+
|
|
138
|
+
return _solve_step(roll_pass, roll_pass, roll_pass.in_profile.ring_temperatures)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@RollPass.DiskElement.OutProfile.ring_temperatures
|
|
142
|
+
def ring_temperatures_disk(self: Union[RollPass.DiskElement.OutProfile, Profile]):
|
|
143
|
+
roll_pass = self.roll_pass
|
|
144
|
+
disk = self.disk_element
|
|
145
|
+
|
|
146
|
+
return _solve_step(disk, roll_pass, disk.in_profile.ring_temperatures)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _surface_temperature(self: Union[RollPass.Profile, Profile]):
|
|
150
|
+
roll_pass: RollPassExt = self.roll_pass
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
free_surface_ratio = roll_pass.free_surface_area / roll_pass.surface_area
|
|
154
|
+
except AttributeError:
|
|
155
|
+
free_surface_ratio = 0
|
|
156
|
+
|
|
157
|
+
def f(ts):
|
|
158
|
+
roll_contact_transfer = (
|
|
159
|
+
roll_pass.roll.heat_transfer_coefficient * (roll_pass.roll.temperature - ts)
|
|
160
|
+
* self.ring_contours[-1].length * (1 - free_surface_ratio)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if Config.ROLL_PASS_ATMOSPHERE_TRANSFER:
|
|
164
|
+
atmosphere_transfer = (
|
|
165
|
+
(
|
|
166
|
+
roll_pass.heat_transfer_coefficient
|
|
167
|
+
* (roll_pass.environment_temperature - ts)
|
|
168
|
+
+ Config.RADIATION_COEFFICIENT * self.relative_radiation_coefficient
|
|
169
|
+
* (roll_pass.environment_temperature ** 4 - ts ** 4)
|
|
170
|
+
)
|
|
171
|
+
* self.ring_contours[-1].length * free_surface_ratio
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
atmosphere_transfer = 0
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
roll_contact_transfer + atmosphere_transfer
|
|
178
|
+
- self.thermal_conductivity
|
|
179
|
+
* (ts - self.ring_temperatures[-1])
|
|
180
|
+
/ (self.equivalent_radius - self.rings[-1])
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def fprime(ts):
|
|
184
|
+
return (
|
|
185
|
+
- roll_pass.roll.heat_transfer_coefficient * (1 - free_surface_ratio)
|
|
186
|
+
- ((
|
|
187
|
+
roll_pass.heat_transfer_coefficient
|
|
188
|
+
+ 4 * Config.RADIATION_COEFFICIENT * self.relative_radiation_coefficient
|
|
189
|
+
* ts ** 3
|
|
190
|
+
) * free_surface_ratio if Config.ROLL_PASS_ATMOSPHERE_TRANSFER else 0)
|
|
191
|
+
- self.thermal_conductivity / (self.equivalent_radius - self.rings[-1])
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
sol = scopt.root_scalar(f=f, fprime=fprime, x0=self.ring_temperatures[-1], method="newton")
|
|
195
|
+
|
|
196
|
+
if sol.converged:
|
|
197
|
+
return sol.root
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@RollPass.Profile.surface_temperature
|
|
201
|
+
def surface_temperature(self: Union[RollPass.Profile, Profile]):
|
|
202
|
+
return _surface_temperature(self)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@RollPass.DiskElement.Profile.surface_temperature
|
|
206
|
+
def disk_surface_temperature(self: Union[RollPass.Profile, Profile]):
|
|
207
|
+
return _surface_temperature(self)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import scipy.optimize as scopt
|
|
5
|
+
from pyroll.core import Transport, Unit, Hook
|
|
6
|
+
|
|
7
|
+
from .config import Config
|
|
8
|
+
from .profile import Profile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@Transport.extension_class
|
|
12
|
+
class TransportExt(Transport):
|
|
13
|
+
heat_transfer_coefficient = Hook[float]()
|
|
14
|
+
"""Heat transfer coefficient by convection to atmosphere."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@TransportExt.heat_transfer_coefficient
|
|
18
|
+
def heat_transfer_coefficient(self: Transport):
|
|
19
|
+
return 15
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_increments(unit: Unit, transport: TransportExt, ring_temperatures) -> np.ndarray:
|
|
23
|
+
p: Profile = unit.out_profile
|
|
24
|
+
|
|
25
|
+
increments = np.zeros_like(ring_temperatures)
|
|
26
|
+
|
|
27
|
+
source_density = 0 # TODO source density term in W / m^3
|
|
28
|
+
|
|
29
|
+
cross_section = p.ring_sections[0].area
|
|
30
|
+
increments[0] = unit.duration / (p.density * p.thermal_capacity * cross_section) * (
|
|
31
|
+
(ring_temperatures[1] - ring_temperatures[0]) * p.ring_contours[1].length
|
|
32
|
+
/ p.rings[1] * p.thermal_conductivity
|
|
33
|
+
+ source_density * cross_section
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
cross_section = p.ring_sections[-1].area
|
|
37
|
+
increments[-1] = unit.duration / (p.density * p.thermal_capacity * cross_section) * (
|
|
38
|
+
(
|
|
39
|
+
transport.heat_transfer_coefficient
|
|
40
|
+
* (transport.environment_temperature - p.surface_temperature)
|
|
41
|
+
+ Config.RADIATION_COEFFICIENT * p.relative_radiation_coefficient
|
|
42
|
+
* (transport.environment_temperature ** 4 - p.surface_temperature ** 4)
|
|
43
|
+
)
|
|
44
|
+
* p.ring_contours[-1].length
|
|
45
|
+
- p.thermal_conductivity * (ring_temperatures[-1] - ring_temperatures[-2])
|
|
46
|
+
/ (p.rings[-1] - p.rings[-2])
|
|
47
|
+
* p.ring_contours[-2].length
|
|
48
|
+
+ source_density * cross_section
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
for i in range(1, len(increments) - 1):
|
|
52
|
+
cross_section = p.ring_sections[i].area
|
|
53
|
+
increments[i] = unit.duration / (p.density * p.thermal_capacity * cross_section) * (
|
|
54
|
+
p.thermal_conductivity
|
|
55
|
+
* (
|
|
56
|
+
(ring_temperatures[i + 1] - ring_temperatures[i]) * p.ring_contours[i + 1].length
|
|
57
|
+
/ (p.rings[i + 1] - p.rings[i])
|
|
58
|
+
- (ring_temperatures[i] - ring_temperatures[i - 1]) * p.ring_contours[i].length
|
|
59
|
+
/ (p.rings[i] - p.rings[i - 1])
|
|
60
|
+
)
|
|
61
|
+
+ source_density * cross_section
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return increments
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _solve_step(unit, transport, in_ring_temperatures):
|
|
68
|
+
x0 = get_increments(unit, transport, in_ring_temperatures)
|
|
69
|
+
|
|
70
|
+
def f(x):
|
|
71
|
+
out_ring_temperatures = in_ring_temperatures + x
|
|
72
|
+
return get_increments(unit, transport, out_ring_temperatures) - x
|
|
73
|
+
|
|
74
|
+
sol = scopt.root(f, x0=x0)
|
|
75
|
+
|
|
76
|
+
if not sol.success:
|
|
77
|
+
raise RuntimeError(f"Numerical procedure did not succeed: {sol.message}.")
|
|
78
|
+
|
|
79
|
+
return in_ring_temperatures + sol.x
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@Transport.OutProfile.ring_temperatures
|
|
83
|
+
def ring_temperatures_disk(self: Union[Transport.OutProfile, Profile]):
|
|
84
|
+
if not self.transport.disk_elements:
|
|
85
|
+
transport = self.transport
|
|
86
|
+
|
|
87
|
+
return _solve_step(transport, transport, transport.in_profile.ring_temperatures)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@Transport.DiskElement.OutProfile.ring_temperatures
|
|
91
|
+
def ring_temperatures_disk(self: Union[Transport.DiskElement.OutProfile, Profile]):
|
|
92
|
+
transport = self.transport
|
|
93
|
+
disk = self.disk_element
|
|
94
|
+
|
|
95
|
+
return _solve_step(disk, transport, disk.in_profile.ring_temperatures)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _surface_temperature(self: Union[Transport.Profile, Profile]):
|
|
99
|
+
transport: TransportExt = self.transport
|
|
100
|
+
|
|
101
|
+
def f(ts):
|
|
102
|
+
return (
|
|
103
|
+
transport.heat_transfer_coefficient
|
|
104
|
+
* (transport.environment_temperature - ts)
|
|
105
|
+
+ Config.RADIATION_COEFFICIENT * self.relative_radiation_coefficient
|
|
106
|
+
* (transport.environment_temperature ** 4 - ts ** 4)
|
|
107
|
+
- self.thermal_conductivity
|
|
108
|
+
* (ts - self.ring_temperatures[-1])
|
|
109
|
+
/ (self.equivalent_radius - self.rings[-1])
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def fprime(ts):
|
|
113
|
+
return (
|
|
114
|
+
-4 * Config.RADIATION_COEFFICIENT * self.relative_radiation_coefficient
|
|
115
|
+
* ts ** 3
|
|
116
|
+
- transport.heat_transfer_coefficient
|
|
117
|
+
- self.thermal_conductivity / (self.equivalent_radius - self.rings[-1])
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
sol = scopt.root_scalar(f=f, fprime=fprime, x0=self.ring_temperatures[-1], method="newton")
|
|
121
|
+
|
|
122
|
+
if sol.converged:
|
|
123
|
+
return sol.root
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@Transport.Profile.surface_temperature
|
|
127
|
+
def surface_temperature(self: Union[Transport.Profile, Profile]):
|
|
128
|
+
return _surface_temperature(self)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@Transport.DiskElement.Profile.surface_temperature
|
|
132
|
+
def disk_surface_temperature(self: Union[Transport.Profile, Profile]):
|
|
133
|
+
return _surface_temperature(self)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyroll-ring-model-thermal
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Plugin for PyRoll providing a radial thermal approach based on the ring model.
|
|
5
|
+
Author-email: Max Weiner <max.weiner@imf.tu-freiberg.de>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: <4.0,>=3.9
|
|
9
|
+
Requires-Dist: pyroll-core~=2.0
|
|
10
|
+
Requires-Dist: pyroll-ring-model~=2.0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyroll/ring_model_thermal/__init__.py,sha256=qbbGudfCewqkZOijlLcruoQ_rC3GqmYnF3viq18-tLk,467
|
|
2
|
+
pyroll/ring_model_thermal/config.py,sha256=-_4WFcZNqAiFkukexpMb_ygZNi69CigBF782JEDdIeU,380
|
|
3
|
+
pyroll/ring_model_thermal/profile.py,sha256=u5z9NadWPQyWBDluy_QDj04KDfZoFdKpjKx7LQ3cCnc,1510
|
|
4
|
+
pyroll/ring_model_thermal/report.py,sha256=PHH2gxlbCsH-kUlLyE4qeYH4P0KTSFuKXmhnHaEGcv4,4573
|
|
5
|
+
pyroll/ring_model_thermal/roll_pass.py,sha256=oCyxnv4rRSHsqKXYzK7WAvaiDnLlfk-cRHEbFL7Wxl8,7288
|
|
6
|
+
pyroll/ring_model_thermal/transport.py,sha256=8be2cDEjzRGZTtwAQ_lDUcjnVopHZWA-IH2aEFcvWSY,4759
|
|
7
|
+
pyroll_ring_model_thermal-2.0.0.dist-info/METADATA,sha256=_X9TOMQPU-sAgGJSHDLluRS-Z8IXhm78AXmR3gXeLKc,367
|
|
8
|
+
pyroll_ring_model_thermal-2.0.0.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
|
|
9
|
+
pyroll_ring_model_thermal-2.0.0.dist-info/licenses/LICENSE,sha256=cC-Hopqx0dkfeivDS5kNfOOAZ-IBvUe5BEF4z9MY1Mo,1474
|
|
10
|
+
pyroll_ring_model_thermal-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Copyright 2022 Max Weiner, Christoph Renzing
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
|
|
7
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
8
|
+
|
|
9
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
10
|
+
|
|
11
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|