Skip to content

Commit

Permalink
impl asymmetric roll pass
Browse files Browse the repository at this point in the history
  • Loading branch information
axtimhaus committed Nov 6, 2024
1 parent 2d82aed commit 2067d6a
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 5 deletions.
4 changes: 3 additions & 1 deletion pyroll/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from .grooves import *
from .transport import Transport, CoolingPipe
from .roll_pass import BaseRollPass, RollPass, DeformationUnit, ThreeRollPass
from .roll_pass import BaseRollPass, RollPass, DeformationUnit, ThreeRollPass, AsymmetricTwoRollPass
from .unit import Unit
from .roll import Roll
from .profile import *
Expand All @@ -16,6 +16,8 @@

root_hooks.extend(
[
AsymmetricTwoRollPass.InProfile.pass_line,
AsymmetricTwoRollPass.InProfile.cross_section,
BaseRollPass.roll_force,
BaseRollPass.Roll.roll_torque,
BaseRollPass.elongation_efficiency,
Expand Down
1 change: 1 addition & 0 deletions pyroll/core/roll_pass/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .base import BaseRollPass
from .two_roll_pass import TwoRollPass
from .asymmetric_two_roll_pass import AsymmetricTwoRollPass
from .three_roll_pass import ThreeRollPass
from .deformation_unit import DeformationUnit

Expand Down
114 changes: 114 additions & 0 deletions pyroll/core/roll_pass/asymmetric_two_roll_pass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from typing import List, cast

import numpy as np
from shapely.affinity import translate, scale
from shapely.geometry import LineString

from .base import BaseRollPass
from ..hooks import Hook
from ..roll import Roll as BaseRoll


class AsymmetricTwoRollPass(BaseRollPass):
"""Represents a symmetric roll pass with equal upper and lower working roll."""

def __init__(
self,
upper_roll: BaseRoll,
lower_roll: BaseRoll,
label: str = "",
**kwargs
):
super().__init__(label, **kwargs)

self.upper_roll = self.Roll(upper_roll, self)
"""The upper working roll of this pass."""

self.lower_roll = self.Roll(lower_roll, self)
"""The upper working roll of this pass."""

@property
def contour_lines(self) -> List[LineString]:
if self._contour_lines:
return self._contour_lines

upper = translate(self.upper_roll.contour_line, yoff=self.gap / 2)
lower = scale(
translate(self.lower_roll.contour_line.reverse(), yoff=self.gap / 2),
xfact=1, yfact=-1, origin=(0, 0)
)

self._contour_lines = [upper, lower]
return self._contour_lines

@property
def classifiers(self):
"""A tuple of keywords to specify the shape type classifiers of this roll pass.
Shortcut to ``self.groove.classifiers``."""
return set(self.upper_roll.groove.classifiers) | set(self.lower_roll.groove.classifiers) | {"asymmetric"}

@property
def disk_elements(self) -> List['AsymmetricTwoRollPass.DiskElement']:
"""A list of disk elements used to subdivide this unit."""
return list(self._subunits)

def get_root_hook_results(self):
super_results = super().get_root_hook_results()
upper_roll_results = self.upper_roll.evaluate_and_set_hooks()
lower_roll_results = self.lower_roll.evaluate_and_set_hooks()
return np.concatenate([super_results, upper_roll_results, lower_roll_results], axis=0)

def reevaluate_cache(self):
super().reevaluate_cache()
self.upper_roll.reevaluate_cache()
self.lower_roll.reevaluate_cache()
self._contour_lines = None

class Profile(BaseRollPass.Profile):
"""Represents a profile in context of a roll pass."""

@property
def roll_pass(self) -> 'AsymmetricTwoRollPass':
"""Reference to the roll pass. Alias for ``self.unit``."""
return cast(AsymmetricTwoRollPass, self.unit)

class InProfile(Profile, BaseRollPass.InProfile):
"""Represents an incoming profile of a roll pass."""

pass_line = Hook[tuple[float, float, float]]()
"""Point (x, y, z) where the incoming profile centroid enters the roll pass."""

class OutProfile(Profile, BaseRollPass.OutProfile):
"""Represents an outgoing profile of a roll pass."""

filling_ratio = Hook[float]()

class Roll(BaseRollPass.Roll):
"""Represents a roll applied in a :py:class:`RollPass`."""

@property
def roll_pass(self) -> 'AsymmetricTwoRollPass':
"""Reference to the roll pass."""
return cast(AsymmetricTwoRollPass, self._roll_pass())

class DiskElement(BaseRollPass.DiskElement):
"""Represents a disk element in a roll pass."""

@property
def roll_pass(self) -> 'AsymmetricTwoRollPass':
"""Reference to the roll pass. Alias for ``self.parent``."""
return cast(AsymmetricTwoRollPass, self.parent)

class Profile(BaseRollPass.DiskElement.Profile):
"""Represents a profile in context of a disk element unit."""

@property
def disk_element(self) -> 'AsymmetricTwoRollPass.DiskElement':
"""Reference to the disk element. Alias for ``self.unit``"""
return cast(AsymmetricTwoRollPass.DiskElement, self.unit)

class InProfile(Profile, BaseRollPass.DiskElement.InProfile):
"""Represents an incoming profile of a disk element unit."""

class OutProfile(Profile, BaseRollPass.DiskElement.OutProfile):
"""Represents an outgoing profile of a disk element unit."""
15 changes: 12 additions & 3 deletions pyroll/core/roll_pass/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ def __init__(

self._contour_lines = None

self.given_in_profile: BaseProfile | None = None
"""The incoming profile as was given to the ``solve`` method."""

self.rotated_in_profile: BaseProfile | None = None
"""The incoming profile after rotation."""

@property
@abstractmethod
def contour_lines(self):
Expand All @@ -128,6 +134,8 @@ def disk_elements(self) -> List['BaseRollPass.DiskElement']:
return list(self._subunits)

def init_solve(self, in_profile: BaseProfile):
self.given_in_profile = in_profile

if self.rotation:
rotator = Rotator(
# make True determining from hook functions
Expand All @@ -136,14 +144,15 @@ def init_solve(self, in_profile: BaseProfile):
duration=0, length=0, parent=self
)
rotator.solve(in_profile)
in_profile = rotator.out_profile
self.rotated_in_profile = rotator.out_profile
else:
self.rotated_in_profile = in_profile

super().init_solve(in_profile)
super().init_solve(self.rotated_in_profile)
self.out_profile.cross_section = self.usable_cross_section

def reevaluate_cache(self):
super().reevaluate_cache()
self.roll.reevaluate_cache()
self._contour_lines = None

class Profile(DiskElementUnit.Profile, DeformationUnit.Profile):
Expand Down
1 change: 1 addition & 0 deletions pyroll/core/roll_pass/hookimpls/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from . import base_roll_pass
from . import symmetric_roll_pass
from . import two_roll_pass
from . import asymmetric_two_roll_pass
from . import three_roll_pass
from . import profile
from . import roll
Expand Down
140 changes: 140 additions & 0 deletions pyroll/core/roll_pass/hookimpls/asymmetric_two_roll_pass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import numpy as np
import scipy.optimize
import shapely.affinity
from shapely import Polygon

from . import helpers
from ..asymmetric_two_roll_pass import AsymmetricTwoRollPass
from ...grooves import GenericElongationGroove


@AsymmetricTwoRollPass.usable_width
def usable_width(self: AsymmetricTwoRollPass):
return min(self.upper_roll.groove.usable_width, self.lower_roll.groove.usable_width)


@AsymmetricTwoRollPass.tip_width
def tip_width(self: AsymmetricTwoRollPass):
if isinstance(self.upper_roll.groove, GenericElongationGroove) and isinstance(
self.lower_roll.groove, GenericElongationGroove
):
return min(
self.upper_roll.groove.usable_width + self.gap / 2 / np.tan(self.upper_roll.groove.flank_angle),
self.lower_roll.groove.usable_width + self.gap / 2 / np.tan(self.lower_roll.groove.flank_angle),
)


@AsymmetricTwoRollPass.usable_cross_section
def usable_cross_section(self: AsymmetricTwoRollPass) -> Polygon:
return helpers.out_cross_section(self, self.usable_width)


@AsymmetricTwoRollPass.tip_cross_section
def tip_cross_section(self: AsymmetricTwoRollPass) -> Polygon:
return helpers.out_cross_section(self, self.tip_width)


@AsymmetricTwoRollPass.gap
def gap(self: AsymmetricTwoRollPass):
if self.has_set_or_cached("height"):
return self.height - self.upper_roll.groove.depth - self.lower_roll.groove.depth


@AsymmetricTwoRollPass.height
def height(self: AsymmetricTwoRollPass):
if self.has_set_or_cached("gap"):
return self.gap + self.upper_roll.groove.depth + self.lower_roll.groove.depth


@AsymmetricTwoRollPass.contact_area
def contact_area(self: AsymmetricTwoRollPass):
return self.upper_roll.contact_area + self.lower_roll.contact_area


@AsymmetricTwoRollPass.target_cross_section_area
def target_cross_section_area_from_target_width(self: AsymmetricTwoRollPass):
if self.has_value("target_width"):
target_cross_section = helpers.out_cross_section(self, self.target_width)
return target_cross_section.area


@AsymmetricTwoRollPass.power
def roll_power(self: AsymmetricTwoRollPass):
return self.upper_roll.roll_power + self.lower_roll.roll_power


@AsymmetricTwoRollPass.velocity
def velocity(self: AsymmetricTwoRollPass):
if self.upper_roll.has_value("neutral_angle") and self.lower_roll.has_value("neutral_angle"):
return (
self.upper_roll.working_velocity * np.cos(self.upper_roll.neutral_angle)
+ self.lower_roll.working_velocity * np.cos(self.lower_roll.neutral_angle)
) / 2
else:
return (self.upper_roll.working_velocity + self.lower_roll.working_velocity) / 2


@AsymmetricTwoRollPass.roll_force
def roll_force(self: AsymmetricTwoRollPass):
return (
(self.in_profile.flow_stress + 2 * self.out_profile.flow_stress)
/ 3
* (self.upper_roll.contact_area + self.lower_roll.contact_area)
/ 2
)


@AsymmetricTwoRollPass.InProfile.pass_line
def pass_line(self: AsymmetricTwoRollPass.InProfile) -> tuple[float, float, float]:
rp = self.roll_pass

if not self.has_set("pass_line"):
height_change = self.height - rp.height
x_guess = -(
np.sqrt(height_change)
* np.sqrt(
(2 * rp.upper_roll.min_radius - height_change)
* (2 * rp.lower_roll.min_radius - height_change)
* (2 * rp.upper_roll.min_radius + 2 * rp.lower_roll.min_radius - height_change)
)
) / (2 * (rp.upper_roll.min_radius + rp.lower_roll.min_radius - height_change))
y_guess = 0
else:
x_guess, y_guess, _ = self.pass_line

def contact_objective(xy):
shifted_cross_section = shapely.affinity.translate(rp.rotated_in_profile.cross_section, yoff=xy[1])

upper_contour = shapely.geometry.LineString(np.stack([
rp.upper_roll.surface_z,
rp.upper_roll.surface_interpolation(xy[0], rp.upper_roll.surface_z).squeeze(axis=1)
], axis=1))
upper_contour = shapely.affinity.translate(upper_contour,yoff=self.roll_pass.gap / 2)
lower_contour = shapely.geometry.LineString(np.stack([
rp.lower_roll.surface_z,
rp.lower_roll.surface_interpolation(xy[0], rp.lower_roll.surface_z).squeeze(axis=1)
], axis=1))
lower_contour = shapely.affinity.scale(shapely.affinity.translate(lower_contour, yoff=self.roll_pass.gap / 2), xfact=1, yfact=-1, origin=(0,0))

upper_intersection = shapely.intersection(upper_contour, shifted_cross_section)
lower_intersection = shapely.intersection(lower_contour, shifted_cross_section)

upper_value = upper_intersection.length if not upper_intersection.is_empty else shapely.shortest_line(upper_contour, shifted_cross_section).length
lower_value = lower_intersection.length if not lower_intersection.is_empty else shapely.shortest_line(lower_contour, shifted_cross_section).length

return upper_value ** 2 + lower_value ** 2

sol = scipy.optimize.minimize(contact_objective, (x_guess, y_guess), method="BFGS", options=dict(xrtol=1e-2))

return sol.x[0], sol.x[1], 0


@AsymmetricTwoRollPass.InProfile.cross_section
def in_cross_section(self:AsymmetricTwoRollPass.InProfile):
return shapely.affinity.translate(self.roll_pass.rotated_in_profile.cross_section, xoff=self.pass_line[2], yoff=self.pass_line[1])


@AsymmetricTwoRollPass.entry_point
def entry_point(self: AsymmetricTwoRollPass):
return self.in_profile.pass_line[0]

3 changes: 2 additions & 1 deletion pyroll/core/roll_pass/hookimpls/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@

from ..two_roll_pass import TwoRollPass
from ..three_roll_pass import ThreeRollPass
from ..asymmetric_two_roll_pass import AsymmetricTwoRollPass
from ...profile.profile import refine_cross_section


def out_cross_section(rp: TwoRollPass, width: float) -> Polygon:
def out_cross_section(rp: TwoRollPass | AsymmetricTwoRollPass, width: float) -> Polygon:
poly = Polygon(np.concatenate([cl.coords for cl in rp.contour_lines]))
poly = clip_by_rect(poly, -width / 2, -math.inf, width / 2, math.inf)
return refine_cross_section(poly)
Expand Down
Loading

0 comments on commit 2067d6a

Please sign in to comment.