pyroll-core 2.0.0__py3-none-any.whl
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.
- pyroll/core/__init__.py +38 -0
- pyroll/core/config.py +155 -0
- pyroll/core/disk_elements/__init__.py +2 -0
- pyroll/core/disk_elements/disk_element_unit.py +73 -0
- pyroll/core/disk_elements/hookimpls.py +26 -0
- pyroll/core/grooves/__init__.py +10 -0
- pyroll/core/grooves/base.py +59 -0
- pyroll/core/grooves/boxes/__init__.py +3 -0
- pyroll/core/grooves/boxes/box.py +57 -0
- pyroll/core/grooves/boxes/constricted_box.py +63 -0
- pyroll/core/grooves/boxes/upset_box.py +18 -0
- pyroll/core/grooves/diamonds/__init__.py +3 -0
- pyroll/core/grooves/diamonds/diamond.py +60 -0
- pyroll/core/grooves/diamonds/gothic.py +41 -0
- pyroll/core/grooves/diamonds/square.py +48 -0
- pyroll/core/grooves/flat.py +31 -0
- pyroll/core/grooves/generic_elongation.py +258 -0
- pyroll/core/grooves/generic_elongation_solvers.py +422 -0
- pyroll/core/grooves/hexagonal.py +57 -0
- pyroll/core/grooves/ovals/__init__.py +8 -0
- pyroll/core/grooves/ovals/circular_oval.py +43 -0
- pyroll/core/grooves/ovals/constricted_circular_oval.py +53 -0
- pyroll/core/grooves/ovals/constricted_swedish_oval.py +61 -0
- pyroll/core/grooves/ovals/flat_oval.py +56 -0
- pyroll/core/grooves/ovals/oval_3radii.py +47 -0
- pyroll/core/grooves/ovals/oval_3radii_flanked.py +65 -0
- pyroll/core/grooves/ovals/swedish_oval.py +57 -0
- pyroll/core/grooves/ovals/upset_oval.py +47 -0
- pyroll/core/grooves/rounds/__init__.py +2 -0
- pyroll/core/grooves/rounds/false_round.py +65 -0
- pyroll/core/grooves/rounds/round.py +44 -0
- pyroll/core/grooves/spline.py +92 -0
- pyroll/core/hooks.py +420 -0
- pyroll/core/log.py +7 -0
- pyroll/core/profile/__init__.py +3 -0
- pyroll/core/profile/hookimpls.py +58 -0
- pyroll/core/profile/profile.py +380 -0
- pyroll/core/repr.py +49 -0
- pyroll/core/roll/__init__.py +3 -0
- pyroll/core/roll/hookimpls.py +65 -0
- pyroll/core/roll/roll.py +145 -0
- pyroll/core/roll_pass/__init__.py +8 -0
- pyroll/core/roll_pass/deformation_unit.py +61 -0
- pyroll/core/roll_pass/hookimpls/__init__.py +5 -0
- pyroll/core/roll_pass/hookimpls/deformation_unit.py +78 -0
- pyroll/core/roll_pass/hookimpls/disk_element.py +16 -0
- pyroll/core/roll_pass/hookimpls/profile.py +94 -0
- pyroll/core/roll_pass/hookimpls/roll.py +43 -0
- pyroll/core/roll_pass/hookimpls/roll_pass.py +107 -0
- pyroll/core/roll_pass/roll_pass.py +168 -0
- pyroll/core/roll_pass/three_roll_pass.py +84 -0
- pyroll/core/rotator/__init__.py +3 -0
- pyroll/core/rotator/hookimpls.py +71 -0
- pyroll/core/rotator/rotator.py +65 -0
- pyroll/core/sequence/__init__.py +3 -0
- pyroll/core/sequence/hookimpls.py +18 -0
- pyroll/core/sequence/sequence.py +68 -0
- pyroll/core/shapes.py +104 -0
- pyroll/core/transport/__init__.py +3 -0
- pyroll/core/transport/hookimpls.py +17 -0
- pyroll/core/transport/transport.py +57 -0
- pyroll/core/unit/__init__.py +3 -0
- pyroll/core/unit/hookimpls.py +67 -0
- pyroll/core/unit/unit.py +271 -0
- pyroll_core-2.0.0.dist-info/METADATA +109 -0
- pyroll_core-2.0.0.dist-info/RECORD +68 -0
- pyroll_core-2.0.0.dist-info/WHEEL +4 -0
- pyroll_core-2.0.0.dist-info/licenses/LICENSE +11 -0
pyroll/core/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
VERSION = "2.0.0"
|
|
2
|
+
|
|
3
|
+
from . import shapes as _
|
|
4
|
+
|
|
5
|
+
from .grooves import (
|
|
6
|
+
BoxGroove, ConstrictedBoxGroove, UpsetBoxGroove, ConstrictedUpsetBoxGroove,
|
|
7
|
+
SquareGroove, DiamondGroove, GothicGroove,
|
|
8
|
+
RoundGroove, FalseRoundGroove,
|
|
9
|
+
CircularOvalGroove, FlatOvalGroove, SwedishOvalGroove, ConstrictedSwedishOvalGroove, Oval3RadiiGroove,
|
|
10
|
+
Oval3RadiiFlankedGroove, UpsetOvalGroove, ConstrictedCircularOvalGroove,
|
|
11
|
+
SplineGroove,
|
|
12
|
+
GenericElongationGroove,
|
|
13
|
+
FlatGroove,
|
|
14
|
+
HexagonalGroove
|
|
15
|
+
)
|
|
16
|
+
from .transport import Transport
|
|
17
|
+
from .roll_pass import RollPass, DeformationUnit, ThreeRollPass
|
|
18
|
+
from .unit import Unit
|
|
19
|
+
from .roll import Roll
|
|
20
|
+
from .profile import Profile
|
|
21
|
+
from .rotator import Rotator
|
|
22
|
+
from .sequence import PassSequence
|
|
23
|
+
from .hooks import Hook, HookHost, HookFunction, root_hooks
|
|
24
|
+
from .disk_elements import DiskElementUnit
|
|
25
|
+
from .config import Config, config
|
|
26
|
+
|
|
27
|
+
root_hooks.update(
|
|
28
|
+
{
|
|
29
|
+
RollPass.roll_force,
|
|
30
|
+
RollPass.Roll.roll_torque,
|
|
31
|
+
Unit.OutProfile.cross_section,
|
|
32
|
+
Unit.OutProfile.strain,
|
|
33
|
+
Unit.OutProfile.length,
|
|
34
|
+
Unit.OutProfile.classifiers,
|
|
35
|
+
Unit.OutProfile.t,
|
|
36
|
+
PassSequence.total_elongation,
|
|
37
|
+
}
|
|
38
|
+
)
|
pyroll/core/config.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional, Iterable, Mapping, Any, Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConfigValue:
|
|
7
|
+
"""Helper descriptor for storing configuration values, able to determine the value from explictly set values,
|
|
8
|
+
environment variables and default values."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, default, env_var: Optional[str] = None, env_var_prefix: Optional[str] = None,
|
|
11
|
+
parser: Optional[Callable[[str], Any]] = None):
|
|
12
|
+
self.default = default
|
|
13
|
+
self.type = type(default)
|
|
14
|
+
self.parser = parser
|
|
15
|
+
|
|
16
|
+
self._env_var = env_var
|
|
17
|
+
self._env_var_prefix = env_var_prefix
|
|
18
|
+
|
|
19
|
+
def __set_name__(self, owner: type, name: str):
|
|
20
|
+
self.owner = owner
|
|
21
|
+
self.name = name
|
|
22
|
+
if not self._env_var_prefix:
|
|
23
|
+
self._env_var_prefix = self.owner.__module__.upper().replace('.', '_')
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def env_var(self):
|
|
27
|
+
"""The name of the related environment variable."""
|
|
28
|
+
if self._env_var:
|
|
29
|
+
return self._env_var
|
|
30
|
+
|
|
31
|
+
return f"{self._env_var_prefix}_{self.name.upper()}"
|
|
32
|
+
|
|
33
|
+
def __get__(self, instance, owner):
|
|
34
|
+
value = getattr(instance, "_" + self.name, None)
|
|
35
|
+
|
|
36
|
+
if value is not None:
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
value = os.getenv(self.env_var, None)
|
|
40
|
+
|
|
41
|
+
if value is not None:
|
|
42
|
+
return self._parse(value)
|
|
43
|
+
|
|
44
|
+
return self.default
|
|
45
|
+
|
|
46
|
+
def __set__(self, instance, value):
|
|
47
|
+
setattr(instance, "_" + self.name, value)
|
|
48
|
+
|
|
49
|
+
def __delete__(self, instance):
|
|
50
|
+
delattr(instance, "_" + self.name)
|
|
51
|
+
|
|
52
|
+
def _parse(self, s: str):
|
|
53
|
+
"""Parse value from string."""
|
|
54
|
+
if self.parser:
|
|
55
|
+
return self.parser(s)
|
|
56
|
+
if self.type is bool:
|
|
57
|
+
if s.lower().strip() == "true":
|
|
58
|
+
return True
|
|
59
|
+
elif s.lower().strip() == "false":
|
|
60
|
+
return False
|
|
61
|
+
raise ValueError(f"{s} could not be parsed to bool")
|
|
62
|
+
if self.type is Path:
|
|
63
|
+
return Path(s)
|
|
64
|
+
if self.type is str:
|
|
65
|
+
return s
|
|
66
|
+
if issubclass(self.type, Mapping):
|
|
67
|
+
return self.type((p2.strip() for p2 in p.strip().split("=")) for p in s.split(","))
|
|
68
|
+
if issubclass(self.type, Iterable):
|
|
69
|
+
return self.type(p.strip() for p in s.split(","))
|
|
70
|
+
|
|
71
|
+
return self.type(s)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ConfigMeta(type):
|
|
75
|
+
def to_dict(cls):
|
|
76
|
+
"""Return the config values of this class as dict."""
|
|
77
|
+
return {
|
|
78
|
+
n: v
|
|
79
|
+
for n, v in type(cls).__dict__.items()
|
|
80
|
+
if isinstance(v, ConfigValue)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def update(cls, d: dict[str, Any]):
|
|
84
|
+
"""
|
|
85
|
+
Update the config values of this class from a dict.
|
|
86
|
+
|
|
87
|
+
:return: the updated contents
|
|
88
|
+
"""
|
|
89
|
+
for n, v in d.items():
|
|
90
|
+
cv = type(cls).__dict__.get(n, None)
|
|
91
|
+
if isinstance(cv, ConfigValue):
|
|
92
|
+
setattr(cls, n, v)
|
|
93
|
+
else:
|
|
94
|
+
AttributeError(f"{cls} has no config value {n}")
|
|
95
|
+
|
|
96
|
+
return cls.to_dict()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def config(env_var_prefix):
|
|
100
|
+
"""
|
|
101
|
+
Decorator for creating config classes.
|
|
102
|
+
Automatically creates a metaclass with respective :py:class:`ConfigValue` descriptors for class attributes with
|
|
103
|
+
uppercase names that do not start with underscore.
|
|
104
|
+
|
|
105
|
+
Use it as follows::
|
|
106
|
+
|
|
107
|
+
@config("PREFIX")
|
|
108
|
+
class Config:
|
|
109
|
+
VAR1 = 42
|
|
110
|
+
VAR2 = "abc"
|
|
111
|
+
|
|
112
|
+
# not modified
|
|
113
|
+
_PRIV_VAR = None
|
|
114
|
+
normal_var = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
:param env_var_prefix: prefix for the respective env vars, should be uppercase and delimited by underscores
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def dec(cls):
|
|
121
|
+
meta_dict = {}
|
|
122
|
+
cls_dict = dict(cls.__dict__)
|
|
123
|
+
|
|
124
|
+
for n, v in cls.__dict__.items():
|
|
125
|
+
if n.isupper() and not n.startswith("_"):
|
|
126
|
+
del cls_dict[n]
|
|
127
|
+
if not isinstance(v, ConfigValue):
|
|
128
|
+
meta_dict[n] = ConfigValue(default=v, env_var_prefix=env_var_prefix)
|
|
129
|
+
else:
|
|
130
|
+
meta_dict[n] = ConfigValue(default=v.default, env_var_prefix=env_var_prefix)
|
|
131
|
+
|
|
132
|
+
meta = type(cls.__name__ + "Meta", (ConfigMeta,), meta_dict)
|
|
133
|
+
cls = meta(cls.__name__, cls.__bases__, cls_dict)
|
|
134
|
+
return cls
|
|
135
|
+
|
|
136
|
+
return dec
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@config("PYROLL_CORE")
|
|
140
|
+
class Config:
|
|
141
|
+
"""Configuration class for ``pyroll.core``."""
|
|
142
|
+
ROLL_PASS_AUTO_ROTATION = True
|
|
143
|
+
"""Whether to enable automatic rotation of incoming profiles in roll passes by default."""
|
|
144
|
+
|
|
145
|
+
GROOVE_PADDING = 0.2
|
|
146
|
+
"""Fraction of the total groove width that is added at the sides of the groove contour to represent the roll face."""
|
|
147
|
+
|
|
148
|
+
DEFAULT_MAX_ITERATION_COUNT = 100
|
|
149
|
+
"""Default maximum count of iterations before aborting the loop."""
|
|
150
|
+
|
|
151
|
+
DEFAULT_ITERATION_PRECISION = 1e-2
|
|
152
|
+
"""Default precision of iteration loops required to break successfully."""
|
|
153
|
+
|
|
154
|
+
ROLL_SURFACE_DISCRETIZATION_COUNT = 100
|
|
155
|
+
"""Count of discrete points used to describe the roll surface."""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import List, cast
|
|
2
|
+
|
|
3
|
+
from ..unit import Unit
|
|
4
|
+
from ..hooks import Hook
|
|
5
|
+
from ..profile import Profile as BaseProfile
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DiskElementUnit(Unit):
|
|
9
|
+
"""Base class for units that can be divided in disk elements."""
|
|
10
|
+
|
|
11
|
+
disk_element_count = Hook[int]()
|
|
12
|
+
"""Number of intermediate profiles to create during unit solution (in and out profiles are not counted within)."""
|
|
13
|
+
|
|
14
|
+
class Profile(Unit.Profile):
|
|
15
|
+
"""Represents a profile in context of a disked unit."""
|
|
16
|
+
|
|
17
|
+
class InProfile(Profile, Unit.InProfile):
|
|
18
|
+
"""Represents an incoming profile of a disked unit."""
|
|
19
|
+
|
|
20
|
+
class OutProfile(Profile, Unit.OutProfile):
|
|
21
|
+
"""Represents an outgoing profile of a disked unit."""
|
|
22
|
+
|
|
23
|
+
class DiskElement(Unit):
|
|
24
|
+
"""
|
|
25
|
+
Represents a disk element unit, a unit used to subdivide other units in rolling direction.
|
|
26
|
+
The basic disk element is not intended to be used directly.
|
|
27
|
+
Use the specialized disk elements of the respective parent unit instead.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
parent: 'Unit',
|
|
33
|
+
index: int,
|
|
34
|
+
**kwargs
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
:param label: label for human identification
|
|
38
|
+
:param kwargs: additional hook values as keyword arguments to set explicitly
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
super().__init__(label=f"{parent}[{index}]", parent=parent, **kwargs)
|
|
42
|
+
|
|
43
|
+
class Profile(Unit.Profile):
|
|
44
|
+
"""Represents a profile in context of a disk element unit."""
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def disk_element(self) -> 'DiskElementUnit.DiskElement':
|
|
48
|
+
"""Reference to the disk element. Alias for ``self.unit``"""
|
|
49
|
+
return cast(DiskElementUnit.DiskElement, self.unit)
|
|
50
|
+
|
|
51
|
+
class InProfile(Profile, Unit.InProfile):
|
|
52
|
+
"""Represents an incoming profile of a disk element unit."""
|
|
53
|
+
|
|
54
|
+
class OutProfile(Profile, Unit.OutProfile):
|
|
55
|
+
"""Represents an outgoing profile of a disk element unit."""
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def disk_elements(self) -> List['DiskElement']:
|
|
59
|
+
"""A list of disk elements used to subdivide this unit."""
|
|
60
|
+
return self._subunits
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def __attrs__(self):
|
|
64
|
+
return super().__attrs__ | {
|
|
65
|
+
"disk_elements": self.disk_elements
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def init_solve(self, in_profile: BaseProfile):
|
|
69
|
+
super().init_solve(in_profile)
|
|
70
|
+
if not self._subunits:
|
|
71
|
+
self._subunits = self._SubUnitsList(
|
|
72
|
+
self, [self.DiskElement(self, i) for i in range(self.disk_element_count)]
|
|
73
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .disk_element_unit import DiskElementUnit
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@DiskElementUnit.disk_element_count
|
|
5
|
+
def no_disks(self: DiskElementUnit):
|
|
6
|
+
return 0
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@DiskElementUnit.DiskElement.duration
|
|
10
|
+
def disk_duration(self: DiskElementUnit.DiskElement):
|
|
11
|
+
return self.parent.duration / self.parent.disk_element_count
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@DiskElementUnit.DiskElement.length
|
|
15
|
+
def disk_length(self: DiskElementUnit.DiskElement):
|
|
16
|
+
return self.parent.length / self.parent.disk_element_count
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@DiskElementUnit.DiskElement.InProfile.x
|
|
20
|
+
def in_x(self: DiskElementUnit.DiskElement.InProfile):
|
|
21
|
+
try:
|
|
22
|
+
# get from prev disk
|
|
23
|
+
return self.disk_element.prev.out_profile.x
|
|
24
|
+
except IndexError:
|
|
25
|
+
# is first disk: get from parent in_profile
|
|
26
|
+
return self.disk_element.parent.in_profile.x
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .base import GrooveBase
|
|
2
|
+
from .spline import SplineGroove
|
|
3
|
+
from .generic_elongation import GenericElongationGroove
|
|
4
|
+
from .boxes import BoxGroove, ConstrictedBoxGroove, UpsetBoxGroove, ConstrictedUpsetBoxGroove
|
|
5
|
+
from .diamonds import DiamondGroove, SquareGroove, GothicGroove
|
|
6
|
+
from .ovals import CircularOvalGroove, FlatOvalGroove, SwedishOvalGroove, ConstrictedSwedishOvalGroove, \
|
|
7
|
+
Oval3RadiiGroove, Oval3RadiiFlankedGroove, UpsetOvalGroove, ConstrictedCircularOvalGroove
|
|
8
|
+
from .rounds import RoundGroove, FalseRoundGroove
|
|
9
|
+
from .flat import FlatGroove
|
|
10
|
+
from .hexagonal import HexagonalGroove
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Union, Tuple
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from shapely.geometry import Polygon, LineString
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GrooveBase(ABC):
|
|
9
|
+
"""Abstract base class for all grooves."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def classifiers(self) -> set[str]:
|
|
14
|
+
"""A tuple of keywords to specify the type classifiers of this groove."""
|
|
15
|
+
raise NotImplemented
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def cross_section(self) -> Polygon:
|
|
20
|
+
"""A polygon representing the cross-section of this groove limited by the contour line and y=0."""
|
|
21
|
+
raise NotImplemented
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def usable_width(self) -> float:
|
|
26
|
+
"""The usable width of the groove, meaning the width of ideal filling."""
|
|
27
|
+
raise NotImplemented
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def width(self) -> float:
|
|
32
|
+
"""The maximum width of the groove representing the definition region in z-direction."""
|
|
33
|
+
raise NotImplemented
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def depth(self) -> float:
|
|
38
|
+
"""The maximum depth of the groove."""
|
|
39
|
+
raise NotImplemented
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def contour_line(self) -> LineString:
|
|
44
|
+
"""A line representing the geometry of the groove contour."""
|
|
45
|
+
raise NotImplemented
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def contour_points(self) -> np.ndarray:
|
|
50
|
+
"""An array of the contour line's points of shape (n, 2)."""
|
|
51
|
+
raise NotImplemented
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def local_depth(self, z: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
|
|
55
|
+
"""Function of the local groove depth in dependence on the z-coordinate."""
|
|
56
|
+
raise NotImplemented
|
|
57
|
+
|
|
58
|
+
def __str__(self):
|
|
59
|
+
return f"{type(self).__name__} {self.usable_width:.4g} x {self.depth:.4g} ({', '.join(self.classifiers)})"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ..generic_elongation import GenericElongationGroove
|
|
6
|
+
from ..generic_elongation_solvers import solve_box_like
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BoxGroove(GenericElongationGroove):
|
|
10
|
+
"""Represents a box-shaped groove."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
r1: float,
|
|
15
|
+
r2: float,
|
|
16
|
+
depth: float,
|
|
17
|
+
ground_width: Optional[float] = None,
|
|
18
|
+
even_ground_width: Optional[float] = None,
|
|
19
|
+
usable_width: Optional[float] = None,
|
|
20
|
+
flank_angle: Optional[float] = None,
|
|
21
|
+
pad_angle: float = 0,
|
|
22
|
+
**kwargs
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Exactly two of ``ground_width``, ``even_ground_width``, ``usable_width`` and ``flank_angle`` must be given,
|
|
26
|
+
but not ``ground_width`` and ``even_ground_width`` at the same time.
|
|
27
|
+
Widths are always measured at the intersection of the extrapolated ground, face and flanks.
|
|
28
|
+
All angles are measured in ° (degree).
|
|
29
|
+
|
|
30
|
+
:param r1: radius 1 (face/flank)
|
|
31
|
+
:param r2: radius 2 (flank/ground)
|
|
32
|
+
:param depth: maximum depth
|
|
33
|
+
:param ground_width: width of the groove ground
|
|
34
|
+
:param even_ground_width: width of the even ground line
|
|
35
|
+
:param usable_width: usable width of the groove
|
|
36
|
+
:param flank_angle: inclination angle of the flanks
|
|
37
|
+
:param pad_angle: angle between z-axis and the roll face padding
|
|
38
|
+
:param kwargs: more keyword arguments passed to the GenericElongationGroove constructor
|
|
39
|
+
:raises ValueError: if not exactly two of ground_width, usable_width and flank_angle are given
|
|
40
|
+
"""
|
|
41
|
+
if flank_angle is not None:
|
|
42
|
+
flank_angle = np.deg2rad(flank_angle)
|
|
43
|
+
|
|
44
|
+
sol = solve_box_like(
|
|
45
|
+
r2=r2, r4=0, depth=depth, ground_width=ground_width, usable_width=usable_width, flank_angle=flank_angle,
|
|
46
|
+
indent=0, even_ground_width=even_ground_width
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
super().__init__(
|
|
50
|
+
r1=r1, r2=r2, pad_angle=np.deg2rad(pad_angle),
|
|
51
|
+
**sol,
|
|
52
|
+
**kwargs
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def classifiers(self):
|
|
57
|
+
return {"box"} | super().classifiers
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ..generic_elongation import GenericElongationGroove
|
|
6
|
+
from ..generic_elongation_solvers import solve_box_like
|
|
7
|
+
from .box import BoxGroove
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConstrictedBoxGroove(BoxGroove):
|
|
11
|
+
"""Represents a box-shaped groove with an indented ground."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
r1: float,
|
|
16
|
+
r2: float,
|
|
17
|
+
r4: float,
|
|
18
|
+
depth: float,
|
|
19
|
+
indent: float,
|
|
20
|
+
ground_width: Optional[float] = None,
|
|
21
|
+
even_ground_width: Optional[float] = None,
|
|
22
|
+
usable_width: Optional[float] = None,
|
|
23
|
+
flank_angle: Optional[float] = None,
|
|
24
|
+
pad_angle: float = 0,
|
|
25
|
+
**kwargs
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Exactly two of ``ground_width``, ``even_ground_width``, ``usable_width`` and ``flank_angle`` must be given,
|
|
29
|
+
but not ``ground_width`` and ``even_ground_width`` at the same time.
|
|
30
|
+
Widths are always measured at the intersection of the extrapolated ground, face and flanks.
|
|
31
|
+
All angles are measured in ° (degree).
|
|
32
|
+
|
|
33
|
+
:param r1: radius 1 (face/flank)
|
|
34
|
+
:param r2: radius 2 (flank)
|
|
35
|
+
:param r4: radius 4 (indent)
|
|
36
|
+
:param depth: maximum depth
|
|
37
|
+
:param indent: depth of the indent
|
|
38
|
+
:param ground_width: width of the groove ground
|
|
39
|
+
:param even_ground_width: width of the even ground line
|
|
40
|
+
:param usable_width: usable width of the groove
|
|
41
|
+
:param flank_angle: inclination angle of the flanks
|
|
42
|
+
:param pad_angle: angle between z-axis and the roll face padding
|
|
43
|
+
:param kwargs: more keyword arguments passed to the GenericElongationGroove constructor
|
|
44
|
+
:raises ValueError: if not exactly two of ground_width, usable_width and flank_angle are given
|
|
45
|
+
"""
|
|
46
|
+
if flank_angle is not None:
|
|
47
|
+
flank_angle = np.deg2rad(flank_angle)
|
|
48
|
+
|
|
49
|
+
sol = solve_box_like(
|
|
50
|
+
r2=r2, r4=r4, depth=depth, ground_width=ground_width, usable_width=usable_width, flank_angle=flank_angle,
|
|
51
|
+
indent=indent, even_ground_width=even_ground_width
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
GenericElongationGroove.__init__(
|
|
55
|
+
self,
|
|
56
|
+
r1=r1, r2=r2, r4=r4, pad_angle=np.deg2rad(pad_angle), indent=indent,
|
|
57
|
+
**sol,
|
|
58
|
+
**kwargs
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def classifiers(self):
|
|
63
|
+
return {"box", "constricted"} | super().classifiers
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .box import BoxGroove
|
|
2
|
+
from .constricted_box import ConstrictedBoxGroove
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class UpsetBoxGroove(BoxGroove):
|
|
6
|
+
"""Represents a box-shaped groove with a high height/width ratio."""
|
|
7
|
+
|
|
8
|
+
@property
|
|
9
|
+
def classifiers(self):
|
|
10
|
+
return {"box", "upset"} | super().classifiers
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConstrictedUpsetBoxGroove(ConstrictedBoxGroove, UpsetBoxGroove):
|
|
14
|
+
"""Represents a box-shaped groove with a high height/width ratio and an indented ground."""
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def classifiers(self):
|
|
18
|
+
return {"box", "constricted", "upset"} | super().classifiers
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy import deg2rad
|
|
5
|
+
|
|
6
|
+
from ..generic_elongation import GenericElongationGroove
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DiamondGroove(GenericElongationGroove):
|
|
10
|
+
"""Represent a diamond-shaped groove."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
r1: float,
|
|
15
|
+
r2: float,
|
|
16
|
+
usable_width: Optional[float] = None,
|
|
17
|
+
tip_depth: Optional[float] = None,
|
|
18
|
+
tip_angle: Optional[float] = None,
|
|
19
|
+
pad_angle: float = 0
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Exactly two of usable_width, tip_depth and tip_angle must be given.
|
|
23
|
+
Widths are always measured at the intersection of the extrapolated ground, face and flanks.
|
|
24
|
+
All angles are measured in ° (degree).
|
|
25
|
+
|
|
26
|
+
:param r1: radius 1 (face/flank)
|
|
27
|
+
:param r2: radius 2 (flank/ground)
|
|
28
|
+
:param usable_width: usable width of the groove
|
|
29
|
+
:param tip_depth: depth of the intersection of the extrapolated flanks
|
|
30
|
+
:param tip_angle: angle between the flanks
|
|
31
|
+
:param pad_angle: angle between z-axis and the roll face padding
|
|
32
|
+
:raises ValueError: if not exactly two of usable_width, tip_depth and tip_angle are given
|
|
33
|
+
"""
|
|
34
|
+
if tip_angle is not None:
|
|
35
|
+
tip_angle = np.deg2rad(tip_angle)
|
|
36
|
+
|
|
37
|
+
if usable_width and tip_depth and not tip_angle:
|
|
38
|
+
alpha = np.arctan(tip_depth / (usable_width / 2))
|
|
39
|
+
|
|
40
|
+
elif usable_width and tip_angle and not tip_depth:
|
|
41
|
+
alpha = np.pi / 2 - tip_angle / 2
|
|
42
|
+
tip_depth = usable_width / 2 * np.tan(alpha)
|
|
43
|
+
|
|
44
|
+
elif tip_depth and tip_angle and not usable_width:
|
|
45
|
+
alpha = np.pi / 2 - tip_angle / 2
|
|
46
|
+
usable_width = tip_depth / np.tan(alpha) * 2
|
|
47
|
+
else:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"Exactly two of the following arguments must be given: usable_width, tip_depth, tip_angle."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
depth = tip_depth - r2 / np.cos(alpha) + r2
|
|
53
|
+
|
|
54
|
+
super().__init__(
|
|
55
|
+
usable_width=usable_width, depth=depth, r1=r1, r2=r2, flank_angle=alpha, pad_angle=deg2rad(pad_angle)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def classifiers(self):
|
|
60
|
+
return {"diamond",} | super().classifiers
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ..generic_elongation import GenericElongationGroove
|
|
3
|
+
from ..generic_elongation_solvers import solve_r123
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GothicGroove(GenericElongationGroove):
|
|
7
|
+
"""Represents a square-shaped groove with curved flanks similar to a gothic arc."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
r1: float,
|
|
12
|
+
r2: float,
|
|
13
|
+
r3: float,
|
|
14
|
+
depth: float,
|
|
15
|
+
usable_width: float,
|
|
16
|
+
pad_angle: float = 0
|
|
17
|
+
):
|
|
18
|
+
"""
|
|
19
|
+
Widths are always measured at the intersection of the extrapolated ground, face and flanks.
|
|
20
|
+
|
|
21
|
+
:param r1: radius 1 (face/flank)
|
|
22
|
+
:param r2: radius 2 (flank/ground)
|
|
23
|
+
:param r3: radius 3 (ground)
|
|
24
|
+
:param depth: maximum depth
|
|
25
|
+
:param usable_width: usable width of the groove
|
|
26
|
+
:param pad_angle: angle between z-axis and the roll face padding
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
pad_angle = np.deg2rad(pad_angle)
|
|
30
|
+
|
|
31
|
+
sol = solve_r123(r1, r2, r3, depth, usable_width, pad_angle)
|
|
32
|
+
|
|
33
|
+
super().__init__(
|
|
34
|
+
usable_width=usable_width, depth=depth,
|
|
35
|
+
r1=r1, r2=r2, r3=r3,
|
|
36
|
+
flank_angle=sol["flank_angle"], alpha3=sol["alpha3"], pad_angle=pad_angle
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def classifiers(self):
|
|
41
|
+
return {"diamond", "square", "gothic"} | super().classifiers
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .diamond import DiamondGroove
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SquareGroove(DiamondGroove):
|
|
9
|
+
"""Represents a square-shaped groove (diamond with tip angle near 90°)."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
r1: float,
|
|
14
|
+
r2: float,
|
|
15
|
+
usable_width: Optional[float] = None,
|
|
16
|
+
tip_depth: Optional[float] = None,
|
|
17
|
+
tip_angle: Optional[float] = None,
|
|
18
|
+
pad_angle: float = 0
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Exactly two of usable_width, tip_depth and tip_angle must be given.
|
|
22
|
+
Widths are always measured at the intersection of the extrapolated ground, face and flanks.
|
|
23
|
+
All angles are measured in ° (degree).
|
|
24
|
+
|
|
25
|
+
:param r1: radius 1 (face/flank)
|
|
26
|
+
:param r2: radius 2 (flank/ground)
|
|
27
|
+
:param usable_width: usable width of the groove
|
|
28
|
+
:param tip_depth: depth of the intersection of the extrapolated flanks
|
|
29
|
+
:param tip_angle: angle between the flanks
|
|
30
|
+
:param pad_angle: angle between z-axis and the roll face padding
|
|
31
|
+
:raises ValueError: if not exactly two of usable_width, tip_depth and tip_angle are given
|
|
32
|
+
:raises ValueError: if tip angle is <85° or >95° (no matter if given or calculated internally)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
super().__init__(
|
|
36
|
+
r1=r1, r2=r2, usable_width=usable_width, tip_depth=tip_depth, tip_angle=tip_angle, pad_angle=pad_angle
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
tip_angle_deg = 180 - 2 * np.rad2deg(self.flank_angle)
|
|
40
|
+
if tip_angle_deg > 95 or tip_angle_deg < 85:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"The tip angle of this SquareGroove significantly deviates from 90°, "
|
|
43
|
+
"you should use a DiamondGroove instead."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def classifiers(self):
|
|
48
|
+
return {"diamond", "square"} | super().classifiers
|