pyroll-core 3.2.0b2__tar.gz → 3.2.0b3__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 (102) hide show
  1. pyroll_core-3.2.0b3/.gitignore +0 -0
  2. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/PKG-INFO +1 -1
  3. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/__init__.py +6 -10
  4. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/engine/hookimpls.py +4 -0
  5. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/hooks.py +33 -22
  6. pyroll_core-3.2.0b3/pyroll/core/roll_pass/hookimpls/asymmetric_two_roll_pass.py +175 -0
  7. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/deformation_unit.py +5 -1
  8. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/engine.py +6 -6
  9. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/helpers.py +18 -0
  10. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/profile.py +3 -2
  11. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/sequence/sequence.py +8 -46
  12. pyroll_core-3.2.0b3/pyroll/core/transport/__init__.py +9 -0
  13. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/transport/hookimpls/__init__.py +2 -0
  14. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/transport/hookimpls/cooling_pipe.py +2 -1
  15. pyroll_core-3.2.0b3/pyroll/core/transport/hookimpls/induction_furnace.py +23 -0
  16. pyroll_core-3.2.0b3/pyroll/core/transport/hookimpls/spooler.py +170 -0
  17. pyroll_core-3.2.0b3/pyroll/core/transport/hookimpls/stelmor_conveyor.py +11 -0
  18. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/transport/hookimpls/transport.py +10 -2
  19. pyroll_core-3.2.0b3/pyroll/core/transport/induction_furnace.py +81 -0
  20. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/transport/spooler.py +21 -0
  21. pyroll_core-3.2.0b3/pyroll/core/transport/stelmor_conveyor.py +71 -0
  22. pyroll_core-3.2.0b2/.gitignore +0 -432
  23. pyroll_core-3.2.0b2/pyroll/core/roll_pass/hookimpls/asymmetric_two_roll_pass.py +0 -194
  24. pyroll_core-3.2.0b2/pyroll/core/transport/__init__.py +0 -7
  25. pyroll_core-3.2.0b2/pyroll/core/transport/hookimpls/spooler.py +0 -68
  26. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/LICENSE +0 -0
  27. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/README.md +0 -0
  28. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/hatch.toml +0 -0
  29. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyproject.toml +0 -0
  30. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/config.py +0 -0
  31. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/disk_elements/__init__.py +0 -0
  32. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/disk_elements/disk_element_unit.py +0 -0
  33. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/disk_elements/hookimpls.py +0 -0
  34. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/engine/__init__.py +0 -0
  35. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/engine/engine.py +0 -0
  36. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/__init__.py +0 -0
  37. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/angles/__init__.py +0 -0
  38. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/angles/_geometry.py +0 -0
  39. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/angles/_path.py +0 -0
  40. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/angles/butterfly_groove.py +0 -0
  41. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/angles/factories.py +0 -0
  42. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/base.py +0 -0
  43. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/boxes/__init__.py +0 -0
  44. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/boxes/box.py +0 -0
  45. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/boxes/constricted_box.py +0 -0
  46. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/boxes/upset_box.py +0 -0
  47. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/diamonds/__init__.py +0 -0
  48. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/diamonds/diamond.py +0 -0
  49. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/diamonds/gothic.py +0 -0
  50. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/diamonds/square.py +0 -0
  51. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/equivalent_ripped_groove.py +0 -0
  52. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/flat.py +0 -0
  53. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/generic_angle.py +0 -0
  54. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/generic_elongation.py +0 -0
  55. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/generic_elongation_solvers.py +0 -0
  56. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/hexagonal.py +0 -0
  57. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/ovals/__init__.py +0 -0
  58. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/ovals/circular_oval.py +0 -0
  59. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/ovals/constricted_circular_oval.py +0 -0
  60. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/ovals/constricted_swedish_oval.py +0 -0
  61. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/ovals/flat_oval.py +0 -0
  62. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/ovals/oval_3radii.py +0 -0
  63. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/ovals/oval_3radii_flanked.py +0 -0
  64. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/ovals/swedish_oval.py +0 -0
  65. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/ovals/upset_oval.py +0 -0
  66. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/rounds/__init__.py +0 -0
  67. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/rounds/false_round.py +0 -0
  68. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/rounds/round.py +0 -0
  69. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/grooves/spline.py +0 -0
  70. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/log.py +0 -0
  71. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/profile/__init__.py +0 -0
  72. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/profile/hookimpls.py +0 -0
  73. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/profile/profile.py +0 -0
  74. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/repr.py +0 -0
  75. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll/__init__.py +0 -0
  76. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll/hookimpls.py +0 -0
  77. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll/roll.py +0 -0
  78. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/__init__.py +0 -0
  79. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/asymmetric_two_roll_pass.py +0 -0
  80. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/base.py +0 -0
  81. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/deformation_unit.py +0 -0
  82. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/__init__.py +0 -0
  83. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/base_roll_pass.py +0 -0
  84. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/disk_element.py +0 -0
  85. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/roll.py +0 -0
  86. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/symmetric_roll_pass.py +0 -0
  87. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/three_roll_pass.py +0 -0
  88. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/hookimpls/two_roll_pass.py +0 -0
  89. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/symmetric_roll_pass.py +0 -0
  90. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/three_roll_pass.py +0 -0
  91. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/roll_pass/two_roll_pass.py +0 -0
  92. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/rotator/__init__.py +0 -0
  93. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/rotator/hookimpls.py +0 -0
  94. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/rotator/rotator.py +0 -0
  95. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/sequence/__init__.py +0 -0
  96. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/sequence/hookimpls.py +0 -0
  97. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/shapes.py +0 -0
  98. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/transport/cooling_pipe.py +0 -0
  99. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/transport/transport.py +0 -0
  100. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/unit/__init__.py +0 -0
  101. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/unit/hookimpls.py +0 -0
  102. {pyroll_core-3.2.0b2 → pyroll_core-3.2.0b3}/pyroll/core/unit/unit.py +1 -1
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyroll-core
3
- Version: 3.2.0b2
3
+ Version: 3.2.0b3
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,15 +29,8 @@ from .grooves import (
29
29
  butterfly_grooves,
30
30
  create_groove_by_type_name,
31
31
  )
32
- from .transport import Transport, CoolingPipe, Spooler
33
- from .roll_pass import (
34
- BaseRollPass,
35
- DeformationUnit,
36
- ThreeRollPass,
37
- SymmetricRollPass,
38
- TwoRollPass,
39
- AsymmetricTwoRollPass,
40
- )
32
+ from .transport import Transport, CoolingPipe, Spooler, StelmorConveyor, InductionFurnace
33
+ from .roll_pass import BaseRollPass, DeformationUnit, ThreeRollPass, SymmetricRollPass, TwoRollPass, AsymmetricTwoRollPass
41
34
  from .roll_pass import TwoRollPass as RollPass
42
35
  from .unit import Unit
43
36
  from .roll import Roll
@@ -55,7 +48,7 @@ from .hooks import Hook, HookHost, HookFunction, root_hooks
55
48
  from .disk_elements import DiskElementUnit
56
49
  from .config import Config, config, PlottingBackend, ConfigValue, ConfigMeta
57
50
 
58
- VERSION = "3.2.0b2"
51
+ VERSION = "3.2.0b3"
59
52
 
60
53
  __all__ = [
61
54
  # grooves
@@ -98,6 +91,8 @@ __all__ = [
98
91
  "Transport",
99
92
  "CoolingPipe",
100
93
  "Spooler",
94
+ "StelmorConveyor",
95
+ "InductionFurnace",
101
96
  # roll_pass
102
97
  "RollPass",
103
98
  "BaseRollPass",
@@ -129,6 +124,7 @@ __all__ = [
129
124
 
130
125
  root_hooks.extend(
131
126
  [
127
+ AsymmetricTwoRollPass.InProfile.pass_line,
132
128
  AsymmetricTwoRollPass.InProfile.cross_section,
133
129
  BaseRollPass.roll_force,
134
130
  SymmetricRollPass.Roll.roll_torque,
@@ -3,6 +3,10 @@ import numpy as np
3
3
  from .engine import Engine
4
4
  from ..transport import Transport
5
5
 
6
+ @Engine.gear_ratio
7
+ def default_gear_ratio(self: Engine):
8
+ return 1
9
+
6
10
  @Engine.gear_box_efficiency
7
11
  def gear_box_efficiency_becker(self: Engine):
8
12
  return 0.95
@@ -448,27 +448,44 @@ class HookHost(ReprMixin, LogMixin, metaclass=_HookHostMeta):
448
448
  return None
449
449
 
450
450
  def evaluate_and_set_hooks(self):
451
- """Evaluate functions of root hooks and set the results explicitly as attributes."""
451
+ """Evaluate functions of root hooks and set the results explicitly as attributes.
452
+
453
+ All applicable root hooks are evaluated first, against the state left by the previous
454
+ call (so their hook functions never observe a partially updated mix of this iteration's
455
+ and the previous iteration's root hook values), and only afterward set as attributes.
456
+ Committing them one by one instead would make the result depend on the arbitrary order
457
+ of ``root_hooks``: a hook function reading another root hook registered later in that
458
+ list would always see last iteration's value, while one reading a hook registered
459
+ earlier would see this iteration's, an inconsistency root hook functions must not have to
460
+ account for.
461
+ """
452
462
 
453
- def _gen():
454
- for h in root_hooks:
455
- if issubclass(type(self), h.owner):
456
- h = getattr(type(self), h.name)
457
- result = h.get_result(self)
463
+ applicable_hooks = [
464
+ getattr(type(self), h.name) for h in root_hooks if issubclass(type(self), h.owner)
465
+ ]
466
+
467
+ results = {}
468
+ for h in applicable_hooks:
469
+ result = h.get_result(self)
458
470
 
459
- if result is None:
460
- result = self.root_hook_fallback(h)
471
+ if result is None:
472
+ result = self.root_hook_fallback(h)
461
473
 
462
- if result is None:
463
- raise AttributeError(f"Call for root hook '{h.name}' on '{self}' resulted in None.")
474
+ if result is None:
475
+ raise AttributeError(f"Call for root hook '{h.name}' on '{self}' resulted in None.")
464
476
 
465
- setattr(self, h.name, result)
477
+ results[h.name] = result
466
478
 
467
- try:
468
- arr = np.array(result)
469
- yield from arr[np.isfinite(arr) | np.isinf(arr) | np.isnan(arr)].flat
470
- except TypeError: # yield only numeric values
471
- continue
479
+ for name, result in results.items():
480
+ setattr(self, name, result)
481
+
482
+ def _gen():
483
+ for result in results.values():
484
+ try:
485
+ arr = np.array(result)
486
+ yield from arr[np.isfinite(arr) | np.isinf(arr) | np.isnan(arr)].flat
487
+ except TypeError: # yield only numeric values
488
+ continue
472
489
 
473
490
  return list(_gen())
474
491
 
@@ -516,9 +533,3 @@ class _RootHooksList(list):
516
533
 
517
534
 
518
535
  root_hooks = _RootHooksList() # filled in __init__.py due to circular imports
519
- """
520
- List of hooks to call explicitly in each solution iteration.
521
- Their values will be treated as explicitly set but reevaluated in every iteration.
522
- They will not be deleted during cache clearing.
523
- They serve as root for the calling tree and persistent iterational variables.
524
- """
@@ -0,0 +1,175 @@
1
+ import numpy as np
2
+ import scipy.optimize
3
+ import shapely.affinity
4
+ from shapely import Polygon
5
+
6
+ from . import helpers
7
+ from ..asymmetric_two_roll_pass import AsymmetricTwoRollPass
8
+ from ...grooves import GenericElongationGroove
9
+
10
+
11
+ @AsymmetricTwoRollPass.usable_width
12
+ def usable_width(self: AsymmetricTwoRollPass):
13
+ return min(self.upper_roll.groove.usable_width, self.lower_roll.groove.usable_width)
14
+
15
+
16
+ @AsymmetricTwoRollPass.tip_width
17
+ def tip_width(self: AsymmetricTwoRollPass):
18
+ if isinstance(self.upper_roll.groove, GenericElongationGroove) and isinstance(
19
+ self.lower_roll.groove, GenericElongationGroove
20
+ ):
21
+ return min(
22
+ self.upper_roll.groove.usable_width + self.gap / 2 / np.tan(self.upper_roll.groove.flank_angle),
23
+ self.lower_roll.groove.usable_width + self.gap / 2 / np.tan(self.lower_roll.groove.flank_angle),
24
+ )
25
+
26
+
27
+ @AsymmetricTwoRollPass.usable_cross_section
28
+ def usable_cross_section(self: AsymmetricTwoRollPass) -> Polygon:
29
+ return helpers.out_cross_section(self, self.usable_width)
30
+
31
+
32
+ @AsymmetricTwoRollPass.tip_cross_section
33
+ def tip_cross_section(self: AsymmetricTwoRollPass) -> Polygon:
34
+ return helpers.out_cross_section(self, self.tip_width)
35
+
36
+
37
+ @AsymmetricTwoRollPass.gap
38
+ def gap(self: AsymmetricTwoRollPass):
39
+ if self.has_set_or_cached("height"):
40
+ return self.height - self.upper_roll.groove.depth - self.lower_roll.groove.depth
41
+
42
+
43
+ @AsymmetricTwoRollPass.height
44
+ def height(self: AsymmetricTwoRollPass):
45
+ if self.has_set_or_cached("gap"):
46
+ return self.gap + self.upper_roll.groove.depth + self.lower_roll.groove.depth
47
+
48
+
49
+ @AsymmetricTwoRollPass.contact_area
50
+ def contact_area(self: AsymmetricTwoRollPass):
51
+ return self.upper_roll.contact_area + self.lower_roll.contact_area
52
+
53
+
54
+ @AsymmetricTwoRollPass.target_cross_section_area
55
+ def target_cross_section_area_from_target_width(self: AsymmetricTwoRollPass):
56
+ if self.has_value("target_width"):
57
+ target_cross_section = helpers.out_cross_section(self, self.target_width)
58
+ return target_cross_section.area
59
+
60
+
61
+ @AsymmetricTwoRollPass.power
62
+ def roll_power(self: AsymmetricTwoRollPass):
63
+ return self.upper_roll.roll_power + self.lower_roll.roll_power
64
+
65
+
66
+ @AsymmetricTwoRollPass.velocity
67
+ def velocity(self: AsymmetricTwoRollPass):
68
+ if self.upper_roll.has_value("neutral_angle") and self.lower_roll.has_value("neutral_angle"):
69
+ return (
70
+ self.upper_roll.working_velocity * np.cos(self.upper_roll.neutral_angle)
71
+ + self.lower_roll.working_velocity * np.cos(self.lower_roll.neutral_angle)
72
+ ) / 2
73
+ else:
74
+ return (self.upper_roll.working_velocity + self.lower_roll.working_velocity) / 2
75
+
76
+
77
+ @AsymmetricTwoRollPass.roll_force
78
+ def roll_force(self: AsymmetricTwoRollPass):
79
+ return (
80
+ (self.in_profile.flow_stress + 2 * self.out_profile.flow_stress)
81
+ / 3
82
+ * (self.upper_roll.contact_area + self.lower_roll.contact_area)
83
+ / 2
84
+ )
85
+
86
+
87
+ @AsymmetricTwoRollPass.InProfile.pass_line
88
+ def pass_line(self: AsymmetricTwoRollPass.InProfile) -> tuple[float, float, float]:
89
+ """Places the incoming profile's cross-section within the roll gap: a vertical (``y``) and
90
+ transverse (``z``) shift, evaluated against the nominal roll-barrel contours (``contour_lines``,
91
+ the same fixed throat geometry ``draught``/``entry_point`` already use elsewhere). The rolling
92
+ direction is not a coordinate of a 2-D cross-section and plays no role here.
93
+
94
+ ``y`` is where the raw (undeformed) profile overlaps the upper and lower roll by exactly
95
+ *equal* area. If the rolls are large enough for the profile to touch both without overlapping
96
+ either, that zero-overlap placement falls out of this same condition automatically (equal
97
+ *and* zero); if not (e.g. an oversized profile against a strongly depth-asymmetric groove
98
+ pair), it is the placement where the initial squeeze is balanced evenly between both rolls
99
+ rather than concentrated on one side. As ``y`` increases the upper overlap area grows
100
+ monotonically while the lower overlap area shrinks monotonically, so there is always exactly
101
+ one such ``y`` -- a plain 1-D root find, no convergence caveats.
102
+
103
+ ``z`` is left at 0: both grooves in the currently supported use cases are left-right symmetric,
104
+ so the balanced placement is trivially centered."""
105
+ rp = self.roll_pass
106
+
107
+ # ``self.roll_pass.in_profile`` *is* ``self`` (this hook lives on the in-profile role of the
108
+ # roll pass), and ``in_cross_section`` (registered below) computes ``self.cross_section`` as
109
+ # this same raw shape translated by ``pass_line`` -- so reading ``self.cross_section`` here
110
+ # would normally be circular. As a root hook, every root hook function is evaluated against
111
+ # the state left by the *previous* iteration before any of this iteration's results are
112
+ # written back (see ``Hook.evaluate_and_set_hooks``), so on the very first call
113
+ # ``self.cross_section`` genuinely is the untranslated base profile -- capture it once, here,
114
+ # and reuse that same cached reference on every later call. Re-deriving it from
115
+ # ``self.cross_section`` on later calls would not work: each iteration's ``in_cross_section``
116
+ # also reads ``self.cross_section`` as its own input (same circularity), so it stacks a new
117
+ # translation on top of all previous ones rather than replacing them -- undoing only the most
118
+ # recent increment leaves the earlier accumulated ones baked in.
119
+ if not self.has_set_or_cached("pass_line"):
120
+ self._raw_in_cross_section = rp.in_profile.cross_section
121
+ raw_cross_section = self._raw_in_cross_section
122
+ raw_height = raw_cross_section.bounds[3] - raw_cross_section.bounds[1]
123
+
124
+ upper_contour, lower_contour = rp.contour_lines.geoms
125
+
126
+ def overlap_areas(y: float) -> tuple[float, float]:
127
+ shifted = shapely.affinity.translate(raw_cross_section, yoff=y)
128
+ a_upper = helpers.overlap_area(shifted, upper_contour.coords, direction=1)
129
+ a_lower = helpers.overlap_area(shifted, lower_contour.coords, direction=-1)
130
+ return a_upper, a_lower
131
+
132
+ # A fixed multiple of ``raw_height`` is not a safe bracket at all profile/roll scales: for a
133
+ # large profile it can end up so wide that ``brentq`` lands on a spurious point near the
134
+ # bracket edge instead of the true (much closer) root. Derive a tight, exactly-scaled bracket
135
+ # instead: shift the profile down until its top is fully below the lower contour's lowest
136
+ # point (guaranteeing full penetration into the lower roll, zero overlap with the upper one),
137
+ # and up until its bottom is fully above the upper contour's highest point (the mirror case).
138
+ # These are the true, guaranteed-sign-change bounds -- not an estimate.
139
+ box_bottom, box_top = raw_cross_section.bounds[1], raw_cross_section.bounds[3]
140
+ upper_contour_y = np.asarray(upper_contour.coords)[:, 1]
141
+ lower_contour_y = np.asarray(lower_contour.coords)[:, 1]
142
+ margin = 0.01 * raw_height + 1e-9
143
+ y_lo = lower_contour_y.min() - box_top - margin
144
+ y_hi = upper_contour_y.max() - box_bottom + margin
145
+ y = scipy.optimize.brentq(
146
+ lambda y: overlap_areas(y)[0] - overlap_areas(y)[1], y_lo, y_hi, xtol=1e-9,
147
+ )
148
+
149
+ common_overlap_area = overlap_areas(y)[0]
150
+ if common_overlap_area > 0:
151
+ self.logger.info(
152
+ "Hook %s.pass_line: the profile cannot touch both rolls without overlapping one of "
153
+ "them; settled at the placement with the evenly balanced overlap (%.3g mm^2 on each "
154
+ "side).",
155
+ type(self).__qualname__, common_overlap_area * 1e6,
156
+ )
157
+
158
+ return 0, y, 0
159
+
160
+
161
+ @AsymmetricTwoRollPass.InProfile.cross_section
162
+ def in_cross_section(self: AsymmetricTwoRollPass.InProfile):
163
+ # ``self.roll_pass.in_profile`` *is* ``self``, so translating ``self.cross_section`` (this
164
+ # hook's own previous result) would stack a new translation on top of all previous ones every
165
+ # iteration instead of replacing them (the same circularity ``pass_line`` avoids by caching
166
+ # the true raw shape in ``self._raw_in_cross_section`` -- reuse that same stable reference
167
+ # here rather than re-reading ``self.cross_section``). Accessing ``self.pass_line`` first
168
+ # guarantees that cache is populated.
169
+ pass_line = self.pass_line
170
+ return shapely.affinity.translate(self._raw_in_cross_section, xoff=pass_line[2], yoff=pass_line[1])
171
+
172
+
173
+ @AsymmetricTwoRollPass.entry_point
174
+ def entry_point(self: AsymmetricTwoRollPass):
175
+ return min(self.upper_roll.entry_point, self.lower_roll.entry_point)
@@ -20,9 +20,13 @@ def spread(self: DeformationUnit):
20
20
  def elongation(self: DeformationUnit):
21
21
  return self.in_profile.cross_section.area / self.out_profile.cross_section.area
22
22
 
23
+
23
24
  @DeformationUnit.reduction
24
25
  def reduction(self: DeformationUnit):
25
- return (self.in_profile.cross_section.area - self.in_profile.cross_section.area) / self.in_profile.cross_section.area
26
+ return (
27
+ self.in_profile.cross_section.area - self.out_profile.cross_section.area
28
+ ) / self.in_profile.cross_section.area
29
+
26
30
 
27
31
  @DeformationUnit.log_draught
28
32
  def log_draught(self: DeformationUnit):
@@ -5,14 +5,15 @@ from ..asymmetric_two_roll_pass import AsymmetricTwoRollPass
5
5
 
6
6
 
7
7
  @TwoRollPass.Engine.torque
8
- def engine_torque(self: TwoRollPass.Engine):
9
- return 2 * self.roll_pass.roll.roll_torque / (self.spindle_efficiency * self.gear_box_efficiency) + self.idle_torque
8
+ def engine_torque_gear_corrected(self: TwoRollPass.Engine):
9
+ return 2 * self.roll_pass.roll.roll_torque / (self.gear_ratio * self.spindle_efficiency * self.gear_box_efficiency
10
+ ) + self.idle_torque
10
11
 
11
12
 
12
13
  @ThreeRollPass.Engine.torque
13
- def engine_torque_three(self: ThreeRollPass.Engine):
14
- return 3 * self.roll_pass.roll.roll_torque / (self.spindle_efficiency * self.gear_box_efficiency) + self.idle_torque
15
-
14
+ def engine_torque_gear_corrected(self: ThreeRollPass.Engine):
15
+ return 3 * self.roll_pass.roll.roll_torque / ( self.gear_ratio * self.spindle_efficiency * self.gear_box_efficiency
16
+ ) + self.idle_torque
16
17
 
17
18
  @AsymmetricTwoRollPass.Engine.torque
18
19
  def engine_torque_asymmetric(self: AsymmetricTwoRollPass.Engine):
@@ -29,7 +30,6 @@ def engine_rotational_frequency(self: BaseRollPass.Engine):
29
30
  else:
30
31
  return self.roll_pass.roll.rotational_frequency
31
32
 
32
-
33
33
  @BaseRollPass.Engine.available_power
34
34
  def available_power(self: BaseRollPass.Engine):
35
35
  if self.has_set_or_cached("base_rotational_frequency"):
@@ -55,6 +55,24 @@ def max_vertical_penetration(cross_section: Polygon, contour_coords, direction:
55
55
  return max(float(penetrations.max(initial=0)), 0)
56
56
 
57
57
 
58
+ def overlap_area(cross_section: Polygon, contour_coords, direction: int) -> float:
59
+ """
60
+ Area of the profile cross-section that overlaps the roll material behind a contour line.
61
+
62
+ :param cross_section: the (already positioned) profile cross-section
63
+ :param contour_coords: coordinate sequence of the (already positioned) contour line
64
+ :param direction: ``1`` for an upper roll (material above the contour), ``-1`` for a lower roll (material below)
65
+ """
66
+ coords = np.asarray(contour_coords)
67
+ y_far = 10.0 if direction > 0 else -10.0
68
+ forbidden_region = Polygon(
69
+ np.concatenate([coords, [[coords[-1, 0], y_far], [coords[0, 0], y_far]]])
70
+ )
71
+ if not forbidden_region.is_valid:
72
+ forbidden_region = shapely.make_valid(forbidden_region)
73
+ return shapely.intersection(cross_section, forbidden_region).area
74
+
75
+
58
76
  def out_cross_section3(rp: ThreeRollPass, width: float) -> Polygon:
59
77
  poly = Polygon(np.concatenate([cl.coords for cl in rp.contour_lines.geoms]))
60
78
 
@@ -72,7 +72,8 @@ def cross_section(self: BaseRollPass.OutProfile) -> Polygon:
72
72
  cs = helpers.out_cross_section(self.roll_pass, self.width)
73
73
  if cs.width * 1.01 < self.width:
74
74
  raise ValueError(
75
- "Profile's width can not be larger than its contour lines." "May be caused by critical overfilling."
75
+ "Profile's width can not be larger than its contour lines."
76
+ "May be caused by critical overfilling."
76
77
  )
77
78
  return cs
78
79
 
@@ -98,7 +99,7 @@ def cross_section(self: BaseRollPass.OutProfile) -> Polygon:
98
99
  half_cross_section_1 = union(intermediate_section, filling_rectangle)
99
100
  half_cross_section_2 = scale(half_cross_section_1, xfact=-1, yfact=-1, origin=(0, 0))
100
101
  cs = union(half_cross_section_1, half_cross_section_2)
101
- except GEOSException:
102
+ except GEOSException as e:
102
103
  cs = helpers.out_cross_section(self.roll_pass, self.width)
103
104
 
104
105
  if cs.width * 1.01 < self.width:
@@ -205,6 +205,10 @@ class PassSequence(Unit, Sequence[Unit]):
205
205
  )
206
206
  set_velocities_to_roll_passes(roll_passes=copied_sequence.roll_passes, velocities=initial_roll_pass_velocities)
207
207
 
208
+ in_profile.velocity = (
209
+ initial_roll_pass_velocities[0] * usable_cross_section_areas[0] / in_profile.cross_section.area
210
+ )
211
+
208
212
  copied_sequence.solve(in_profile)
209
213
 
210
214
  for i in range(self.max_iteration_count):
@@ -217,6 +221,8 @@ class PassSequence(Unit, Sequence[Unit]):
217
221
  roll_passes=copied_sequence.roll_passes, velocities=current_roll_pass_velocities
218
222
  )
219
223
 
224
+ in_profile.velocity = current_roll_pass_velocities[0] * profile_areas[0] / in_profile.cross_section.area
225
+
220
226
  copied_sequence.solve(in_profile)
221
227
 
222
228
  difference = np.abs(prior_roll_pass_velocities - current_roll_pass_velocities)
@@ -235,6 +241,8 @@ class PassSequence(Unit, Sequence[Unit]):
235
241
  :param final_cross_section_area: area of the final cross-section
236
242
  """
237
243
 
244
+ in_profile.velocity = initial_speed
245
+
238
246
  copied_sequence = copy.deepcopy(self)
239
247
 
240
248
  def calculate_velocities_array(velocities: np.ndarray[float], cross_sections_areas: np.ndarray[float]):
@@ -279,52 +287,6 @@ class PassSequence(Unit, Sequence[Unit]):
279
287
  self.solve(in_profile)
280
288
  break
281
289
 
282
- def solve_interstand_tensions_with_given_velocity_ratios(
283
- self, in_profile: Profile, velocity_ratios: np.ndarray[float], final_speed: float
284
- ):
285
- """
286
- Solve method, that calculates the resulting tensions for given reductions.
287
- Further, it sets the velocities according to these reductions and the finishing speed.
288
-
289
- :param in_profile: incoming profile
290
- :param velocity_ratios: velocity_ratio per stand.
291
- :param final_speed: speed of the last stand
292
- """
293
-
294
- roll_pass_velocities = [final_speed]
295
- for ratio in reversed(velocity_ratios):
296
- previous_roll_pass_velocity = roll_pass_velocities[0] / ratio
297
- roll_pass_velocities.insert(0, previous_roll_pass_velocity)
298
-
299
- roll_pass_velocities = np.array(roll_pass_velocities)
300
- for i, roll_pass in enumerate(self.roll_passes):
301
- roll_pass.velocity = roll_pass_velocities[i]
302
-
303
- copied_sequence = copy.deepcopy(self)
304
- copied_sequence.solve(in_profile)
305
-
306
- tensions = np.zeros(len(copied_sequence.roll_passes) * 2)
307
- engineering_strains = (roll_pass_velocities[1:] - roll_pass_velocities[:-1]) / roll_pass_velocities[:-1]
308
-
309
- for index in range(1, len(self.roll_passes)):
310
- engineering_strain = engineering_strains[index - 1]
311
-
312
- tensions[2 * index - 1] = (
313
- copied_sequence.roll_passes[index - 1].in_profile.elastic_modulus * engineering_strain
314
- )
315
- tensions[2 * index] -= (
316
- copied_sequence.roll_passes[index - 1].out_profile.elastic_modulus * engineering_strain
317
- )
318
-
319
- tensions[0] = 0
320
- tensions[-1] = 0
321
-
322
- for index, roll_pass in enumerate(self.roll_passes):
323
- roll_pass.back_tension = tensions[2 * index]
324
- roll_pass.front_tension = tensions[2 * index + 1]
325
-
326
- self.solve(in_profile=in_profile)
327
-
328
290
  def find_value_by_position_or_time(self, hook_name: str, position: float = None, time: float = None):
329
291
  if position is None:
330
292
  search_coordinate = "t"
@@ -0,0 +1,9 @@
1
+ from .transport import Transport
2
+ from .cooling_pipe import CoolingPipe
3
+ from .spooler import Spooler
4
+ from .stelmor_conveyor import StelmorConveyor
5
+ from .induction_furnace import InductionFurnace
6
+
7
+ from . import hookimpls # noqa: F401
8
+
9
+ __all__ = ["Transport", "CoolingPipe", "Spooler", "StelmorConveyor", "InductionFurnace"]
@@ -1,3 +1,5 @@
1
1
  from . import transport
2
2
  from . import cooling_pipe
3
3
  from . import spooler
4
+ from . import stelmor_conveyor
5
+ from . import induction_furnace
@@ -1,4 +1,5 @@
1
1
  import numpy as np
2
+
2
3
  from ..cooling_pipe import CoolingPipe
3
4
 
4
5
 
@@ -11,7 +12,7 @@ def inner_radius(self: CoolingPipe):
11
12
  @CoolingPipe.cross_section_area
12
13
  def cross_section_area(self: CoolingPipe):
13
14
  if self.has_set_or_cached("inner_radius"):
14
- return self.inner_radius**2 * np.pi
15
+ return self.inner_radius ** 2 * np.pi
15
16
 
16
17
 
17
18
  @CoolingPipe.coolant_flow_cross_section
@@ -0,0 +1,23 @@
1
+ from ..induction_furnace import InductionFurnace
2
+
3
+
4
+ @InductionFurnace.electrical_efficiency
5
+ def default_electrical_efficiency(self: InductionFurnace):
6
+ return 0.6
7
+
8
+
9
+ @InductionFurnace.generator_power
10
+ def generator_power_from_target_temperature(self: InductionFurnace):
11
+ if self.has_value("target_temperature"):
12
+ delta_t = self.target_temperature - self.in_profile.temperature
13
+ return self.mass_flux * self.in_profile.specific_heat_capacity * delta_t / self.electrical_efficiency
14
+
15
+
16
+ @InductionFurnace.absorbed_power
17
+ def absorbed_power(self: InductionFurnace):
18
+ return self.generator_power * self.electrical_efficiency
19
+
20
+
21
+ @InductionFurnace.power_density
22
+ def power_density(self: InductionFurnace):
23
+ return self.absorbed_power / self.volume