Skip to content

Commit 2067d6a

Browse files
committed
impl asymmetric roll pass
1 parent 2d82aed commit 2067d6a

8 files changed

+344
-5
lines changed

pyroll/core/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from .grooves import *
44
from .transport import Transport, CoolingPipe
5-
from .roll_pass import BaseRollPass, RollPass, DeformationUnit, ThreeRollPass
5+
from .roll_pass import BaseRollPass, RollPass, DeformationUnit, ThreeRollPass, AsymmetricTwoRollPass
66
from .unit import Unit
77
from .roll import Roll
88
from .profile import *
@@ -16,6 +16,8 @@
1616

1717
root_hooks.extend(
1818
[
19+
AsymmetricTwoRollPass.InProfile.pass_line,
20+
AsymmetricTwoRollPass.InProfile.cross_section,
1921
BaseRollPass.roll_force,
2022
BaseRollPass.Roll.roll_torque,
2123
BaseRollPass.elongation_efficiency,

pyroll/core/roll_pass/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .base import BaseRollPass
22
from .two_roll_pass import TwoRollPass
3+
from .asymmetric_two_roll_pass import AsymmetricTwoRollPass
34
from .three_roll_pass import ThreeRollPass
45
from .deformation_unit import DeformationUnit
56

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from typing import List, cast
2+
3+
import numpy as np
4+
from shapely.affinity import translate, scale
5+
from shapely.geometry import LineString
6+
7+
from .base import BaseRollPass
8+
from ..hooks import Hook
9+
from ..roll import Roll as BaseRoll
10+
11+
12+
class AsymmetricTwoRollPass(BaseRollPass):
13+
"""Represents a symmetric roll pass with equal upper and lower working roll."""
14+
15+
def __init__(
16+
self,
17+
upper_roll: BaseRoll,
18+
lower_roll: BaseRoll,
19+
label: str = "",
20+
**kwargs
21+
):
22+
super().__init__(label, **kwargs)
23+
24+
self.upper_roll = self.Roll(upper_roll, self)
25+
"""The upper working roll of this pass."""
26+
27+
self.lower_roll = self.Roll(lower_roll, self)
28+
"""The upper working roll of this pass."""
29+
30+
@property
31+
def contour_lines(self) -> List[LineString]:
32+
if self._contour_lines:
33+
return self._contour_lines
34+
35+
upper = translate(self.upper_roll.contour_line, yoff=self.gap / 2)
36+
lower = scale(
37+
translate(self.lower_roll.contour_line.reverse(), yoff=self.gap / 2),
38+
xfact=1, yfact=-1, origin=(0, 0)
39+
)
40+
41+
self._contour_lines = [upper, lower]
42+
return self._contour_lines
43+
44+
@property
45+
def classifiers(self):
46+
"""A tuple of keywords to specify the shape type classifiers of this roll pass.
47+
Shortcut to ``self.groove.classifiers``."""
48+
return set(self.upper_roll.groove.classifiers) | set(self.lower_roll.groove.classifiers) | {"asymmetric"}
49+
50+
@property
51+
def disk_elements(self) -> List['AsymmetricTwoRollPass.DiskElement']:
52+
"""A list of disk elements used to subdivide this unit."""
53+
return list(self._subunits)
54+
55+
def get_root_hook_results(self):
56+
super_results = super().get_root_hook_results()
57+
upper_roll_results = self.upper_roll.evaluate_and_set_hooks()
58+
lower_roll_results = self.lower_roll.evaluate_and_set_hooks()
59+
return np.concatenate([super_results, upper_roll_results, lower_roll_results], axis=0)
60+
61+
def reevaluate_cache(self):
62+
super().reevaluate_cache()
63+
self.upper_roll.reevaluate_cache()
64+
self.lower_roll.reevaluate_cache()
65+
self._contour_lines = None
66+
67+
class Profile(BaseRollPass.Profile):
68+
"""Represents a profile in context of a roll pass."""
69+
70+
@property
71+
def roll_pass(self) -> 'AsymmetricTwoRollPass':
72+
"""Reference to the roll pass. Alias for ``self.unit``."""
73+
return cast(AsymmetricTwoRollPass, self.unit)
74+
75+
class InProfile(Profile, BaseRollPass.InProfile):
76+
"""Represents an incoming profile of a roll pass."""
77+
78+
pass_line = Hook[tuple[float, float, float]]()
79+
"""Point (x, y, z) where the incoming profile centroid enters the roll pass."""
80+
81+
class OutProfile(Profile, BaseRollPass.OutProfile):
82+
"""Represents an outgoing profile of a roll pass."""
83+
84+
filling_ratio = Hook[float]()
85+
86+
class Roll(BaseRollPass.Roll):
87+
"""Represents a roll applied in a :py:class:`RollPass`."""
88+
89+
@property
90+
def roll_pass(self) -> 'AsymmetricTwoRollPass':
91+
"""Reference to the roll pass."""
92+
return cast(AsymmetricTwoRollPass, self._roll_pass())
93+
94+
class DiskElement(BaseRollPass.DiskElement):
95+
"""Represents a disk element in a roll pass."""
96+
97+
@property
98+
def roll_pass(self) -> 'AsymmetricTwoRollPass':
99+
"""Reference to the roll pass. Alias for ``self.parent``."""
100+
return cast(AsymmetricTwoRollPass, self.parent)
101+
102+
class Profile(BaseRollPass.DiskElement.Profile):
103+
"""Represents a profile in context of a disk element unit."""
104+
105+
@property
106+
def disk_element(self) -> 'AsymmetricTwoRollPass.DiskElement':
107+
"""Reference to the disk element. Alias for ``self.unit``"""
108+
return cast(AsymmetricTwoRollPass.DiskElement, self.unit)
109+
110+
class InProfile(Profile, BaseRollPass.DiskElement.InProfile):
111+
"""Represents an incoming profile of a disk element unit."""
112+
113+
class OutProfile(Profile, BaseRollPass.DiskElement.OutProfile):
114+
"""Represents an outgoing profile of a disk element unit."""

pyroll/core/roll_pass/base.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ def __init__(
109109

110110
self._contour_lines = None
111111

112+
self.given_in_profile: BaseProfile | None = None
113+
"""The incoming profile as was given to the ``solve`` method."""
114+
115+
self.rotated_in_profile: BaseProfile | None = None
116+
"""The incoming profile after rotation."""
117+
112118
@property
113119
@abstractmethod
114120
def contour_lines(self):
@@ -128,6 +134,8 @@ def disk_elements(self) -> List['BaseRollPass.DiskElement']:
128134
return list(self._subunits)
129135

130136
def init_solve(self, in_profile: BaseProfile):
137+
self.given_in_profile = in_profile
138+
131139
if self.rotation:
132140
rotator = Rotator(
133141
# make True determining from hook functions
@@ -136,14 +144,15 @@ def init_solve(self, in_profile: BaseProfile):
136144
duration=0, length=0, parent=self
137145
)
138146
rotator.solve(in_profile)
139-
in_profile = rotator.out_profile
147+
self.rotated_in_profile = rotator.out_profile
148+
else:
149+
self.rotated_in_profile = in_profile
140150

141-
super().init_solve(in_profile)
151+
super().init_solve(self.rotated_in_profile)
142152
self.out_profile.cross_section = self.usable_cross_section
143153

144154
def reevaluate_cache(self):
145155
super().reevaluate_cache()
146-
self.roll.reevaluate_cache()
147156
self._contour_lines = None
148157

149158
class Profile(DiskElementUnit.Profile, DeformationUnit.Profile):

pyroll/core/roll_pass/hookimpls/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from . import base_roll_pass
22
from . import symmetric_roll_pass
33
from . import two_roll_pass
4+
from . import asymmetric_two_roll_pass
45
from . import three_roll_pass
56
from . import profile
67
from . import roll
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
rp = self.roll_pass
90+
91+
if not self.has_set("pass_line"):
92+
height_change = self.height - rp.height
93+
x_guess = -(
94+
np.sqrt(height_change)
95+
* np.sqrt(
96+
(2 * rp.upper_roll.min_radius - height_change)
97+
* (2 * rp.lower_roll.min_radius - height_change)
98+
* (2 * rp.upper_roll.min_radius + 2 * rp.lower_roll.min_radius - height_change)
99+
)
100+
) / (2 * (rp.upper_roll.min_radius + rp.lower_roll.min_radius - height_change))
101+
y_guess = 0
102+
else:
103+
x_guess, y_guess, _ = self.pass_line
104+
105+
def contact_objective(xy):
106+
shifted_cross_section = shapely.affinity.translate(rp.rotated_in_profile.cross_section, yoff=xy[1])
107+
108+
upper_contour = shapely.geometry.LineString(np.stack([
109+
rp.upper_roll.surface_z,
110+
rp.upper_roll.surface_interpolation(xy[0], rp.upper_roll.surface_z).squeeze(axis=1)
111+
], axis=1))
112+
upper_contour = shapely.affinity.translate(upper_contour,yoff=self.roll_pass.gap / 2)
113+
lower_contour = shapely.geometry.LineString(np.stack([
114+
rp.lower_roll.surface_z,
115+
rp.lower_roll.surface_interpolation(xy[0], rp.lower_roll.surface_z).squeeze(axis=1)
116+
], axis=1))
117+
lower_contour = shapely.affinity.scale(shapely.affinity.translate(lower_contour, yoff=self.roll_pass.gap / 2), xfact=1, yfact=-1, origin=(0,0))
118+
119+
upper_intersection = shapely.intersection(upper_contour, shifted_cross_section)
120+
lower_intersection = shapely.intersection(lower_contour, shifted_cross_section)
121+
122+
upper_value = upper_intersection.length if not upper_intersection.is_empty else shapely.shortest_line(upper_contour, shifted_cross_section).length
123+
lower_value = lower_intersection.length if not lower_intersection.is_empty else shapely.shortest_line(lower_contour, shifted_cross_section).length
124+
125+
return upper_value ** 2 + lower_value ** 2
126+
127+
sol = scipy.optimize.minimize(contact_objective, (x_guess, y_guess), method="BFGS", options=dict(xrtol=1e-2))
128+
129+
return sol.x[0], sol.x[1], 0
130+
131+
132+
@AsymmetricTwoRollPass.InProfile.cross_section
133+
def in_cross_section(self:AsymmetricTwoRollPass.InProfile):
134+
return shapely.affinity.translate(self.roll_pass.rotated_in_profile.cross_section, xoff=self.pass_line[2], yoff=self.pass_line[1])
135+
136+
137+
@AsymmetricTwoRollPass.entry_point
138+
def entry_point(self: AsymmetricTwoRollPass):
139+
return self.in_profile.pass_line[0]
140+

pyroll/core/roll_pass/hookimpls/helpers.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88
from ..two_roll_pass import TwoRollPass
99
from ..three_roll_pass import ThreeRollPass
10+
from ..asymmetric_two_roll_pass import AsymmetricTwoRollPass
1011
from ...profile.profile import refine_cross_section
1112

1213

13-
def out_cross_section(rp: TwoRollPass, width: float) -> Polygon:
14+
def out_cross_section(rp: TwoRollPass | AsymmetricTwoRollPass, width: float) -> Polygon:
1415
poly = Polygon(np.concatenate([cl.coords for cl in rp.contour_lines]))
1516
poly = clip_by_rect(poly, -width / 2, -math.inf, width / 2, math.inf)
1617
return refine_cross_section(poly)

0 commit comments

Comments
 (0)