From 2d82aedbc59fdc7741be086e10295e81fe8f09b3 Mon Sep 17 00:00:00 2001 From: Max Weiner Date: Fri, 23 Aug 2024 14:10:33 +0200 Subject: [PATCH] add common symmetric roll passes base class --- pyroll/core/roll/hookimpls.py | 2 +- pyroll/core/roll_pass/__init__.py | 4 +- pyroll/core/roll_pass/base.py | 31 +- pyroll/core/roll_pass/hookimpls/__init__.py | 5 +- .../roll_pass/hookimpls/base_roll_pass.py | 124 ++++++++ .../roll_pass/hookimpls/deformation_unit.py | 3 - pyroll/core/roll_pass/hookimpls/helpers.py | 8 +- pyroll/core/roll_pass/hookimpls/roll.py | 28 +- pyroll/core/roll_pass/hookimpls/roll_pass.py | 293 ------------------ .../hookimpls/symmetric_roll_pass.py | 22 ++ .../roll_pass/hookimpls/three_roll_pass.py | 89 ++++++ .../core/roll_pass/hookimpls/two_roll_pass.py | 66 ++++ pyroll/core/roll_pass/roll_pass.py | 76 ----- pyroll/core/roll_pass/symmetric_roll_pass.py | 87 ++++++ pyroll/core/roll_pass/three_roll_pass.py | 30 +- pyroll/core/roll_pass/two_roll_pass.py | 86 +++++ 16 files changed, 533 insertions(+), 421 deletions(-) create mode 100644 pyroll/core/roll_pass/hookimpls/base_roll_pass.py delete mode 100644 pyroll/core/roll_pass/hookimpls/roll_pass.py create mode 100644 pyroll/core/roll_pass/hookimpls/symmetric_roll_pass.py create mode 100644 pyroll/core/roll_pass/hookimpls/three_roll_pass.py create mode 100644 pyroll/core/roll_pass/hookimpls/two_roll_pass.py delete mode 100644 pyroll/core/roll_pass/roll_pass.py create mode 100644 pyroll/core/roll_pass/symmetric_roll_pass.py create mode 100644 pyroll/core/roll_pass/two_roll_pass.py diff --git a/pyroll/core/roll/hookimpls.py b/pyroll/core/roll/hookimpls.py index f7f89213..f3029e1e 100644 --- a/pyroll/core/roll/hookimpls.py +++ b/pyroll/core/roll/hookimpls.py @@ -51,7 +51,7 @@ def contour_points(self: Roll): @Roll.surface_x def surface_x(self: Roll): - padded_contact_angle = np.arcsin(1.1 * self.contact_length / self.min_radius) + padded_contact_angle = np.arcsin(1.1 * self.contact_length / self.min_radius) if self.has_set_or_cached("contact_length") else np.pi / 4 points = np.concatenate([ np.linspace(0, padded_contact_angle, Config.ROLL_SURFACE_DISCRETIZATION_COUNT, endpoint=False), np.linspace(padded_contact_angle, np.pi / 2, Config.ROLL_SURFACE_DISCRETIZATION_COUNT), diff --git a/pyroll/core/roll_pass/__init__.py b/pyroll/core/roll_pass/__init__.py index 09074197..36025e34 100644 --- a/pyroll/core/roll_pass/__init__.py +++ b/pyroll/core/roll_pass/__init__.py @@ -1,9 +1,11 @@ from .base import BaseRollPass -from .roll_pass import RollPass +from .two_roll_pass import TwoRollPass from .three_roll_pass import ThreeRollPass from .deformation_unit import DeformationUnit from . import hookimpls +RollPass = TwoRollPass + diff --git a/pyroll/core/roll_pass/base.py b/pyroll/core/roll_pass/base.py index fb4cd99b..823068cb 100644 --- a/pyroll/core/roll_pass/base.py +++ b/pyroll/core/roll_pass/base.py @@ -1,4 +1,5 @@ import weakref +from abc import abstractmethod, ABC from typing import List, Union, cast import numpy as np @@ -13,7 +14,7 @@ from .deformation_unit import DeformationUnit -class BaseRollPass(DiskElementUnit, DeformationUnit): +class BaseRollPass(DiskElementUnit, DeformationUnit, ABC): """Represents a roll pass with two symmetric working rolls.""" rotation = Hook[Union[bool, float]]() @@ -63,15 +64,9 @@ class BaseRollPass(DiskElementUnit, DeformationUnit): entry_point = Hook[float]() """Point where the material enters the roll gap.""" - entry_angle = Hook[float]() - """Angle at which the material enters the roll gap.""" - exit_point = Hook[float]() """Point where the material exits the roll gap.""" - exit_angle = Hook[float]() - """Angle at which the material exits the roll gap.""" - front_tension = Hook[float]() """Front tension acting on the current roll pass.""" @@ -101,7 +96,6 @@ class BaseRollPass(DiskElementUnit, DeformationUnit): def __init__( self, - roll: BaseRoll, label: str = "", **kwargs ): @@ -113,21 +107,20 @@ def __init__( super().__init__(label=label, **kwargs) - self.roll = self.Roll(roll, self) - """The working roll of this pass (equal upper and lower).""" - self._contour_lines = None @property + @abstractmethod def contour_lines(self): """List of line strings bounding the roll pass at the high point.""" - raise NotImplementedError + raise NotImplementedError() @property + @abstractmethod 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.roll.groove.classifiers) + raise NotImplementedError() @property def disk_elements(self) -> List['BaseRollPass.DiskElement']: @@ -148,12 +141,6 @@ def init_solve(self, in_profile: BaseProfile): super().init_solve(in_profile) self.out_profile.cross_section = self.usable_cross_section - def get_root_hook_results(self): - super_results = super().get_root_hook_results() - roll_results = self.roll.evaluate_and_set_hooks() - - return np.concatenate([super_results, roll_results], axis=0) - def reevaluate_cache(self): super().reevaluate_cache() self.roll.reevaluate_cache() @@ -197,6 +184,12 @@ def __init__(self, template: BaseRoll, roll_pass: 'BaseRollPass'): self._roll_pass = weakref.ref(roll_pass) + entry_angle = Hook[float]() + """Angle at which the material enters the roll gap.""" + + exit_angle = Hook[float]() + """Angle at which the material exits the roll gap.""" + @property def roll_pass(self): """Reference to the roll pass this roll is used in.""" diff --git a/pyroll/core/roll_pass/hookimpls/__init__.py b/pyroll/core/roll_pass/hookimpls/__init__.py index a8fc1bae..f2217e5c 100644 --- a/pyroll/core/roll_pass/hookimpls/__init__.py +++ b/pyroll/core/roll_pass/hookimpls/__init__.py @@ -1,4 +1,7 @@ -from . import roll_pass +from . import base_roll_pass +from . import symmetric_roll_pass +from . import two_roll_pass +from . import three_roll_pass from . import profile from . import roll from . import disk_element diff --git a/pyroll/core/roll_pass/hookimpls/base_roll_pass.py b/pyroll/core/roll_pass/hookimpls/base_roll_pass.py new file mode 100644 index 00000000..5a47a841 --- /dev/null +++ b/pyroll/core/roll_pass/hookimpls/base_roll_pass.py @@ -0,0 +1,124 @@ +from shapely import difference +from shapely.ops import linemerge + +from ..base import BaseRollPass +from ...config import Config +from ...rotator import Rotator + + +@BaseRollPass.rotation +def auto_rotation(self: BaseRollPass): + return Config.ROLL_PASS_AUTO_ROTATION + + +@BaseRollPass.rotation +def detect_already_rotated(self: BaseRollPass): + if Config.ROLL_PASS_AUTO_ROTATION and self.parent is not None: + try: + prev = self.prev + except IndexError: + return True + + while True: + if isinstance(prev, BaseRollPass): + return True + if isinstance(prev, Rotator): + return False + try: + prev = prev.prev + except IndexError: + return True + + +@BaseRollPass.orientation +def default_orientation(self: BaseRollPass): + return 0 + + +@BaseRollPass.volume +def volume(self: BaseRollPass): + return (self.in_profile.cross_section.area + 2 * self.out_profile.cross_section.area + ) / 3 * self.length + + +@BaseRollPass.surface_area +def surface_area(self: BaseRollPass): + return (self.in_profile.cross_section.perimeter + 2 * self.out_profile.cross_section.perimeter + ) / 3 * self.length + + +@BaseRollPass.duration +def duration(self: BaseRollPass): + return self.length / self.velocity + + +@BaseRollPass.length +def length(self: BaseRollPass): + return -self.entry_point + self.exit_point + + +@BaseRollPass.displaced_cross_section +def displaced_cross_section(self: BaseRollPass): + return difference(self.in_profile.cross_section, self.usable_cross_section) + + +@BaseRollPass.reappearing_cross_section +def reappearing_cross_section(self: BaseRollPass): + return difference(self.out_profile.cross_section, self.in_profile.cross_section) + + +@BaseRollPass.elongation_efficiency +def elongation_efficiency(self: BaseRollPass): + return 1 - self.reappearing_cross_section.area / self.displaced_cross_section.area + + +@BaseRollPass.target_filling_ratio(trylast=True) +def default_target_filling(self: BaseRollPass): + return 1 + + +@BaseRollPass.target_width +def target_width_from_target_filling_ratio(self: BaseRollPass): + if self.has_value("target_filling_ratio"): + return self.target_filling_ratio * self.usable_width + + +@BaseRollPass.target_filling_ratio +def target_filling_ratio_from_target_width(self: BaseRollPass): + if self.has_set_or_cached("target_width"): + return self.target_width / self.usable_width + + +@BaseRollPass.target_cross_section_area +def target_cross_section_area_from_target_cross_section_filling_ratio(self: BaseRollPass): + if self.has_set_or_cached("target_cross_section_filling_ratio"): + return self.target_cross_section_filling_ratio * self.usable_cross_section.area + + +@BaseRollPass.target_cross_section_filling_ratio +def target_cross_section_filling_ratio_from_target_cross_section_area(self: BaseRollPass): + if self.has_value("target_cross_section_area"): # important has_value for computing from target_width + return self.target_cross_section_area / self.usable_cross_section.area + + +@BaseRollPass.exit_point +def exit_point(self: BaseRollPass): + return 0 + + +@BaseRollPass.Profile.contact_lines +def contact_contour_lines(self: BaseRollPass.Profile): + rp = self.roll_pass + return [linemerge(cl.intersection(self.cross_section.exterior.buffer(1e-9))) for cl in rp.contour_lines] + + +@BaseRollPass.front_tension +def default_front_tension(self: BaseRollPass): + return 0 + + +@BaseRollPass.back_tension +def default_back_tension(self: BaseRollPass): + return 0 + + diff --git a/pyroll/core/roll_pass/hookimpls/deformation_unit.py b/pyroll/core/roll_pass/hookimpls/deformation_unit.py index 4fc2d247..94bd3e9e 100644 --- a/pyroll/core/roll_pass/hookimpls/deformation_unit.py +++ b/pyroll/core/roll_pass/hookimpls/deformation_unit.py @@ -1,11 +1,8 @@ import numpy as np -from shapely.ops import linemerge from shapely.geometry import LineString -from shapely.affinity import translate, rotate from ..deformation_unit import DeformationUnit -from ..roll_pass import RollPass from ...config import Config diff --git a/pyroll/core/roll_pass/hookimpls/helpers.py b/pyroll/core/roll_pass/hookimpls/helpers.py index 5b4e439a..96436d57 100644 --- a/pyroll/core/roll_pass/hookimpls/helpers.py +++ b/pyroll/core/roll_pass/hookimpls/helpers.py @@ -1,17 +1,19 @@ import math import numpy as np +import shapely from shapely import Polygon, clip_by_rect from shapely.affinity import rotate -from ..roll_pass import RollPass +from ..two_roll_pass import TwoRollPass from ..three_roll_pass import ThreeRollPass from ...profile.profile import refine_cross_section -def out_cross_section(rp: RollPass, width: float) -> Polygon: +def out_cross_section(rp: TwoRollPass, width: float) -> Polygon: poly = Polygon(np.concatenate([cl.coords for cl in rp.contour_lines])) - return refine_cross_section(clip_by_rect(poly, -width / 2, -math.inf, width / 2, math.inf)) + poly = clip_by_rect(poly, -width / 2, -math.inf, width / 2, math.inf) + return refine_cross_section(poly) def out_cross_section3(rp: ThreeRollPass, width: float) -> Polygon: diff --git a/pyroll/core/roll_pass/hookimpls/roll.py b/pyroll/core/roll_pass/hookimpls/roll.py index 13a42b40..693cdbb6 100644 --- a/pyroll/core/roll_pass/hookimpls/roll.py +++ b/pyroll/core/roll_pass/hookimpls/roll.py @@ -1,7 +1,7 @@ import numpy as np from ..base import BaseRollPass -from ..roll_pass import RollPass +from ..two_roll_pass import TwoRollPass from ..three_roll_pass import ThreeRollPass @@ -12,21 +12,11 @@ def roll_torque(self: BaseRollPass.Roll): @BaseRollPass.Roll.contact_length def contact_length(self: BaseRollPass.Roll): - height_change = self.roll_pass.in_profile.height - self.roll_pass.height - return np.sqrt(self.min_radius * height_change - height_change ** 2 / 4) + return self.roll_pass.exit_point - self.roll_pass.entry_point -@BaseRollPass.Roll.contact_length -def contact_length_square_oval(self: BaseRollPass.Roll): - if "square" in self.roll_pass.in_profile.classifiers and "oval" in self.roll_pass.classifiers: - depth = self.groove.local_depth(self.roll_pass.in_profile.width / 2) - height_change = self.roll_pass.in_profile.height - self.roll_pass.gap - 2 * depth - radius = self.max_radius - depth - return np.sqrt(radius * height_change - height_change ** 2 / 4) - - -@RollPass.Roll.contact_area -def contact_area(self: RollPass.Roll): +@BaseRollPass.Roll.contact_area +def contact_area(self: TwoRollPass.Roll): return (self.roll_pass.in_profile.width + self.roll_pass.out_profile.width) / 2 * self.contact_length @@ -60,3 +50,13 @@ def surface_velocity(self: BaseRollPass.Roll): return self.roll_pass.velocity / np.cos(self.neutral_angle) else: return self.roll_pass.velocity / np.cos(self.roll_pass.exit_angle) + + +@BaseRollPass.Roll.entry_angle +def entry_angle(self: BaseRollPass.Roll): + return np.arcsin(self.roll_pass.entry_point / self.working_radius) + + +@BaseRollPass.Roll.exit_angle +def exit_angle(self: BaseRollPass.Roll): + return np.arcsin(self.roll_pass.exit_point / self.working_radius) diff --git a/pyroll/core/roll_pass/hookimpls/roll_pass.py b/pyroll/core/roll_pass/hookimpls/roll_pass.py deleted file mode 100644 index b2e43668..00000000 --- a/pyroll/core/roll_pass/hookimpls/roll_pass.py +++ /dev/null @@ -1,293 +0,0 @@ -import math - -import numpy as np -from shapely import Polygon, difference, clip_by_rect -from shapely.ops import linemerge - -from ..base import BaseRollPass -from ..three_roll_pass import ThreeRollPass -from ..roll_pass import RollPass -from ...rotator import Rotator -from ...grooves import GenericElongationGroove - -from ...config import Config -from . import helpers - - -@BaseRollPass.rotation -def auto_rotation(self: BaseRollPass): - return Config.ROLL_PASS_AUTO_ROTATION - - -@BaseRollPass.rotation -def detect_already_rotated(self: BaseRollPass): - if Config.ROLL_PASS_AUTO_ROTATION and self.parent is not None: - try: - prev = self.prev - except IndexError: - return True - - while True: - if isinstance(prev, BaseRollPass): - return True - if isinstance(prev, Rotator): - return False - try: - prev = prev.prev - except IndexError: - return True - - -@BaseRollPass.orientation -def default_orientation(self: BaseRollPass): - return 0 - - -@BaseRollPass.roll_force -def roll_force(self: BaseRollPass): - return (self.in_profile.flow_stress + 2 * self.out_profile.flow_stress) / 3 * self.roll.contact_area - - -@RollPass.usable_width -def usable_width(self: RollPass): - return self.roll.groove.usable_width - - -@ThreeRollPass.usable_width -def usable_width3(self: BaseRollPass): - return 2 / 3 * np.sqrt(3) * (self.roll.groove.usable_width + self.gap / 2) - - -@RollPass.tip_width -def tip_width(self): - if isinstance(self.roll.groove, GenericElongationGroove): - return self.roll.groove.usable_width + self.gap / 2 / np.tan(self.roll.groove.flank_angle) - - -@ThreeRollPass.tip_width -def tip_width3(self): - if isinstance(self.roll.groove, GenericElongationGroove): - return ( - 2 / 3 * np.sqrt(3) * (self.roll.groove.usable_width + self.gap / 2) - + self.gap / np.sqrt(3) * np.cos(self.roll.groove.flank_angle) - ) - - -@RollPass.usable_cross_section -def usable_cross_section(self: RollPass) -> Polygon: - return helpers.out_cross_section(self, self.usable_width) - - -@ThreeRollPass.usable_cross_section -def usable_cross_section3(self: ThreeRollPass) -> Polygon: - return helpers.out_cross_section3(self, self.usable_width) - - -@RollPass.tip_cross_section -def tip_cross_section(self: RollPass) -> Polygon: - return helpers.out_cross_section(self, self.tip_width) - - -@ThreeRollPass.tip_cross_section -def tip_cross_section3(self: ThreeRollPass) -> Polygon: - return helpers.out_cross_section3(self, self.tip_width) - - -@RollPass.gap -def gap(self: RollPass): - if self.has_set_or_cached("height"): - return self.height - 2 * self.roll.groove.depth - - -@ThreeRollPass.inscribed_circle_diameter -def inscribed_circle_diameter_from_gap(self: ThreeRollPass): - if self.has_set_or_cached("gap"): - half = self.roll.groove.usable_width / 2 / np.sqrt(3) + self.gap / np.sqrt(3) + self.roll.groove.depth - return half * 2 - - -@ThreeRollPass.gap -def gap3_from_height(self: ThreeRollPass): - if self.has_set_or_cached("height"): - return ( - self.height / 2 - - self.roll.groove.usable_width / 2 / np.sqrt(3) - - self.roll.groove.depth - ) * np.sqrt(3) - - -@ThreeRollPass.gap -def gap3_from_icd(self: ThreeRollPass): - if self.has_set_or_cached("inscribed_circle_diameter"): - return ( - self.inscribed_circle_diameter / 2 - - self.roll.groove.usable_width / 2 / np.sqrt(3) - - self.roll.groove.depth - ) * np.sqrt(3) - - -@RollPass.height -def height(self): - if self.has_set_or_cached("gap"): - return self.gap + 2 * self.roll.groove.depth - - -@ThreeRollPass.height -def height3(self): - usable_contour = clip_by_rect( - self.contour_lines[1], - -self.roll.groove.usable_width / 2, - -math.inf, - self.roll.groove.usable_width / 2, - math.inf - ) - return abs(2 * usable_contour.bounds[1]) - - -@BaseRollPass.volume -def volume(self: BaseRollPass): - return (self.in_profile.cross_section.area + 2 * self.out_profile.cross_section.area - ) / 3 * self.length - - -@BaseRollPass.surface_area -def surface_area(self: BaseRollPass): - return (self.in_profile.cross_section.perimeter + 2 * self.out_profile.cross_section.perimeter - ) / 3 * self.length - - -@RollPass.contact_area -def contact_area(self: RollPass): - return 2 * self.roll.contact_area - - -@ThreeRollPass.contact_area -def contact_area3(self: ThreeRollPass): - return 3 * self.roll.contact_area - - -@BaseRollPass.velocity -def velocity(self: BaseRollPass): - if self.roll.has_value("neutral_angle"): - return self.roll.working_velocity * np.cos(self.roll.neutral_angle) - else: - return self.roll.rotational_frequency * self.roll.working_radius * 2 * np.pi - - -@BaseRollPass.duration -def duration(self: BaseRollPass): - return self.length / self.velocity - - -@BaseRollPass.length -def length(self: BaseRollPass): - return self.roll.contact_length - - -@BaseRollPass.displaced_cross_section -def displaced_cross_section(self: BaseRollPass): - return difference(self.in_profile.cross_section, self.usable_cross_section) - - -@BaseRollPass.reappearing_cross_section -def reappearing_cross_section(self: BaseRollPass): - return difference(self.out_profile.cross_section, self.in_profile.cross_section) - - -@BaseRollPass.elongation_efficiency -def elongation_efficiency(self: BaseRollPass): - return 1 - self.reappearing_cross_section.area / self.displaced_cross_section.area - - -@BaseRollPass.target_filling_ratio(trylast=True) -def default_target_filling(self: BaseRollPass): - return 1 - - -@BaseRollPass.target_width -def target_width_from_target_filling_ratio(self: BaseRollPass): - if self.has_value("target_filling_ratio"): - return self.target_filling_ratio * self.usable_width - - -@BaseRollPass.target_filling_ratio -def target_filling_ratio_from_target_width(self: BaseRollPass): - if self.has_set_or_cached("target_width"): - return self.target_width / self.usable_width - - -@RollPass.target_cross_section_area -def target_cross_section_area_from_target_width(self: RollPass): - if self.has_value("target_width"): - target_cross_section = helpers.out_cross_section(self, self.target_width) - return target_cross_section.area - - -@ThreeRollPass.target_cross_section_area -def target_cross_section_area_from_target_width3(self: ThreeRollPass): - if self.has_value("target_width"): - target_cross_section = helpers.out_cross_section3(self, self.target_width) - return target_cross_section.area - - -@BaseRollPass.target_cross_section_area -def target_cross_section_area_from_target_cross_section_filling_ratio(self: BaseRollPass): - if self.has_set_or_cached("target_cross_section_filling_ratio"): - return self.target_cross_section_filling_ratio * self.usable_cross_section.area - - -@BaseRollPass.target_cross_section_filling_ratio -def target_cross_section_filling_ratio_from_target_cross_section_area(self: BaseRollPass): - if self.has_value("target_cross_section_area"): # important has_value for computing from target_width - return self.target_cross_section_area / self.usable_cross_section.area - - -@RollPass.power -def roll_power(self: RollPass): - return 2 * self.roll.roll_power - - -@ThreeRollPass.power -def roll_power_3(self: ThreeRollPass): - return 3 * self.roll.roll_power - - -@BaseRollPass.entry_point -def entry_point(self: BaseRollPass): - return - self.roll.contact_length - - -@BaseRollPass.entry_angle -def entry_angle(self: BaseRollPass): - if "square" in self.in_profile.classifiers and "oval" in self.classifiers: - depth = self.roll.groove.local_depth(self.in_profile.width / 2) - radius = self.roll.max_radius - depth - return np.arcsin(self.entry_point / radius) - - return np.arcsin(self.entry_point / self.roll.min_radius) - - -@BaseRollPass.exit_point -def exit_point(self: BaseRollPass): - return 0 - - -@BaseRollPass.exit_angle -def exit_angle(self: BaseRollPass): - return np.arcsin(self.exit_point / self.roll.working_radius) - - -@BaseRollPass.Profile.contact_lines -def contact_contour_lines(self: BaseRollPass.Profile): - rp = self.roll_pass - return [linemerge(cl.intersection(self.cross_section.exterior.buffer(1e-9))) for cl in rp.contour_lines] - - -@RollPass.front_tension -def default_front_tension(self: RollPass): - return 0 - - -@RollPass.back_tension -def default_back_tension(self: RollPass): - return 0 diff --git a/pyroll/core/roll_pass/hookimpls/symmetric_roll_pass.py b/pyroll/core/roll_pass/hookimpls/symmetric_roll_pass.py new file mode 100644 index 00000000..6224200a --- /dev/null +++ b/pyroll/core/roll_pass/hookimpls/symmetric_roll_pass.py @@ -0,0 +1,22 @@ +import numpy as np + +from ..symmetric_roll_pass import SymmetricRollPass + + +@SymmetricRollPass.entry_point +def entry_point(self: SymmetricRollPass): + height_change = self.in_profile.height - self.height + return -np.sqrt(self.roll.min_radius * height_change - height_change ** 2 / 4) + + +@SymmetricRollPass.velocity +def velocity(self: SymmetricRollPass): + if self.roll.has_value("neutral_angle"): + return self.roll.working_velocity * np.cos(self.roll.neutral_angle) + else: + return self.roll.working_velocity + + +@SymmetricRollPass.roll_force +def roll_force(self: SymmetricRollPass): + return (self.in_profile.flow_stress + 2 * self.out_profile.flow_stress) / 3 * self.roll.contact_area diff --git a/pyroll/core/roll_pass/hookimpls/three_roll_pass.py b/pyroll/core/roll_pass/hookimpls/three_roll_pass.py new file mode 100644 index 00000000..1ecaa06a --- /dev/null +++ b/pyroll/core/roll_pass/hookimpls/three_roll_pass.py @@ -0,0 +1,89 @@ +import math + +import numpy as np +from shapely import Polygon, clip_by_rect + +from . import helpers +from ..three_roll_pass import ThreeRollPass +from ...grooves import GenericElongationGroove + + +@ThreeRollPass.usable_width +def usable_width3(self: ThreeRollPass) -> float: + return 2 / 3 * np.sqrt(3) * (self.roll.groove.usable_width + self.gap / 2) + + +@ThreeRollPass.tip_width +def tip_width3(self: ThreeRollPass) -> float: + if isinstance(self.roll.groove, GenericElongationGroove): + return ( + 2 / 3 * np.sqrt(3) * (self.roll.groove.usable_width + self.gap / 2) + + self.gap / np.sqrt(3) * np.cos(self.roll.groove.flank_angle) + ) + + +@ThreeRollPass.usable_cross_section +def usable_cross_section3(self: ThreeRollPass) -> Polygon: + return helpers.out_cross_section3(self, self.usable_width) + + +@ThreeRollPass.tip_cross_section +def tip_cross_section3(self: ThreeRollPass) -> Polygon: + return helpers.out_cross_section3(self, self.tip_width) + + +@ThreeRollPass.inscribed_circle_diameter +def inscribed_circle_diameter_from_gap(self: ThreeRollPass) -> float: + if self.has_set_or_cached("gap"): + half = self.roll.groove.usable_width / 2 / np.sqrt(3) + self.gap / np.sqrt(3) + self.roll.groove.depth + return half * 2 + + +@ThreeRollPass.gap +def gap3_from_height(self: ThreeRollPass) -> float: + if self.has_set_or_cached("height"): + return ( + self.height / 2 + - self.roll.groove.usable_width / 2 / np.sqrt(3) + - self.roll.groove.depth + ) * np.sqrt(3) + + +@ThreeRollPass.gap +def gap3_from_icd(self: ThreeRollPass) -> float: + if self.has_set_or_cached("inscribed_circle_diameter"): + return ( + self.inscribed_circle_diameter / 2 + - self.roll.groove.usable_width / 2 / np.sqrt(3) + - self.roll.groove.depth + ) * np.sqrt(3) + + +@ThreeRollPass.height +def height3(self: ThreeRollPass) -> float: + usable_contour = clip_by_rect( + self.contour_lines[1], + -self.roll.groove.usable_width / 2, + -math.inf, + self.roll.groove.usable_width / 2, + math.inf + ) + return abs(2 * usable_contour.bounds[1]) + + +@ThreeRollPass.contact_area +def contact_area3(self: ThreeRollPass) -> float: + return 3 * self.roll.contact_area + + +@ThreeRollPass.target_cross_section_area +def target_cross_section_area_from_target_width3(self: ThreeRollPass) -> float: + if self.has_value("target_width"): + target_cross_section = helpers.out_cross_section3(self, self.target_width) + return target_cross_section.area + + +@ThreeRollPass.power +def roll_power_3(self: ThreeRollPass) -> float: + return 3 * self.roll.roll_power + diff --git a/pyroll/core/roll_pass/hookimpls/two_roll_pass.py b/pyroll/core/roll_pass/hookimpls/two_roll_pass.py new file mode 100644 index 00000000..9765bff1 --- /dev/null +++ b/pyroll/core/roll_pass/hookimpls/two_roll_pass.py @@ -0,0 +1,66 @@ +import numpy as np +from shapely import Polygon + +from . import helpers +from ..two_roll_pass import TwoRollPass +from ...grooves import GenericElongationGroove + + +@TwoRollPass.usable_width +def usable_width(self: TwoRollPass): + return self.roll.groove.usable_width + + +@TwoRollPass.tip_width +def tip_width(self): + if isinstance(self.roll.groove, GenericElongationGroove): + return self.roll.groove.usable_width + self.gap / 2 / np.tan(self.roll.groove.flank_angle) + + +@TwoRollPass.usable_cross_section +def usable_cross_section(self: TwoRollPass) -> Polygon: + return helpers.out_cross_section(self, self.usable_width) + + +@TwoRollPass.tip_cross_section +def tip_cross_section(self: TwoRollPass) -> Polygon: + return helpers.out_cross_section(self, self.tip_width) + + +@TwoRollPass.gap +def gap(self: TwoRollPass): + if self.has_set_or_cached("height"): + return self.height - 2 * self.roll.groove.depth + + +@TwoRollPass.height +def height(self): + if self.has_set_or_cached("gap"): + return self.gap + 2 * self.roll.groove.depth + + +@TwoRollPass.contact_area +def contact_area(self: TwoRollPass): + return 2 * self.roll.contact_area + + +@TwoRollPass.target_cross_section_area +def target_cross_section_area_from_target_width(self: TwoRollPass): + if self.has_value("target_width"): + target_cross_section = helpers.out_cross_section(self, self.target_width) + return target_cross_section.area + + +@TwoRollPass.power +def roll_power(self: TwoRollPass): + return 2 * self.roll.roll_power + + +@TwoRollPass.entry_point +def entry_point_square_oval(self: TwoRollPass): + if "square" in self.in_profile.classifiers and "oval" in self.classifiers: + depth = self.roll.groove.local_depth(self.in_profile.width / 2) + height_change = self.in_profile.height - self.gap - 2 * depth + radius = self.roll.max_radius - depth + return -np.sqrt(radius * height_change - height_change ** 2 / 4) + diff --git a/pyroll/core/roll_pass/roll_pass.py b/pyroll/core/roll_pass/roll_pass.py deleted file mode 100644 index 4d5adbe9..00000000 --- a/pyroll/core/roll_pass/roll_pass.py +++ /dev/null @@ -1,76 +0,0 @@ -from typing import List, cast - -from shapely.affinity import translate, rotate -from shapely.geometry import LineString - -from .base import BaseRollPass -from ..hooks import Hook - - -class RollPass(BaseRollPass): - """Represents a roll pass with three working rolls and 3-fold symmetry.""" - - inscribed_circle_diameter = Hook[float]() - """Diameter of inscribed circle between roll barrels as alternative to roll gap definition.""" - - @property - def contour_lines(self) -> List[LineString]: - if self._contour_lines: - return self._contour_lines - - upper = translate(self.roll.contour_line, yoff=self.gap / 2) - lower = rotate(upper, angle=180, origin=(0, 0)) - - self._contour_lines = [upper, lower] - return self._contour_lines - - @property - def disk_elements(self) -> List['RollPass.DiskElement']: - """A list of disk elements used to subdivide this unit.""" - return list(self._subunits) - - class Profile(BaseRollPass.Profile): - """Represents a profile in context of a roll pass.""" - - @property - def roll_pass(self) -> 'RollPass': - """Reference to the roll pass. Alias for ``self.unit``.""" - return cast(RollPass, self.unit) - - class InProfile(Profile, BaseRollPass.InProfile): - """Represents an incoming profile of a 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) -> 'RollPass': - """Reference to the roll pass.""" - return cast(RollPass, self._roll_pass()) - - class DiskElement(BaseRollPass.DiskElement): - """Represents a disk element in a roll pass.""" - - @property - def roll_pass(self) -> 'RollPass': - """Reference to the roll pass. Alias for ``self.parent``.""" - return cast(RollPass, self.parent) - - class Profile(BaseRollPass.DiskElement.Profile): - """Represents a profile in context of a disk element unit.""" - - @property - def disk_element(self) -> 'RollPass.DiskElement': - """Reference to the disk element. Alias for ``self.unit``""" - return cast(RollPass.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.""" diff --git a/pyroll/core/roll_pass/symmetric_roll_pass.py b/pyroll/core/roll_pass/symmetric_roll_pass.py new file mode 100644 index 00000000..a2f321ec --- /dev/null +++ b/pyroll/core/roll_pass/symmetric_roll_pass.py @@ -0,0 +1,87 @@ +from abc import ABC +from typing import List, cast + +import numpy as np +from .base import BaseRollPass +from ..roll import Roll as BaseRoll + + +class SymmetricRollPass(BaseRollPass, ABC): + """Represents a symmetric roll pass with equal upper and lower working roll.""" + + def __init__( + self, + roll: BaseRoll, + label: str = "", + **kwargs + ): + super().__init__(label, **kwargs) + + self.roll = self.Roll(roll, self) + """The working roll of this pass (all equal).""" + + @property + def disk_elements(self) -> List['SymmetricRollPass.DiskElement']: + """A list of disk elements used to subdivide this unit.""" + return list(self._subunits) + + @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.roll.groove.classifiers) | {"symmetric"} + + def get_root_hook_results(self): + super_results = super().get_root_hook_results() + roll_results = self.roll.evaluate_and_set_hooks() + + return np.concatenate([super_results, roll_results], axis=0) + + def reevaluate_cache(self): + super().reevaluate_cache() + self.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) -> 'SymmetricRollPass': + """Reference to the roll pass. Alias for ``self.unit``.""" + return cast(SymmetricRollPass, self.unit) + + class InProfile(Profile, BaseRollPass.InProfile): + """Represents an incoming profile of a roll pass.""" + + class OutProfile(Profile, BaseRollPass.OutProfile): + """Represents an outgoing profile of a roll pass.""" + + class Roll(BaseRollPass.Roll): + """Represents a roll applied in a :py:class:`RollPass`.""" + + @property + def roll_pass(self) -> 'SymmetricRollPass': + """Reference to the roll pass.""" + return cast(SymmetricRollPass, self._roll_pass()) + + class DiskElement(BaseRollPass.DiskElement): + """Represents a disk element in a roll pass.""" + + @property + def roll_pass(self) -> 'SymmetricRollPass': + """Reference to the roll pass. Alias for ``self.parent``.""" + return cast(SymmetricRollPass, self.parent) + + class Profile(BaseRollPass.DiskElement.Profile): + """Represents a profile in context of a disk element unit.""" + + @property + def disk_element(self) -> 'SymmetricRollPass.DiskElement': + """Reference to the disk element. Alias for ``self.unit``""" + return cast(SymmetricRollPass.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.""" diff --git a/pyroll/core/roll_pass/three_roll_pass.py b/pyroll/core/roll_pass/three_roll_pass.py index 05bd2d61..00707b14 100644 --- a/pyroll/core/roll_pass/three_roll_pass.py +++ b/pyroll/core/roll_pass/three_roll_pass.py @@ -5,14 +5,24 @@ from shapely.geometry import LineString from ..hooks import Hook -from .base import BaseRollPass +from .symmetric_roll_pass import SymmetricRollPass +from ..roll import Roll as BaseRoll -class ThreeRollPass(BaseRollPass): + +class ThreeRollPass(SymmetricRollPass): """Represents a roll pass with three working rolls and 3-fold symmetry.""" inscribed_circle_diameter = Hook[float]() """Diameter of inscribed circle between roll barrels as alternative to roll gap definition.""" + def __init__( + self, + roll: BaseRoll, + label: str = "", + **kwargs + ): + super().__init__(roll, label, **kwargs) + @property def contour_lines(self) -> List[LineString]: if self._contour_lines: @@ -39,7 +49,7 @@ def disk_elements(self) -> List['ThreeRollPass.DiskElement']: """A list of disk elements used to subdivide this unit.""" return list(self._subunits) - class Profile(BaseRollPass.Profile): + class Profile(SymmetricRollPass.Profile): """Represents a profile in context of a roll pass.""" @property @@ -47,15 +57,15 @@ def roll_pass(self) -> 'ThreeRollPass': """Reference to the roll pass. Alias for ``self.unit``.""" return cast(ThreeRollPass, self.unit) - class InProfile(Profile, BaseRollPass.InProfile): + class InProfile(Profile, SymmetricRollPass.InProfile): """Represents an incoming profile of a roll pass.""" - class OutProfile(Profile, BaseRollPass.OutProfile): + class OutProfile(Profile, SymmetricRollPass.OutProfile): """Represents an outgoing profile of a roll pass.""" filling_ratio = Hook[float]() - class Roll(BaseRollPass.Roll): + class Roll(SymmetricRollPass.Roll): """Represents a roll applied in a :py:class:`ThreeRollPass`.""" @property @@ -63,7 +73,7 @@ def roll_pass(self) -> 'ThreeRollPass': """Reference to the roll pass.""" return cast(ThreeRollPass, self._roll_pass()) - class DiskElement(BaseRollPass.DiskElement): + class DiskElement(SymmetricRollPass.DiskElement): """Represents a disk element in a roll pass.""" @property @@ -71,7 +81,7 @@ def roll_pass(self) -> 'ThreeRollPass': """Reference to the roll pass. Alias for ``self.parent``.""" return cast(ThreeRollPass, self.parent) - class Profile(BaseRollPass.DiskElement.Profile): + class Profile(SymmetricRollPass.DiskElement.Profile): """Represents a profile in context of a disk element unit.""" @property @@ -79,8 +89,8 @@ def disk_element(self) -> 'ThreeRollPass.DiskElement': """Reference to the disk element. Alias for ``self.unit``""" return cast(ThreeRollPass.DiskElement, self.unit) - class InProfile(Profile, BaseRollPass.DiskElement.InProfile): + class InProfile(Profile, SymmetricRollPass.DiskElement.InProfile): """Represents an incoming profile of a disk element unit.""" - class OutProfile(Profile, BaseRollPass.DiskElement.OutProfile): + class OutProfile(Profile, SymmetricRollPass.DiskElement.OutProfile): """Represents an outgoing profile of a disk element unit.""" diff --git a/pyroll/core/roll_pass/two_roll_pass.py b/pyroll/core/roll_pass/two_roll_pass.py new file mode 100644 index 00000000..1afa01a7 --- /dev/null +++ b/pyroll/core/roll_pass/two_roll_pass.py @@ -0,0 +1,86 @@ +from typing import List, cast + +import numpy as np +from shapely.affinity import translate, rotate +from shapely.geometry import LineString + +from .symmetric_roll_pass import SymmetricRollPass +from ..roll import Roll as BaseRoll + + +class TwoRollPass(SymmetricRollPass): + """Represents a symmetric two-roll pass with equal upper and lower working roll.""" + + def __init__( + self, + roll: BaseRoll, + label: str = "", + **kwargs + ): + super().__init__(roll, label, **kwargs) + + @property + def contour_lines(self) -> List[LineString]: + if self._contour_lines: + return self._contour_lines + + upper = translate(self.roll.contour_line, yoff=self.gap / 2) + lower = rotate(upper, angle=180, origin=(0, 0)) + + self._contour_lines = [upper, lower] + return self._contour_lines + + @property + def disk_elements(self) -> List['TwoRollPass.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() + roll_results = self.roll.evaluate_and_set_hooks() + + return np.concatenate([super_results, roll_results], axis=0) + + class Profile(SymmetricRollPass.Profile): + """Represents a profile in context of a roll pass.""" + + @property + def roll_pass(self) -> 'TwoRollPass': + """Reference to the roll pass. Alias for ``self.unit``.""" + return cast(TwoRollPass, self.unit) + + class InProfile(Profile, SymmetricRollPass.InProfile): + """Represents an incoming profile of a roll pass.""" + + class OutProfile(Profile, SymmetricRollPass.OutProfile): + """Represents an outgoing profile of a roll pass.""" + + class Roll(SymmetricRollPass.Roll): + """Represents a roll applied in a :py:class:`TwoRollPass`.""" + + @property + def roll_pass(self) -> 'TwoRollPass': + """Reference to the roll pass.""" + return cast(TwoRollPass, self._roll_pass()) + + class DiskElement(SymmetricRollPass.DiskElement): + """Represents a disk element in a roll pass.""" + + @property + def roll_pass(self) -> 'TwoRollPass': + """Reference to the roll pass. Alias for ``self.parent``.""" + return cast(TwoRollPass, self.parent) + + class Profile(SymmetricRollPass.DiskElement.Profile): + """Represents a profile in context of a disk element unit.""" + + @property + def disk_element(self) -> 'TwoRollPass.DiskElement': + """Reference to the disk element. Alias for ``self.unit``""" + return cast(TwoRollPass.DiskElement, self.unit) + + class InProfile(Profile, SymmetricRollPass.DiskElement.InProfile): + """Represents an incoming profile of a disk element unit.""" + + class OutProfile(Profile, SymmetricRollPass.DiskElement.OutProfile): + """Represents an outgoing profile of a disk element unit."""