-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
344 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
pyroll/core/roll_pass/hookimpls/asymmetric_two_roll_pass.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.