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.
Files changed (68) hide show
  1. pyroll/core/__init__.py +38 -0
  2. pyroll/core/config.py +155 -0
  3. pyroll/core/disk_elements/__init__.py +2 -0
  4. pyroll/core/disk_elements/disk_element_unit.py +73 -0
  5. pyroll/core/disk_elements/hookimpls.py +26 -0
  6. pyroll/core/grooves/__init__.py +10 -0
  7. pyroll/core/grooves/base.py +59 -0
  8. pyroll/core/grooves/boxes/__init__.py +3 -0
  9. pyroll/core/grooves/boxes/box.py +57 -0
  10. pyroll/core/grooves/boxes/constricted_box.py +63 -0
  11. pyroll/core/grooves/boxes/upset_box.py +18 -0
  12. pyroll/core/grooves/diamonds/__init__.py +3 -0
  13. pyroll/core/grooves/diamonds/diamond.py +60 -0
  14. pyroll/core/grooves/diamonds/gothic.py +41 -0
  15. pyroll/core/grooves/diamonds/square.py +48 -0
  16. pyroll/core/grooves/flat.py +31 -0
  17. pyroll/core/grooves/generic_elongation.py +258 -0
  18. pyroll/core/grooves/generic_elongation_solvers.py +422 -0
  19. pyroll/core/grooves/hexagonal.py +57 -0
  20. pyroll/core/grooves/ovals/__init__.py +8 -0
  21. pyroll/core/grooves/ovals/circular_oval.py +43 -0
  22. pyroll/core/grooves/ovals/constricted_circular_oval.py +53 -0
  23. pyroll/core/grooves/ovals/constricted_swedish_oval.py +61 -0
  24. pyroll/core/grooves/ovals/flat_oval.py +56 -0
  25. pyroll/core/grooves/ovals/oval_3radii.py +47 -0
  26. pyroll/core/grooves/ovals/oval_3radii_flanked.py +65 -0
  27. pyroll/core/grooves/ovals/swedish_oval.py +57 -0
  28. pyroll/core/grooves/ovals/upset_oval.py +47 -0
  29. pyroll/core/grooves/rounds/__init__.py +2 -0
  30. pyroll/core/grooves/rounds/false_round.py +65 -0
  31. pyroll/core/grooves/rounds/round.py +44 -0
  32. pyroll/core/grooves/spline.py +92 -0
  33. pyroll/core/hooks.py +420 -0
  34. pyroll/core/log.py +7 -0
  35. pyroll/core/profile/__init__.py +3 -0
  36. pyroll/core/profile/hookimpls.py +58 -0
  37. pyroll/core/profile/profile.py +380 -0
  38. pyroll/core/repr.py +49 -0
  39. pyroll/core/roll/__init__.py +3 -0
  40. pyroll/core/roll/hookimpls.py +65 -0
  41. pyroll/core/roll/roll.py +145 -0
  42. pyroll/core/roll_pass/__init__.py +8 -0
  43. pyroll/core/roll_pass/deformation_unit.py +61 -0
  44. pyroll/core/roll_pass/hookimpls/__init__.py +5 -0
  45. pyroll/core/roll_pass/hookimpls/deformation_unit.py +78 -0
  46. pyroll/core/roll_pass/hookimpls/disk_element.py +16 -0
  47. pyroll/core/roll_pass/hookimpls/profile.py +94 -0
  48. pyroll/core/roll_pass/hookimpls/roll.py +43 -0
  49. pyroll/core/roll_pass/hookimpls/roll_pass.py +107 -0
  50. pyroll/core/roll_pass/roll_pass.py +168 -0
  51. pyroll/core/roll_pass/three_roll_pass.py +84 -0
  52. pyroll/core/rotator/__init__.py +3 -0
  53. pyroll/core/rotator/hookimpls.py +71 -0
  54. pyroll/core/rotator/rotator.py +65 -0
  55. pyroll/core/sequence/__init__.py +3 -0
  56. pyroll/core/sequence/hookimpls.py +18 -0
  57. pyroll/core/sequence/sequence.py +68 -0
  58. pyroll/core/shapes.py +104 -0
  59. pyroll/core/transport/__init__.py +3 -0
  60. pyroll/core/transport/hookimpls.py +17 -0
  61. pyroll/core/transport/transport.py +57 -0
  62. pyroll/core/unit/__init__.py +3 -0
  63. pyroll/core/unit/hookimpls.py +67 -0
  64. pyroll/core/unit/unit.py +271 -0
  65. pyroll_core-2.0.0.dist-info/METADATA +109 -0
  66. pyroll_core-2.0.0.dist-info/RECORD +68 -0
  67. pyroll_core-2.0.0.dist-info/WHEEL +4 -0
  68. pyroll_core-2.0.0.dist-info/licenses/LICENSE +11 -0
@@ -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,2 @@
1
+ from . import hookimpls
2
+ from .disk_element_unit import DiskElementUnit
@@ -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,3 @@
1
+ from .box import BoxGroove
2
+ from .constricted_box import ConstrictedBoxGroove
3
+ from .upset_box import UpsetBoxGroove, ConstrictedUpsetBoxGroove
@@ -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,3 @@
1
+ from .diamond import DiamondGroove
2
+ from .square import SquareGroove
3
+ from .gothic import GothicGroove
@@ -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