pyroll-core 2.1.3__tar.gz → 2.1.4__tar.gz

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.
Files changed (74) hide show
  1. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/PKG-INFO +8 -1
  2. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/README.md +1 -0
  3. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/hatch.toml +3 -4
  4. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyproject.toml +8 -0
  5. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/__init__.py +14 -1
  6. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/config.py +22 -0
  7. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/base.py +36 -14
  8. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/generic_elongation.py +4 -4
  9. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/spline.py +4 -4
  10. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/profile/profile.py +74 -22
  11. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/repr.py +31 -16
  12. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/deformation_unit.py +3 -0
  13. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/hookimpls/deformation_unit.py +8 -0
  14. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/hookimpls/helpers.py +3 -2
  15. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/roll_pass.py +128 -54
  16. pyroll_core-2.1.4/pyroll/core/transport/hookimpls.py +52 -0
  17. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/unit/hookimpls.py +5 -5
  18. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/unit/unit.py +14 -0
  19. pyroll_core-2.1.3/pyroll/core/transport/hookimpls.py +0 -26
  20. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/.gitignore +0 -0
  21. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/LICENSE +0 -0
  22. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/disk_elements/__init__.py +0 -0
  23. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/disk_elements/disk_element_unit.py +0 -0
  24. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/disk_elements/hookimpls.py +0 -0
  25. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/__init__.py +0 -0
  26. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/boxes/__init__.py +0 -0
  27. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/boxes/box.py +0 -0
  28. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/boxes/constricted_box.py +0 -0
  29. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/boxes/upset_box.py +0 -0
  30. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/diamonds/__init__.py +0 -0
  31. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/diamonds/diamond.py +0 -0
  32. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/diamonds/gothic.py +0 -0
  33. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/diamonds/square.py +0 -0
  34. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/equivalent_ripped_groove.py +0 -0
  35. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/flat.py +0 -0
  36. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/generic_elongation_solvers.py +0 -0
  37. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/hexagonal.py +0 -0
  38. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/ovals/__init__.py +0 -0
  39. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/ovals/circular_oval.py +0 -0
  40. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/ovals/constricted_circular_oval.py +0 -0
  41. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/ovals/constricted_swedish_oval.py +0 -0
  42. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/ovals/flat_oval.py +0 -0
  43. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/ovals/oval_3radii.py +0 -0
  44. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/ovals/oval_3radii_flanked.py +0 -0
  45. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/ovals/swedish_oval.py +0 -0
  46. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/ovals/upset_oval.py +0 -0
  47. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/rounds/__init__.py +0 -0
  48. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/rounds/false_round.py +0 -0
  49. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/grooves/rounds/round.py +0 -0
  50. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/hooks.py +0 -0
  51. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/log.py +0 -0
  52. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/profile/__init__.py +0 -0
  53. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/profile/hookimpls.py +0 -0
  54. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll/__init__.py +0 -0
  55. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll/hookimpls.py +0 -0
  56. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll/roll.py +0 -0
  57. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/__init__.py +0 -0
  58. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/hookimpls/__init__.py +0 -0
  59. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/hookimpls/disk_element.py +0 -0
  60. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/hookimpls/profile.py +0 -0
  61. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/hookimpls/roll.py +0 -0
  62. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/hookimpls/roll_pass.py +0 -0
  63. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/roll_pass/three_roll_pass.py +0 -0
  64. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/rotator/__init__.py +0 -0
  65. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/rotator/hookimpls.py +0 -0
  66. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/rotator/rotator.py +0 -0
  67. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/sequence/__init__.py +0 -0
  68. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/sequence/hookimpls.py +0 -0
  69. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/sequence/sequence.py +0 -0
  70. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/shapes.py +0 -0
  71. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/transport/__init__.py +0 -0
  72. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/transport/cooling_pipe.py +0 -0
  73. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/transport/transport.py +0 -0
  74. {pyroll_core-2.1.3 → pyroll_core-2.1.4}/pyroll/core/unit/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyroll-core
3
- Version: 2.1.3
3
+ Version: 2.1.4
4
4
  Summary: PyRoll rolling simulation framework - core library.
5
5
  Project-URL: Homepage, https://pyroll-project.github.io
6
6
  Project-URL: Repository, https://github.com/pyroll-project/pyroll-core
@@ -29,8 +29,14 @@ Requires-Dist: ezdxf
29
29
  Requires-Dist: numpy~=1.19
30
30
  Requires-Dist: scipy~=1.9
31
31
  Requires-Dist: shapely~=2.0
32
+ Provides-Extra: matplotlib
33
+ Requires-Dist: matplotlib~=3.7; extra == 'matplotlib'
32
34
  Provides-Extra: plot
33
35
  Requires-Dist: matplotlib~=3.7; extra == 'plot'
36
+ Provides-Extra: plotly
37
+ Requires-Dist: kaleido; extra == 'plotly'
38
+ Requires-Dist: pandas~=2.0; extra == 'plotly'
39
+ Requires-Dist: plotly~=5.18; extra == 'plotly'
34
40
  Description-Content-Type: text/markdown
35
41
 
36
42
  # PyRolL Rolling Simulation Framework
@@ -38,6 +44,7 @@ Description-Content-Type: text/markdown
38
44
  [![PyPI](https://img.shields.io/pypi/v/pyroll-core)](https://pypi.org/project/pyroll-core/)
39
45
  ![Python Versions](https://img.shields.io/pypi/pyversions/pyroll-core)
40
46
  [![License](https://img.shields.io/pypi/l/pyroll-core)](LICENSE)
47
+ [![DOI](https://joss.theoj.org/papers/10.21105/joss.06200/status.svg)](https://doi.org/10.21105/joss.06200)
41
48
  ![Downloads](https://img.shields.io/pypi/dm/pyroll-core)
42
49
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/7971/badge)](https://www.bestpractices.dev/projects/7971)
43
50
 
@@ -3,6 +3,7 @@
3
3
  [![PyPI](https://img.shields.io/pypi/v/pyroll-core)](https://pypi.org/project/pyroll-core/)
4
4
  ![Python Versions](https://img.shields.io/pypi/pyversions/pyroll-core)
5
5
  [![License](https://img.shields.io/pypi/l/pyroll-core)](LICENSE)
6
+ [![DOI](https://joss.theoj.org/papers/10.21105/joss.06200/status.svg)](https://doi.org/10.21105/joss.06200)
6
7
  ![Downloads](https://img.shields.io/pypi/dm/pyroll-core)
7
8
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/7971/badge)](https://www.bestpractices.dev/projects/7971)
8
9
 
@@ -11,20 +11,19 @@ path = ".venv"
11
11
  dependencies = [
12
12
  "ipython ~= 8.0",
13
13
  "pytest ~= 7.0",
14
- "matplotlib ~= 3.6",
15
14
  "pyroll-report ~= 2.0.0",
16
- "jupyter"
15
+ "jupyter",
17
16
  ]
17
+ features = ["plotly", "matplotlib"]
18
18
 
19
19
  [envs.test]
20
20
  path = ""
21
21
  dependencies = [
22
22
  "pytest ~= 7.0",
23
- "matplotlib ~= 3.6",
24
23
  ]
25
24
 
26
25
  [[envs.test.matrix]]
27
- python = ["3.9", "3.10", "3.11"]
26
+ features = ["matplotlib", "plotly"]
28
27
 
29
28
  [envs.test.scripts]
30
29
  all = "pytest tests"
@@ -46,6 +46,14 @@ dynamic = ["version"]
46
46
  plot = [
47
47
  "matplotlib ~= 3.7"
48
48
  ]
49
+ matplotlib = [
50
+ "matplotlib ~= 3.7"
51
+ ]
52
+ plotly = [
53
+ "plotly ~= 5.18",
54
+ "pandas ~= 2.0",
55
+ "kaleido"
56
+ ]
49
57
 
50
58
  [project.urls]
51
59
  Homepage = "https://pyroll-project.github.io"
@@ -12,7 +12,7 @@ from .hooks import Hook, HookHost, HookFunction, root_hooks
12
12
  from .disk_elements import DiskElementUnit
13
13
  from .config import Config, config
14
14
 
15
- VERSION = "2.1.3"
15
+ VERSION = "2.1.4"
16
16
 
17
17
  root_hooks.extend(
18
18
  [
@@ -32,3 +32,16 @@ root_hooks.extend(
32
32
  PassSequence.log_elongation,
33
33
  ]
34
34
  )
35
+
36
+ # determine available plotting backend, plotly is preferred
37
+ try:
38
+ import plotly as _
39
+ PLOTTING_BACKEND = "plotly"
40
+
41
+ except ImportError:
42
+ try:
43
+ import matplotlib as _
44
+ PLOTTING_BACKEND = "matplotlib"
45
+
46
+ except ImportError:
47
+ PLOTTING_BACKEND = None
@@ -168,3 +168,25 @@ class Config:
168
168
 
169
169
  ROLL_SURFACE_DISCRETIZATION_COUNT = 100
170
170
  """Count of discrete points used to describe the roll surface."""
171
+
172
+ UNIVERSAL_GAS_CONSTANT = 8.31446261815324
173
+ """Universal gas constant R."""
174
+
175
+ BOLTZMANN_CONSTANT = 1.380649e-23
176
+ """Boltzmann constant kB."""
177
+
178
+ STEFAN_BOLTZMANN_CONSTANT = 5.670374419e-8
179
+ """Stefan-Boltzmann radiation constant σ."""
180
+
181
+ AVOGADRO_CONSTANT = 6.02214076e23
182
+ """Avogadro constant NA."""
183
+
184
+ STANDARD_GRAVITY = 9.80665
185
+ """Standard acceleration of gravity g0."""
186
+
187
+ PROFILE_CONTOUR_REFINEMENT = 0
188
+ """Refine the line string of profile contours with more intermediate points.
189
+ Higher integers mean finer. Values < 1 disable this feature."""
190
+
191
+ GROOVE_RADIUS_POINT_COUNT = 20
192
+ """Count of points used to represent the radii in a generic elongation groove as line string."""
@@ -65,22 +65,44 @@ class GrooveBase(ReprMixin):
65
65
  }
66
66
 
67
67
  def plot(self, **kwargs):
68
- try:
69
- import matplotlib.pyplot as plt
70
- except ImportError as e:
68
+ from pyroll.core import PLOTTING_BACKEND
69
+ if PLOTTING_BACKEND is None:
71
70
  raise RuntimeError(
72
- "This method is only available if matplotlib is installed in the environment. "
73
- "You may install it using the 'plot' extra of pyroll-core."
74
- ) from e
71
+ "This method is only available if matplotlib or plotly is installed in the environment. "
72
+ "You may install one of them using the 'plot', 'matplotlib' or 'plotly' extras of pyroll-core."
73
+ )
74
+
75
+ if PLOTTING_BACKEND == "matplotlib":
76
+ import matplotlib.pyplot as plt
77
+
78
+ fig: plt.Figure = plt.figure(**kwargs)
79
+ ax: plt.Axes = fig.subplots()
80
+
81
+ ax.set_ylabel("y")
82
+ ax.set_xlabel("z")
83
+
84
+ ax.set_aspect("equal", "datalim")
85
+ ax.grid(lw=0.5)
86
+
87
+ ax.plot(*self.contour_line.xy, color="k")
88
+ return fig
89
+
90
+ if PLOTTING_BACKEND == "plotly":
91
+ import plotly.express as px
92
+
93
+ fig = px.line(
94
+ x=self.contour_line.xy[0],
95
+ y=self.contour_line.xy[1],
96
+ labels={"y": "y", "x": "z"},
97
+ template="simple_white",
98
+ )
75
99
 
76
- fig: plt.Figure = plt.figure(**kwargs)
77
- ax: plt.Axes = fig.subplots()
100
+ fig.update_traces(line_color="black")
78
101
 
79
- ax.set_ylabel("y")
80
- ax.set_xlabel("z")
102
+ fig.update_yaxes(
103
+ scaleanchor="x",
104
+ scaleratio=1
105
+ )
81
106
 
82
- ax.set_aspect("equal", "datalim")
83
- ax.grid(lw=0.5)
107
+ return fig
84
108
 
85
- ax.plot(*self.contour_line.xy, color="k")
86
- return fig
@@ -193,22 +193,22 @@ class GenericElongationGroove(GrooveBase, ReprMixin):
193
193
  yield self.z0, self.y0
194
194
 
195
195
  if not np.isclose(self.z1, self.z3):
196
- for z in np.linspace(self.z1, self.z3, 20, endpoint=False):
196
+ for z in np.linspace(self.z1, self.z3, Config.GROOVE_RADIUS_POINT_COUNT, endpoint=False):
197
197
  yield z, self._r1_contour_line(z)
198
198
 
199
199
  if not np.isclose(self.z3, self.z4):
200
200
  yield self.z3, self.y3
201
201
 
202
202
  if not np.isclose(self.z4, self.z5):
203
- for z in np.linspace(self.z4, self.z5, 20, endpoint=False):
203
+ for z in np.linspace(self.z4, self.z5, Config.GROOVE_RADIUS_POINT_COUNT, endpoint=False):
204
204
  yield z, self._r2_contour_line(z)
205
205
 
206
206
  if not np.isclose(self.z5, self.z6):
207
- for z in np.linspace(self.z5, self.z6, 20, endpoint=False):
207
+ for z in np.linspace(self.z5, self.z6, Config.GROOVE_RADIUS_POINT_COUNT, endpoint=False):
208
208
  yield z, self._r3_contour_line(z)
209
209
 
210
210
  if not np.isclose(self.z6, self.z7):
211
- for z in np.linspace(self.z6, self.z7, 20, endpoint=False):
211
+ for z in np.linspace(self.z6, self.z7, Config.GROOVE_RADIUS_POINT_COUNT, endpoint=False):
212
212
  yield z, self._r4_contour_line(z)
213
213
 
214
214
  yield self.z9, self.y9
@@ -1,9 +1,9 @@
1
- from typing import Union, Tuple, Iterable, Optional
2
-
3
1
  import numpy as np
4
2
  import scipy.interpolate
3
+ from pathlib import Path
5
4
  from ezdxf import readfile
6
- from shapely.geometry import Polygon, LineString, Point
5
+ from shapely.geometry import Polygon, LineString
6
+ from typing import Union, Tuple, Iterable, Optional
7
7
 
8
8
  from pyroll.core.grooves import GrooveBase
9
9
 
@@ -93,7 +93,7 @@ class SplineGroove(GrooveBase):
93
93
  return self._local_depth(z)
94
94
 
95
95
  @classmethod
96
- def from_dxf_drawing(cls, filepath: str, classifiers: Iterable[str], usable_width: Optional[float] = None,
96
+ def from_dxf_drawing(cls, filepath: Path, classifiers: Iterable[str], usable_width: Optional[float] = None,
97
97
  arc_discretization: int = 1000) -> 'SplineGroove':
98
98
  """
99
99
  Creates a spline groove with a given contour line as defined inside the .dxf drawing.
@@ -7,6 +7,7 @@ from shapely.affinity import translate, rotate
7
7
  from shapely.geometry import Point, LinearRing, Polygon, LineString
8
8
  from shapely.ops import clip_by_rect, unary_union
9
9
 
10
+ from ..config import Config
10
11
  from ..grooves import GrooveBase
11
12
  from ..hooks import HookHost, Hook
12
13
 
@@ -106,6 +107,30 @@ class Profile(HookHost):
106
107
  scale_thickness = Hook[float]()
107
108
  """Thickness of the scale covering the profile."""
108
109
 
110
+ deformation_activation_energy = Hook[float]()
111
+ """Activation energy of deformation especially for calculation of Zener-Holomon-Parameter."""
112
+
113
+ martensite_start_temperature = Hook[float]()
114
+ """Martensite start temperature for the given chemical composition of the profile."""
115
+
116
+ martensite_finish_temperature = Hook[float]()
117
+ """Martensite finish temperature for the given chemical composition of the profile."""
118
+
119
+ bainite_start_temperature = Hook[float]()
120
+ """Bainite start temperature for the given chemical composition of the profile."""
121
+
122
+ bainite_finish_temperature = Hook[float]()
123
+ """Bainite finish temperature for the given chemical composition of the profile."""
124
+
125
+ ae1_temperature = Hook[float]()
126
+ """Temperature at which austenite starts to transform into a mixture of ferrite and cementite during cooling."""
127
+
128
+ ae3_temperature = Hook[float]()
129
+ """Temperature above which the transformation of austenite into a mixture of ferrite and cementite is complete during cooling."""
130
+
131
+ vickers_hardness = Hook[float]()
132
+ """Vickers hardness of the cold profile material."""
133
+
109
134
  def __init__(self, **kwargs):
110
135
  """Using the ``__init__`` is not recommended, use one of the factory class methods instead."""
111
136
  self.t = 0
@@ -190,7 +215,7 @@ class Profile(HookHost):
190
215
  polygon = clip_by_rect(poly, -width / 2, -math.inf, width / 2, math.inf)
191
216
 
192
217
  return cls(
193
- cross_section=polygon,
218
+ cross_section=refine_cross_section(polygon),
194
219
  classifiers=set(groove.classifiers),
195
220
  **kwargs
196
221
  )
@@ -340,7 +365,6 @@ class Profile(HookHost):
340
365
  :raises ValueError: if arguments are out of range
341
366
  """
342
367
 
343
-
344
368
  return HexagonProfile(side, height, diagonal, corner_radius, **kwargs)
345
369
 
346
370
  def local_height(self, z: float) -> float:
@@ -390,26 +414,47 @@ class Profile(HookHost):
390
414
  raise ValueError("Value of self.material is neither a string or a collection of strings.")
391
415
 
392
416
  def plot(self, **kwargs):
393
- try:
394
- import matplotlib.pyplot as plt
395
- except ImportError as e:
417
+ from pyroll.core import PLOTTING_BACKEND
418
+ if PLOTTING_BACKEND is None:
396
419
  raise RuntimeError(
397
- "This method is only available if matplotlib is installed in the environment. "
398
- "You may install it using the 'plot' extra of pyroll-core."
399
- ) from e
420
+ "This method is only available if matplotlib or plotly is installed in the environment. "
421
+ "You may install one of them using the 'plot', 'matplotlib' or 'plotly' extras of pyroll-core."
422
+ )
400
423
 
401
- fig: plt.Figure = plt.figure(**kwargs)
402
- ax: plt.Axes = fig.subplots()
424
+ if PLOTTING_BACKEND == "matplotlib":
425
+ import matplotlib.pyplot as plt
426
+
427
+ fig: plt.Figure = plt.figure(**kwargs)
428
+ ax: plt.Axes = fig.subplots()
429
+
430
+ ax.set_ylabel("y")
431
+ ax.set_xlabel("z")
432
+
433
+ ax.set_aspect("equal", "datalim")
434
+ ax.grid(lw=0.5)
435
+
436
+ ax.plot(*self.cross_section.boundary.xy, color="k")
437
+ ax.fill(*self.cross_section.boundary.xy, color="k", alpha=0.5)
438
+ return fig
439
+
440
+ if PLOTTING_BACKEND == "plotly":
441
+ import plotly.express as px
403
442
 
404
- ax.set_ylabel("y")
405
- ax.set_xlabel("z")
443
+ fig = px.line(
444
+ x=self.cross_section.boundary.xy[0],
445
+ y=self.cross_section.boundary.xy[1],
446
+ labels={"y": "y", "x": "z"},
447
+ template="simple_white",
448
+ )
449
+
450
+ fig.data[0].fill = "toself"
406
451
 
407
- ax.set_aspect("equal", "datalim")
408
- ax.grid(lw=0.5)
452
+ fig.update_yaxes(
453
+ scaleanchor="x",
454
+ scaleratio=1
455
+ )
409
456
 
410
- ax.plot(*self.cross_section.boundary.xy, color="k")
411
- ax.fill(*self.cross_section.boundary.xy, color="k", alpha=0.5)
412
- return fig
457
+ return fig
413
458
 
414
459
 
415
460
  class RoundProfile(Profile):
@@ -447,7 +492,7 @@ class RoundProfile(Profile):
447
492
  circle = center.buffer(radius)
448
493
 
449
494
  super().__init__(
450
- cross_section=circle,
495
+ cross_section=refine_cross_section(circle),
451
496
  classifiers={"round"},
452
497
  **kwargs
453
498
  )
@@ -506,7 +551,7 @@ class BoxProfile(Profile):
506
551
  polygon = polygon.buffer(corner_radius)
507
552
 
508
553
  super().__init__(
509
- cross_section=polygon,
554
+ cross_section=refine_cross_section(polygon),
510
555
  classifiers={"box"},
511
556
  **kwargs
512
557
  )
@@ -560,7 +605,7 @@ class DiamondProfile(Profile):
560
605
  polygon = polygon.buffer(corner_radius)
561
606
 
562
607
  super().__init__(
563
- cross_section=polygon,
608
+ cross_section=refine_cross_section(polygon),
564
609
  classifiers={"diamond"},
565
610
  **kwargs
566
611
  )
@@ -621,7 +666,7 @@ class SquareProfile(Profile):
621
666
  polygon = polygon.buffer(corner_radius)
622
667
 
623
668
  super().__init__(
624
- cross_section=polygon,
669
+ cross_section=refine_cross_section(polygon),
625
670
  classifiers={"square", "diamond"},
626
671
  **kwargs
627
672
  )
@@ -713,7 +758,7 @@ class HexagonProfile(Profile):
713
758
  polygon = polygon.buffer(corner_radius)
714
759
 
715
760
  super().__init__(
716
- cross_section=polygon,
761
+ cross_section=refine_cross_section(polygon),
717
762
  classifiers={"hexagon"},
718
763
  **kwargs
719
764
  )
@@ -739,3 +784,10 @@ class HexagonProfile(Profile):
739
784
  side=self._side,
740
785
  diagonal=self._diagonal,
741
786
  corner_radius=self._corner_radius)
787
+
788
+
789
+ def refine_cross_section(cross_section: Polygon):
790
+ if Config.PROFILE_CONTOUR_REFINEMENT < 1:
791
+ return cross_section
792
+
793
+ return cross_section.segmentize(cross_section.boundary.length / Config.PROFILE_CONTOUR_REFINEMENT)
@@ -48,38 +48,53 @@ class ReprMixin(ABC):
48
48
  def _repr_html_(self):
49
49
  """HTML repr for IPython."""
50
50
 
51
- buf = [f"<table><tr><th colspan=2 style='text-align:center'>{html.escape(str(self), True)}</th></tr>"]
51
+ buf = [f"<details><summary style='font-weight:bold'>{html.escape(str(self), True)}</summary><table>"]
52
52
 
53
53
  for name, value in sorted(self.__attrs__.items()):
54
54
  if hasattr(value, "_repr_html_"):
55
55
  r = value._repr_html_()
56
+ elif isinstance(value, list) or isinstance(value, set):
57
+ item_reprs = (
58
+ item._repr_html_() if hasattr(item, "_repr_html_") else html.escape(repr(item), True)
59
+ for item in value
60
+ )
61
+ r = f"<table style='margin:0'>{''.join(f'<tr><td>{item}</td></tr>' for item in item_reprs)}</table>"
56
62
  else:
57
63
  r = html.escape(repr(value), True)
58
64
  buf.append(f"<tr><td style='text-align:left'>{html.escape(name, True)}</td><td>{r}</td></tr>")
59
65
 
60
- buf.append("</table>")
66
+ buf.append("</table></details>")
61
67
 
62
68
  table = ''.join(buf)
63
69
 
64
70
  try:
65
71
  plot = self.plot()
66
72
 
67
- import matplotlib.pyplot as plt
68
- import re
73
+ from pyroll.core import PLOTTING_BACKEND
69
74
 
70
- with StringIO() as sio:
71
- plot.savefig(sio, format="svg")
72
- plt.close(plot)
75
+ if PLOTTING_BACKEND == "matplotlib":
76
+ import matplotlib.pyplot as plt
77
+ with StringIO() as sio:
78
+ plot.savefig(sio, format="svg")
73
79
  svg = sio.getvalue()
74
-
75
- return (
76
- "<table>"
77
- + "<tr><td style='text-align: center'>" + svg + "</td></tr>"
78
- + "<tr><td style='text-align: left'>" + table + "</td></tr>"
79
- + "</table>"
80
- )
81
-
82
- except (NotImplementedError, ImportError, AttributeError, TypeError):
80
+ plt.close(plot)
81
+ if PLOTTING_BACKEND == "plotly":
82
+ from plotly.io import to_image
83
+ svg = to_image(
84
+ plot,
85
+ format="svg",
86
+ width=600,
87
+ height=400
88
+ ).decode("utf-8")
89
+
90
+ return (
91
+ "<table>"
92
+ + "<tr><td style='text-align: center'>" + svg + "</td></tr>"
93
+ + "<tr><td style='text-align: left'>" + table + "</td></tr>"
94
+ + "</table>"
95
+ )
96
+
97
+ except (NotImplementedError, ImportError, AttributeError, TypeError) as e:
83
98
  return table
84
99
 
85
100
  def __rich_repr__(self):
@@ -54,6 +54,9 @@ class DeformationUnit(Unit):
54
54
  deformation_resistance = Hook[float]()
55
55
  """Equivalent deformation resistance (mean flow stress increased by deformation efficiency)."""
56
56
 
57
+ zener_holomon_parameter = Hook[float]()
58
+ """Temperature corrected strain rate acc. to Zener and Holomon."""
59
+
57
60
  class Profile(Unit.Profile):
58
61
  """Represents a profile in context of a deformation unit."""
59
62
 
@@ -1,6 +1,7 @@
1
1
  import numpy as np
2
2
 
3
3
  from ..roll_pass import DeformationUnit
4
+ from ...config import Config
4
5
 
5
6
 
6
7
  @DeformationUnit.draught
@@ -76,3 +77,10 @@ def strain_rate(self: DeformationUnit):
76
77
  @DeformationUnit.free_surface_area
77
78
  def free_surface_area(self: DeformationUnit):
78
79
  return self.surface_area - self.contact_area
80
+
81
+
82
+ @DeformationUnit.zener_holomon_parameter
83
+ def zener_holomon_parameter(self: DeformationUnit):
84
+ return self.strain_rate * np.exp(self.in_profile.deformation_activation_energy / (
85
+ Config.UNIVERSAL_GAS_CONSTANT * self.in_profile.temperature
86
+ ))
@@ -6,11 +6,12 @@ from shapely.affinity import rotate
6
6
 
7
7
  from ..roll_pass import RollPass
8
8
  from ..three_roll_pass import ThreeRollPass
9
+ from ...profile.profile import refine_cross_section
9
10
 
10
11
 
11
12
  def out_cross_section(rp: RollPass, width: float) -> Polygon:
12
13
  poly = Polygon(np.concatenate([cl.coords for cl in rp.contour_lines]))
13
- return clip_by_rect(poly, -width / 2, -math.inf, width / 2, math.inf)
14
+ return refine_cross_section(clip_by_rect(poly, -width / 2, -math.inf, width / 2, math.inf))
14
15
 
15
16
 
16
17
  def out_cross_section3(rp: ThreeRollPass, width: float) -> Polygon:
@@ -20,4 +21,4 @@ def out_cross_section3(rp: ThreeRollPass, width: float) -> Polygon:
20
21
  poly = clip_by_rect(poly, -math.inf, -width / 2, math.inf, math.inf)
21
22
  poly = rotate(poly, angle=120, origin=(0, 0))
22
23
 
23
- return poly
24
+ return refine_cross_section(poly)
@@ -96,6 +96,9 @@ class RollPass(DiskElementUnit, DeformationUnit):
96
96
  forward_slip_ratio = Hook[float]()
97
97
  """Ratio of forward slip of the roll gap."""
98
98
 
99
+ location = Hook[float]()
100
+ """Coordinate of the passes high point in rolling direction."""
101
+
99
102
  def __init__(
100
103
  self,
101
104
  roll: BaseRoll,
@@ -234,27 +237,12 @@ class RollPass(DiskElementUnit, DeformationUnit):
234
237
  """Represents an outgoing profile of a disk element unit."""
235
238
 
236
239
  def plot(self, **kwargs):
237
- try:
238
- import matplotlib.pyplot as plt
239
- except ImportError as e:
240
+ from pyroll.core import PLOTTING_BACKEND
241
+ if PLOTTING_BACKEND is None:
240
242
  raise RuntimeError(
241
- "This method is only available if matplotlib is installed in the environment. "
242
- "You may install it using the 'plot' extra of pyroll-core."
243
- ) from e
244
-
245
- fig: plt.Figure = plt.figure(**kwargs)
246
- ax: plt.Axes
247
- axl: plt.Axes
248
- ax = fig.subplots()
249
-
250
- if self.label:
251
- ax.set_title(f"Roll Pass '{self.label}'")
252
-
253
- ax.set_ylabel("y")
254
- ax.set_xlabel("z")
255
-
256
- ax.set_aspect("equal", "datalim")
257
- ax.grid(lw=0.5)
243
+ "This method is only available if matplotlib or plotly is installed in the environment. "
244
+ "You may install one of them using the 'plot', 'matplotlib' or 'plotly' extras of pyroll-core."
245
+ )
258
246
 
259
247
  def oriented(geom):
260
248
  orientation = self.orientation
@@ -271,38 +259,124 @@ class RollPass(DiskElementUnit, DeformationUnit):
271
259
  return rotate(geom, orientation, (0, 0))
272
260
  return geom
273
261
 
274
- artists = []
275
-
276
- if self.in_profile:
277
- artists += ax.fill(
278
- *oriented(self.in_profile.cross_section.boundary).xy,
279
- alpha=0.5, color="red", label="in profile"
280
- )
281
- artists += ax.fill(
282
- *oriented(self.in_profile.equivalent_rectangle.boundary).xy,
283
- fill=False, color="red", ls="--", label="in eq. rectangle"
284
- )
285
-
286
- if self.out_profile:
287
- artists += ax.fill(
288
- *oriented(self.out_profile.cross_section.boundary).xy,
289
- alpha=0.5, color="blue", label="out profile"
290
- )
291
- artists += ax.fill(
292
- *oriented(self.out_profile.equivalent_rectangle.boundary).xy,
293
- fill=False, color="blue", ls="--", label="out eq. rectangle"
294
- )
295
-
296
- c = None
297
- for cl in self.contour_lines:
298
- c = ax.plot(*oriented(cl).xy, color="k", label="roll surface")
299
-
300
- if c is not None:
301
- artists += c
302
-
303
- ncols = len(artists) // 2 + 1
304
-
305
- ax.legend(handles=artists, ncols=ncols, loc='lower center')
306
- fig.set_layout_engine('constrained')
262
+ if PLOTTING_BACKEND == "matplotlib":
263
+ import matplotlib.pyplot as plt
307
264
 
308
- return fig
265
+ fig: plt.Figure = plt.figure(**kwargs)
266
+ ax: plt.Axes
267
+ axl: plt.Axes
268
+ ax = fig.subplots()
269
+
270
+ if self.label:
271
+ ax.set_title(f"Roll Pass '{self.label}'")
272
+
273
+ ax.set_ylabel("y")
274
+ ax.set_xlabel("z")
275
+
276
+ ax.set_aspect("equal", "datalim")
277
+ ax.grid(lw=0.5)
278
+
279
+ artists = []
280
+
281
+ if self.in_profile:
282
+ artists += ax.fill(
283
+ *oriented(self.in_profile.cross_section.boundary).xy,
284
+ alpha=0.5, color="red", label="in profile"
285
+ )
286
+ artists += ax.fill(
287
+ *oriented(self.in_profile.equivalent_rectangle.boundary).xy,
288
+ fill=False, color="red", ls="--", label="in eq. rectangle"
289
+ )
290
+
291
+ if self.out_profile:
292
+ artists += ax.fill(
293
+ *oriented(self.out_profile.cross_section.boundary).xy,
294
+ alpha=0.5, color="blue", label="out profile"
295
+ )
296
+ artists += ax.fill(
297
+ *oriented(self.out_profile.equivalent_rectangle.boundary).xy,
298
+ fill=False, color="blue", ls="--", label="out eq. rectangle"
299
+ )
300
+
301
+ c = None
302
+ for cl in self.contour_lines:
303
+ c = ax.plot(*oriented(cl).xy, color="k", label="roll surface")
304
+
305
+ if c is not None:
306
+ artists += c
307
+
308
+ ncols = len(artists) // 2 + 1
309
+
310
+ ax.legend(handles=artists, ncols=ncols, loc='lower center')
311
+ fig.set_layout_engine('constrained')
312
+
313
+ return fig
314
+
315
+ if PLOTTING_BACKEND == "plotly":
316
+ import plotly.graph_objects as go
317
+
318
+ fig = go.Figure(layout=go.Layout(
319
+ xaxis=dict(
320
+ title="z",
321
+ ),
322
+ yaxis=dict(
323
+ title="y",
324
+ scaleanchor="x",
325
+ scaleratio=1
326
+ ),
327
+ title=f"Roll Pass '{self.label}'" if self.label else None,
328
+ template="simple_white"
329
+ ))
330
+
331
+ if self.in_profile:
332
+ coords = oriented(self.in_profile.cross_section.boundary).xy
333
+ fig.add_trace(go.Scatter(
334
+ x=np.array(coords[0]),
335
+ y=np.array(coords[1]),
336
+ mode="lines",
337
+ fill="toself",
338
+ line=dict(color="red"),
339
+ name="in profile"
340
+ ))
341
+ coords = oriented(self.in_profile.equivalent_rectangle.boundary).xy
342
+ fig.add_trace(go.Scatter(
343
+ x=np.array(coords[0]),
344
+ y=np.array(coords[1]),
345
+ mode="lines",
346
+ line=dict(color="red", dash="dash"),
347
+ name="in eq. rectangle"
348
+ ))
349
+
350
+ if self.out_profile:
351
+ coords = oriented(self.out_profile.cross_section.boundary).xy
352
+ fig.add_trace(go.Scatter(
353
+ x=np.array(coords[0]),
354
+ y=np.array(coords[1]),
355
+ mode="lines",
356
+ fill="toself",
357
+ line=dict(color="blue"),
358
+ name="out profile"
359
+ ))
360
+ coords = oriented(self.out_profile.equivalent_rectangle.boundary).xy
361
+ fig.add_trace(go.Scatter(
362
+ x=np.array(coords[0]),
363
+ y=np.array(coords[1]),
364
+ mode="lines",
365
+ line=dict(color="blue", dash="dash"),
366
+ name="out eq. rectangle"
367
+ ))
368
+
369
+ show_in_legend = True
370
+ for cl in self.contour_lines:
371
+ coords = oriented(cl).xy
372
+ fig.add_trace(go.Scatter(
373
+ x=np.array(coords[0]),
374
+ y=np.array(coords[1]),
375
+ mode="lines",
376
+ line=dict(color="black"),
377
+ name="roll surface",
378
+ showlegend=show_in_legend
379
+ ))
380
+ show_in_legend = False
381
+
382
+ return fig
@@ -0,0 +1,52 @@
1
+ from .transport import Transport
2
+
3
+
4
+ @Transport.OutProfile.strain
5
+ def out_strain(self: Transport.OutProfile):
6
+ """Assume total recrystallization during transport."""
7
+ return 0
8
+
9
+
10
+ @Transport.duration
11
+ def duration(self: Transport):
12
+ if self.has_set_or_cached("length"):
13
+ return self.length / self.velocity
14
+
15
+
16
+ @Transport.velocity(
17
+ trylast=True # do not override getting from in_profile
18
+ )
19
+ def conti_velocity(self: Transport):
20
+ if self.has_set_or_cached("length"): # probably indicates conti process
21
+ return self.prev.velocity
22
+
23
+
24
+ @Transport.environment_temperature
25
+ def environment_temperature(self):
26
+ return 293
27
+
28
+
29
+ @Transport.length
30
+ def length_from_roll_pass_positions(self: Transport, cycle):
31
+ if cycle:
32
+ return None
33
+
34
+ from ..roll_pass import RollPass
35
+ next_pass = self.next_of(RollPass)
36
+ prev_pass = self.prev_of(RollPass)
37
+
38
+ if next_pass.has_value("location") and prev_pass.has_value("location"):
39
+ entry = next_pass.entry_point if next_pass.has_value("entry_point") else 0
40
+ length = 0
41
+
42
+ current = self
43
+ while not (current.next is next_pass):
44
+ length += current.next.length
45
+ current = current.next
46
+
47
+ current = self
48
+ while not (current.prev is prev_pass):
49
+ length += current.prev.length
50
+ current = current.prev
51
+
52
+ return next_pass.location - prev_pass.location + entry - length
@@ -23,20 +23,20 @@ def surface_area(self: Unit):
23
23
 
24
24
 
25
25
  @Unit.length
26
- def length(self: Unit):
27
- if self.has_set_or_cached("duration"):
26
+ def length(self: Unit, cycle):
27
+ if not cycle and self.has_value("duration"):
28
28
  return self.velocity * self.duration
29
29
 
30
30
 
31
31
  @Unit.duration
32
- def duration(self: Unit):
33
- if self.has_set_or_cached("length"):
32
+ def duration(self: Unit, cycle):
33
+ if not cycle and self.has_value("length"):
34
34
  return self.length / self.velocity
35
35
 
36
36
 
37
37
  @Unit.velocity
38
38
  def velocity(self: Unit):
39
- if self.in_profile.has_set_or_cached("velocity"):
39
+ if self.in_profile.has_value("velocity"):
40
40
  return self.in_profile.velocity
41
41
 
42
42
 
@@ -218,6 +218,20 @@ class Unit(HookHost):
218
218
  """List of the subunits."""
219
219
  return self._subunits
220
220
 
221
+ @property
222
+ def profiles(self) -> List[BaseProfile]:
223
+ """Returns a list of all profiles appearing in this unit.
224
+ The list contains in and out profiles of all subunits and this unit itself in the order of rolling direction.
225
+ This property recurses into all subunits."""
226
+
227
+ def _yield_profiles():
228
+ yield self.in_profile
229
+ for u in self._subunits:
230
+ yield from u.profiles
231
+ yield self.out_profile
232
+
233
+ return list(_yield_profiles())
234
+
221
235
  class Profile(BaseProfile):
222
236
  """Represents a profile in context of a unit."""
223
237
 
@@ -1,26 +0,0 @@
1
- from .transport import Transport
2
-
3
-
4
- @Transport.OutProfile.strain
5
- def out_strain(self: Transport.OutProfile):
6
- """Assume total recrystallization during transport."""
7
- return 0
8
-
9
-
10
- @Transport.duration
11
- def duration(self: Transport):
12
- if self.has_set_or_cached("length"):
13
- return self.length / self.velocity
14
-
15
-
16
- @Transport.velocity(
17
- trylast=True # do not override getting from in_profile
18
- )
19
- def conti_velocity(self: Transport):
20
- if self.has_set_or_cached("length"): # probably indicates conti process
21
- return self.prev.velocity
22
-
23
-
24
- @Transport.environment_temperature
25
- def environment_temperature(self):
26
- return 293
File without changes
File without changes