pyroll-core 3.1.13__tar.gz → 3.2.0b2__tar.gz

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 (103) hide show
  1. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/PKG-INFO +1 -1
  2. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/__init__.py +19 -6
  3. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/engine/hookimpls.py +0 -4
  4. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/__init__.py +10 -1
  5. pyroll_core-3.2.0b2/pyroll/core/grooves/angles/__init__.py +8 -0
  6. pyroll_core-3.2.0b2/pyroll/core/grooves/angles/_geometry.py +224 -0
  7. pyroll_core-3.2.0b2/pyroll/core/grooves/angles/_path.py +203 -0
  8. pyroll_core-3.2.0b2/pyroll/core/grooves/angles/butterfly_groove.py +544 -0
  9. pyroll_core-3.2.0b2/pyroll/core/grooves/angles/factories.py +67 -0
  10. pyroll_core-3.2.0b2/pyroll/core/grooves/generic_angle.py +129 -0
  11. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/hooks.py +22 -33
  12. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll/hookimpls.py +1 -2
  13. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/__init__.py +2 -0
  14. pyroll_core-3.2.0b2/pyroll/core/roll_pass/asymmetric_two_roll_pass.py +159 -0
  15. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/base.py +0 -22
  16. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/__init__.py +1 -0
  17. pyroll_core-3.2.0b2/pyroll/core/roll_pass/hookimpls/asymmetric_two_roll_pass.py +194 -0
  18. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/deformation_unit.py +1 -5
  19. pyroll_core-3.2.0b2/pyroll/core/roll_pass/hookimpls/engine.py +47 -0
  20. pyroll_core-3.2.0b2/pyroll/core/roll_pass/hookimpls/helpers.py +65 -0
  21. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/profile.py +9 -1
  22. pyroll_core-3.2.0b2/pyroll/core/roll_pass/hookimpls/roll.py +182 -0
  23. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/symmetric_roll_pass.py +18 -4
  24. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/sequence/sequence.py +46 -8
  25. pyroll_core-3.2.0b2/pyroll/core/transport/__init__.py +7 -0
  26. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/hookimpls/__init__.py +0 -2
  27. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/hookimpls/cooling_pipe.py +1 -2
  28. pyroll_core-3.2.0b2/pyroll/core/transport/hookimpls/spooler.py +68 -0
  29. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/hookimpls/transport.py +2 -10
  30. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/spooler.py +0 -21
  31. pyroll_core-3.1.13/pyroll/core/roll_pass/hookimpls/engine.py +0 -29
  32. pyroll_core-3.1.13/pyroll/core/roll_pass/hookimpls/helpers.py +0 -25
  33. pyroll_core-3.1.13/pyroll/core/roll_pass/hookimpls/roll.py +0 -75
  34. pyroll_core-3.1.13/pyroll/core/transport/__init__.py +0 -9
  35. pyroll_core-3.1.13/pyroll/core/transport/hookimpls/induction_furnace.py +0 -23
  36. pyroll_core-3.1.13/pyroll/core/transport/hookimpls/spooler.py +0 -170
  37. pyroll_core-3.1.13/pyroll/core/transport/hookimpls/stelmor_conveyor.py +0 -11
  38. pyroll_core-3.1.13/pyroll/core/transport/induction_furnace.py +0 -81
  39. pyroll_core-3.1.13/pyroll/core/transport/stelmor_conveyor.py +0 -71
  40. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/.gitignore +1 -1
  41. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/LICENSE +0 -0
  42. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/README.md +0 -0
  43. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/hatch.toml +0 -0
  44. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyproject.toml +0 -0
  45. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/config.py +0 -0
  46. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/disk_elements/__init__.py +0 -0
  47. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/disk_elements/disk_element_unit.py +0 -0
  48. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/disk_elements/hookimpls.py +0 -0
  49. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/engine/__init__.py +0 -0
  50. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/engine/engine.py +0 -0
  51. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/base.py +0 -0
  52. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/boxes/__init__.py +0 -0
  53. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/boxes/box.py +0 -0
  54. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/boxes/constricted_box.py +0 -0
  55. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/boxes/upset_box.py +0 -0
  56. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/diamonds/__init__.py +0 -0
  57. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/diamonds/diamond.py +0 -0
  58. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/diamonds/gothic.py +0 -0
  59. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/diamonds/square.py +0 -0
  60. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/equivalent_ripped_groove.py +0 -0
  61. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/flat.py +0 -0
  62. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/generic_elongation.py +0 -0
  63. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/generic_elongation_solvers.py +0 -0
  64. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/hexagonal.py +0 -0
  65. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/__init__.py +0 -0
  66. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/circular_oval.py +0 -0
  67. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/constricted_circular_oval.py +0 -0
  68. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/constricted_swedish_oval.py +0 -0
  69. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/flat_oval.py +0 -0
  70. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/oval_3radii.py +0 -0
  71. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/oval_3radii_flanked.py +0 -0
  72. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/swedish_oval.py +0 -0
  73. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/upset_oval.py +0 -0
  74. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/rounds/__init__.py +0 -0
  75. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/rounds/false_round.py +0 -0
  76. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/rounds/round.py +0 -0
  77. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/spline.py +0 -0
  78. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/log.py +0 -0
  79. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/profile/__init__.py +0 -0
  80. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/profile/hookimpls.py +0 -0
  81. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/profile/profile.py +0 -0
  82. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/repr.py +0 -0
  83. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll/__init__.py +0 -0
  84. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll/roll.py +0 -0
  85. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/deformation_unit.py +0 -0
  86. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/base_roll_pass.py +0 -0
  87. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/disk_element.py +0 -0
  88. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/symmetric_roll_pass.py +0 -0
  89. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/three_roll_pass.py +0 -0
  90. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/two_roll_pass.py +0 -0
  91. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/three_roll_pass.py +0 -0
  92. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/two_roll_pass.py +0 -0
  93. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/rotator/__init__.py +0 -0
  94. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/rotator/hookimpls.py +0 -0
  95. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/rotator/rotator.py +0 -0
  96. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/sequence/__init__.py +0 -0
  97. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/sequence/hookimpls.py +0 -0
  98. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/shapes.py +0 -0
  99. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/cooling_pipe.py +0 -0
  100. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/transport.py +0 -0
  101. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/unit/__init__.py +0 -0
  102. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/unit/hookimpls.py +0 -0
  103. {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/unit/unit.py +1 -1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyroll-core
3
- Version: 3.1.13
3
+ Version: 3.2.0b2
4
4
  Summary: PyRoll rolling simulation framework - core library.
5
5
  Project-URL: Homepage, https://pyroll-project.github.io
6
6
  Project-URL: Repository, https://github.com/pyroll-project/pyroll-core
@@ -24,10 +24,20 @@ from .grooves import (
24
24
  FlatGroove,
25
25
  HexagonalGroove,
26
26
  EquivalentRibbedGroove,
27
+ ButterflyGroove,
28
+ GroovePair,
29
+ butterfly_grooves,
27
30
  create_groove_by_type_name,
28
31
  )
29
- from .transport import Transport, CoolingPipe, Spooler, StelmorConveyor, InductionFurnace
30
- from .roll_pass import BaseRollPass, DeformationUnit, ThreeRollPass, SymmetricRollPass, TwoRollPass
32
+ from .transport import Transport, CoolingPipe, Spooler
33
+ from .roll_pass import (
34
+ BaseRollPass,
35
+ DeformationUnit,
36
+ ThreeRollPass,
37
+ SymmetricRollPass,
38
+ TwoRollPass,
39
+ AsymmetricTwoRollPass,
40
+ )
31
41
  from .roll_pass import TwoRollPass as RollPass
32
42
  from .unit import Unit
33
43
  from .roll import Roll
@@ -45,7 +55,7 @@ from .hooks import Hook, HookHost, HookFunction, root_hooks
45
55
  from .disk_elements import DiskElementUnit
46
56
  from .config import Config, config, PlottingBackend, ConfigValue, ConfigMeta
47
57
 
48
- VERSION = "3.1.13"
58
+ VERSION = "3.2.0b2"
49
59
 
50
60
  __all__ = [
51
61
  # grooves
@@ -72,6 +82,9 @@ __all__ = [
72
82
  "FlatGroove",
73
83
  "HexagonalGroove",
74
84
  "EquivalentRibbedGroove",
85
+ "ButterflyGroove",
86
+ "GroovePair",
87
+ "butterfly_grooves",
75
88
  "create_groove_by_type_name",
76
89
  # profile
77
90
  "Profile",
@@ -85,8 +98,6 @@ __all__ = [
85
98
  "Transport",
86
99
  "CoolingPipe",
87
100
  "Spooler",
88
- "StelmorConveyor",
89
- "InductionFurnace",
90
101
  # roll_pass
91
102
  "RollPass",
92
103
  "BaseRollPass",
@@ -118,8 +129,10 @@ __all__ = [
118
129
 
119
130
  root_hooks.extend(
120
131
  [
132
+ AsymmetricTwoRollPass.InProfile.cross_section,
121
133
  BaseRollPass.roll_force,
122
- BaseRollPass.Roll.roll_torque,
134
+ SymmetricRollPass.Roll.roll_torque,
135
+ AsymmetricTwoRollPass.Roll.roll_torque,
123
136
  BaseRollPass.Engine.power,
124
137
  BaseRollPass.elongation_efficiency,
125
138
  Unit.power,
@@ -3,10 +3,6 @@ import numpy as np
3
3
  from .engine import Engine
4
4
  from ..transport import Transport
5
5
 
6
- @Engine.gear_ratio
7
- def default_gear_ratio(self: Engine):
8
- return 1
9
-
10
6
  @Engine.gear_box_efficiency
11
7
  def gear_box_efficiency_becker(self: Engine):
12
8
  return 0.95
@@ -2,11 +2,16 @@ from .base import GrooveBase
2
2
 
3
3
  from .spline import SplineGroove
4
4
  from .generic_elongation import GenericElongationGroove
5
+ from .generic_angle import GenericAngleGroove
5
6
 
6
7
  from .boxes import BoxGroove, ConstrictedBoxGroove, UpsetBoxGroove, ConstrictedUpsetBoxGroove
7
8
 
8
9
  from .diamonds import DiamondGroove, SquareGroove, GothicGroove
9
-
10
+ from .angles import (
11
+ ButterflyGroove,
12
+ GroovePair,
13
+ butterfly_grooves,
14
+ )
10
15
  from .ovals import (
11
16
  CircularOvalGroove,
12
17
  FlatOvalGroove,
@@ -50,6 +55,10 @@ __all__ = [
50
55
  "FlatGroove",
51
56
  "HexagonalGroove",
52
57
  "EquivalentRibbedGroove",
58
+ "ButterflyGroove",
59
+ "GroovePair",
60
+ "butterfly_grooves",
61
+ "GenericAngleGroove",
53
62
  "create_groove_by_type_name",
54
63
  ]
55
64
 
@@ -0,0 +1,8 @@
1
+ from .butterfly_groove import ButterflyGroove
2
+ from .factories import GroovePair, butterfly_grooves
3
+
4
+ __all__ = [
5
+ "ButterflyGroove",
6
+ "GroovePair",
7
+ "butterfly_grooves",
8
+ ]
@@ -0,0 +1,224 @@
1
+ from typing import Tuple
2
+
3
+ import numpy as np
4
+
5
+ __all__ = [
6
+ "line_through",
7
+ "line_point_angle",
8
+ "horizontal_line",
9
+ "vertical_line",
10
+ "intersect",
11
+ "offset_toward",
12
+ "perpendicular_foot",
13
+ "fillet_line_line",
14
+ "fillet_line_circle",
15
+ "line_circle_intersection",
16
+ "circle_circle_intersection",
17
+ "arc_points",
18
+ "Circle",
19
+ ]
20
+
21
+ Line = np.ndarray
22
+ Point = np.ndarray
23
+
24
+ Circle = Tuple[float, float, float]
25
+ """Circle as tuple ``(center_z, center_y, radius)``."""
26
+
27
+
28
+ def line_through(p1, p2) -> Line:
29
+ """Line through two points (Matlab ``gpp``)."""
30
+ p1 = np.asarray(p1, dtype=float)
31
+ p2 = np.asarray(p2, dtype=float)
32
+ a = p2[1] - p1[1]
33
+ b = p1[0] - p2[0]
34
+ c = -a * p1[0] - b * p1[1]
35
+ return np.array([a, b, c])
36
+
37
+
38
+ def line_point_angle(p, angle: float) -> Line:
39
+ """Line through a point with given angle measured from the vertical axis.
40
+
41
+ :param angle: angle to the y-axis in radians
42
+ """
43
+ p = np.asarray(p, dtype=float)
44
+ p2 = p + np.array([-np.sin(angle), np.cos(angle)])
45
+ return line_through(p, p2)
46
+
47
+
48
+ def horizontal_line(y: float) -> Line:
49
+ """Horizontal line at given y."""
50
+ return np.array([0.0, 1.0, -float(y)])
51
+
52
+
53
+ def vertical_line(z: float) -> Line:
54
+ """Vertical line at given z."""
55
+ return np.array([1.0, 0.0, -float(z)])
56
+
57
+
58
+ def intersect(g1: Line, g2: Line) -> Point:
59
+ """Intersection point of two lines."""
60
+ d = g1[0] * g2[1] - g2[0] * g1[1]
61
+ if np.isclose(d, 0):
62
+ raise ValueError("Lines are parallel, no intersection exists.")
63
+ z = (g2[2] * g1[1] - g1[2] * g2[1]) / d
64
+ y = (g1[2] * g2[0] - g2[2] * g1[0]) / d
65
+ return np.array([z, y])
66
+
67
+
68
+ def _normalized(g: Line) -> Line:
69
+ n = np.hypot(g[0], g[1])
70
+ return g / n
71
+
72
+
73
+ def signed_distance(g: Line, p) -> float:
74
+ """Signed distance of a point from a line."""
75
+ g = _normalized(g)
76
+ p = np.asarray(p, dtype=float)
77
+ return g[0] * p[0] + g[1] * p[1] + g[2]
78
+
79
+
80
+ def offset_toward(g: Line, dist: float, toward) -> Line:
81
+ """Parallel line at given distance, shifted towards the hint point."""
82
+ g = _normalized(g)
83
+ side = np.sign(signed_distance(g, toward))
84
+ if side == 0:
85
+ raise ValueError("Hint point lies on the line, offset side is ambiguous.")
86
+ return np.array([g[0], g[1], g[2] - side * dist])
87
+
88
+
89
+ def perpendicular_foot(g: Line, p) -> Point:
90
+ """Foot of the perpendicular from a point onto a line."""
91
+ g = _normalized(g)
92
+ p = np.asarray(p, dtype=float)
93
+ return p - signed_distance(g, p) * g[:2]
94
+
95
+
96
+ def fillet_line_line(g1: Line, g2: Line, radius: float, toward, toward2=None) -> Tuple[Point, Point, Point]:
97
+ """
98
+ Fillet circle of given radius tangent to two lines.
99
+
100
+ :param toward: hint point on the side of ``g1`` where the fillet center lies
101
+ :param toward2: hint point on the side of ``g2`` where the fillet center lies, defaults to ``toward``
102
+ :return: tuple of (center, tangent point on g1, tangent point on g2)
103
+ """
104
+ if toward2 is None:
105
+ toward2 = toward
106
+ center = intersect(offset_toward(g1, radius, toward), offset_toward(g2, radius, toward2))
107
+ return center, perpendicular_foot(g1, center), perpendicular_foot(g2, center)
108
+
109
+
110
+ def fillet_corner(g1: Line, g2: Line, radius: float, along1, along2) -> Tuple[Point, Point, Point]:
111
+ """
112
+ Fillet circle smoothing the corner between two lines, selecting the branch by the
113
+ retained line segments: the fillet lies inside the wedge spanned by the half-lines
114
+ from the corner towards ``along1`` and ``along2``.
115
+
116
+ :param along1: point on (or towards) the retained part of ``g1``
117
+ :param along2: point on (or towards) the retained part of ``g2``
118
+ :return: tuple of (center, tangent point on g1, tangent point on g2)
119
+ """
120
+ corner = intersect(g1, g2)
121
+
122
+ d1 = np.asarray(along1, dtype=float) - corner
123
+ d2 = np.asarray(along2, dtype=float) - corner
124
+ d1 = d1 / np.hypot(*d1)
125
+ d2 = d2 / np.hypot(*d2)
126
+
127
+ bisector = d1 + d2
128
+ norm = np.hypot(*bisector)
129
+ if np.isclose(norm, 0):
130
+ raise ValueError("Retained segments are opposite, fillet side is ambiguous.")
131
+
132
+ hint = corner + bisector / norm
133
+ return fillet_line_line(g1, g2, radius, toward=hint)
134
+
135
+
136
+ def line_circle_intersection(g: Line, k: Circle, toward) -> Point:
137
+ """Intersection of a line and a circle nearest to the hint point."""
138
+ g = _normalized(g)
139
+ m = np.array([k[0], k[1]])
140
+ r = k[2]
141
+
142
+ d = signed_distance(g, m)
143
+ if abs(d) > r:
144
+ raise ValueError("Line and circle do not intersect.")
145
+
146
+ foot_point = m - d * g[:2]
147
+ half_chord = np.sqrt(max(r**2 - d**2, 0))
148
+ direction = np.array([-g[1], g[0]])
149
+
150
+ candidates = [foot_point + half_chord * direction, foot_point - half_chord * direction]
151
+ toward = np.asarray(toward, dtype=float)
152
+ return min(candidates, key=lambda p: np.hypot(*(p - toward)))
153
+
154
+
155
+ def circle_circle_intersection(k1: Circle, k2: Circle, toward) -> Point:
156
+ """Intersection of two circles nearest to the hint point; tangency point if they just touch."""
157
+ m1 = np.array([k1[0], k1[1]])
158
+ m2 = np.array([k2[0], k2[1]])
159
+ d = np.hypot(*(m2 - m1))
160
+
161
+ if d > k1[2] + k2[2] and not np.isclose(d, k1[2] + k2[2]):
162
+ raise ValueError("Circles do not intersect.")
163
+ if d < abs(k1[2] - k2[2]) and not np.isclose(d, abs(k1[2] - k2[2])):
164
+ raise ValueError("One circle lies inside the other, no intersection exists.")
165
+
166
+ a = (k1[2] ** 2 - k2[2] ** 2 + d**2) / (2 * d)
167
+ h = np.sqrt(max(k1[2] ** 2 - a**2, 0))
168
+
169
+ direction = (m2 - m1) / d
170
+ base = m1 + a * direction
171
+ perp = np.array([-direction[1], direction[0]])
172
+
173
+ candidates = [base + h * perp, base - h * perp]
174
+ toward = np.asarray(toward, dtype=float)
175
+ return min(candidates, key=lambda p: np.hypot(*(p - toward)))
176
+
177
+
178
+ def fillet_line_circle(
179
+ g: Line, k: Circle, radius: float, toward, externally: bool = True
180
+ ) -> Tuple[Point, Point, Point]:
181
+ """
182
+ Fillet circle of given radius tangent to a line and a circle.
183
+
184
+ :param toward: hint point selecting the offset side of the line and the solution branch
185
+ :param externally: whether the fillet touches the circle from outside (default) or inside
186
+ :return: tuple of (center, tangent point on the line, tangent point on the circle)
187
+ """
188
+ g_offset = offset_toward(g, radius, toward)
189
+ expanded = (k[0], k[1], k[2] + radius if externally else k[2] - radius)
190
+ center = line_circle_intersection(g_offset, expanded, toward)
191
+
192
+ tangent_line = perpendicular_foot(g, center)
193
+
194
+ circle_center = np.array([k[0], k[1]])
195
+ direction = center - circle_center
196
+ direction = direction / np.hypot(*direction)
197
+ tangent_circle = circle_center + k[2] * direction
198
+
199
+ return center, tangent_line, tangent_circle
200
+
201
+
202
+ def arc_points(center, start, end, count: int) -> np.ndarray:
203
+ """
204
+ Discretize the circular arc from start to end point around center,
205
+ always sweeping the smaller angle.
206
+
207
+ :return: array of shape (count, 2) including start and end point
208
+ """
209
+ center = np.asarray(center, dtype=float)
210
+ v1 = np.asarray(start, dtype=float) - center
211
+ v2 = np.asarray(end, dtype=float) - center
212
+
213
+ radius = np.hypot(*v1)
214
+ phi1 = np.arctan2(v1[1], v1[0])
215
+ phi2 = np.arctan2(v2[1], v2[0])
216
+
217
+ sweep = phi2 - phi1
218
+ if sweep > np.pi:
219
+ sweep -= 2 * np.pi
220
+ elif sweep < -np.pi:
221
+ sweep += 2 * np.pi
222
+
223
+ phi = phi1 + np.linspace(0, sweep, count)
224
+ return center + radius * np.stack([np.cos(phi), np.sin(phi)], axis=1)
@@ -0,0 +1,203 @@
1
+ from typing import Sequence, Union
2
+
3
+ import numpy as np
4
+
5
+ from . import _geometry as geo
6
+ from ...config import Config
7
+
8
+ __all__ = ["chain_contour", "build_filleted_contour"]
9
+
10
+ Carrier = Union[np.ndarray, geo.Circle]
11
+
12
+
13
+ def _is_line(carrier: Carrier) -> bool:
14
+ return isinstance(carrier, np.ndarray)
15
+
16
+
17
+ def chain_contour(start, steps: Sequence[tuple]) -> np.ndarray:
18
+ """
19
+ Assemble a contour point array from explicit steps.
20
+
21
+ :param start: starting point of the contour
22
+ :param steps: sequence of steps, each either ``("line", point)`` for a straight
23
+ segment to ``point`` or ``("arc", center, point)`` for a circular arc around
24
+ ``center`` to ``point`` (sweeping the smaller angle)
25
+ :return: array of shape (n, 2)
26
+ """
27
+ n = Config.GROOVE_RADIUS_POINT_COUNT
28
+ current = np.asarray(start, dtype=float)
29
+ parts = [current.reshape(1, 2)]
30
+
31
+ for step in steps:
32
+ if step[0] == "line":
33
+ target = np.asarray(step[1], dtype=float)
34
+ parts.append(target.reshape(1, 2))
35
+ elif step[0] == "arc":
36
+ center, target = step[1], np.asarray(step[2], dtype=float)
37
+ arc = geo.arc_points(center, current, target, n)[1:]
38
+ arc[-1] = target # snap the recomputed arc end onto the exact target
39
+ parts.append(arc)
40
+ else:
41
+ raise ValueError(f"Unknown contour step type: {step[0]!r}")
42
+ current = target
43
+
44
+ return np.vstack(parts)
45
+
46
+
47
+ def _nominal_vertex(c1: Carrier, c2: Carrier, toward) -> np.ndarray:
48
+ """Nominal junction point of two carriers: intersection resp. tangency nearest the hint."""
49
+ if _is_line(c1) and _is_line(c2):
50
+ return geo.intersect(c1, c2)
51
+
52
+ if _is_line(c1) or _is_line(c2):
53
+ line, circle = (c1, c2) if _is_line(c1) else (c2, c1)
54
+ try:
55
+ return geo.line_circle_intersection(line, circle, toward)
56
+ except ValueError:
57
+ center = np.array([circle[0], circle[1]])
58
+ foot = geo.perpendicular_foot(line, center)
59
+ direction = foot - center
60
+ direction = direction / np.hypot(*direction)
61
+ return (foot + center + circle[2] * direction) / 2
62
+
63
+ m1 = np.array([c1[0], c1[1]])
64
+ m2 = np.array([c2[0], c2[1]])
65
+ direction = m2 - m1
66
+ direction = direction / np.hypot(*direction)
67
+ return m1 + c1[2] * direction
68
+
69
+
70
+ def _arc_midpoint(circle: geo.Circle, p1, p2) -> np.ndarray:
71
+ """Midpoint of the smaller arc of the circle between the projections of two points onto it."""
72
+ center = np.array([circle[0], circle[1]])
73
+ phi = []
74
+ for p in (p1, p2):
75
+ d = np.asarray(p, dtype=float) - center
76
+ phi.append(np.arctan2(d[1], d[0]))
77
+
78
+ sweep = phi[1] - phi[0]
79
+ if sweep > np.pi:
80
+ sweep -= 2 * np.pi
81
+ elif sweep < -np.pi:
82
+ sweep += 2 * np.pi
83
+
84
+ mid = phi[0] + sweep / 2
85
+ return center + circle[2] * np.array([np.cos(mid), np.sin(mid)])
86
+
87
+
88
+ def _fillet_line_circle_auto(
89
+ line: np.ndarray, circle: geo.Circle, radius: float, vertex, line_ref, circle_ref, hint=None
90
+ ):
91
+ """
92
+ Fillet between a line and a circle with automatic branch selection.
93
+
94
+ The offset side hint is taken along the bisector between the retained line direction
95
+ and the direction to the midpoint of the retained circle arc (or from the explicit
96
+ ``hint`` point); the internal/external branch by the solution whose tangent points
97
+ lie nearest to the nominal junction vertex.
98
+ """
99
+ if hint is None:
100
+ d1 = np.asarray(line_ref, dtype=float) - vertex
101
+ d2 = _arc_midpoint(circle, vertex, circle_ref) - vertex
102
+ d1 = d1 / np.hypot(*d1)
103
+ norm2 = np.hypot(*d2)
104
+ if np.isclose(norm2, 0):
105
+ # junction vertex coincides with the retained arc midpoint: tangent line and circle,
106
+ # the fillet side degenerates to the circle side of the line
107
+ d2 = np.array([circle[0], circle[1]]) - vertex
108
+ norm2 = np.hypot(*d2)
109
+ d2 = d2 / norm2
110
+ bisector = d1 + d2
111
+ norm = np.hypot(*bisector)
112
+ if np.isclose(norm, 0):
113
+ raise ValueError("Retained segments are opposite, fillet side is ambiguous.")
114
+ hint = vertex + bisector / norm * radius
115
+
116
+ solutions = []
117
+ for externally in (True, False):
118
+ try:
119
+ center, t_line, t_circle = geo.fillet_line_circle(line, circle, radius, toward=hint, externally=externally)
120
+ except ValueError:
121
+ continue
122
+ score = np.hypot(*(t_line - vertex)) + np.hypot(*(t_circle - vertex))
123
+ solutions.append((score, center, t_line, t_circle))
124
+
125
+ if not solutions:
126
+ raise ValueError("No fillet between line and circle exists for the given radius.")
127
+
128
+ _, center, t_line, t_circle = min(solutions, key=lambda s: s[0])
129
+ return center, t_line, t_circle
130
+
131
+
132
+ def build_filleted_contour(start, path: Sequence, end, hints: dict = None) -> np.ndarray:
133
+ """
134
+ Build a contour from a chain of carriers with transition radii at the junctions.
135
+
136
+ :param start: starting point, lying on the first carrier
137
+ :param path: flat alternating sequence ``carrier, radius, carrier, radius, ..., carrier``;
138
+ lines as ``numpy`` arrays (see :py:mod:`._geometry`), circles as
139
+ ``(center_z, center_y, radius)`` tuples; a radius of 0 gives a sharp corner
140
+ (line-line) resp. a direct tangent junction (with a circle carrier)
141
+ :param end: end point, lying on the last carrier
142
+ :param hints: optional explicit fillet side hint points by junction index (0-based,
143
+ counting the radii in ``path``), overriding the automatic side selection for
144
+ geometrically ambiguous junctions
145
+ :return: array of shape (n, 2)
146
+ """
147
+ hints = hints or {}
148
+ carriers = list(path[0::2])
149
+ radii = list(path[1::2])
150
+ if len(carriers) != len(radii) + 1:
151
+ raise ValueError("Path must be an alternating sequence carrier, radius, ..., carrier.")
152
+
153
+ start = np.asarray(start, dtype=float)
154
+ end = np.asarray(end, dtype=float)
155
+
156
+ vertices = []
157
+ ref = start
158
+ for c1, c2 in zip(carriers[:-1], carriers[1:]):
159
+ if not _is_line(c1):
160
+ ref = 2 * np.array([c1[0], c1[1]]) - ref
161
+ ref = _nominal_vertex(c1, c2, ref)
162
+ vertices.append(ref)
163
+
164
+ ref_points = [start, *vertices, end]
165
+
166
+ steps = []
167
+ for i, radius in enumerate(radii):
168
+ c1, c2 = carriers[i], carriers[i + 1]
169
+ vertex = vertices[i]
170
+ prev_ref, next_ref = ref_points[i], ref_points[i + 2]
171
+
172
+ if radius == 0:
173
+ junction_steps = [("line", vertex)] if _is_line(c1) else [("arc", (c1[0], c1[1]), vertex)]
174
+ steps.append((junction_steps, vertex))
175
+ continue
176
+
177
+ hint = hints.get(i)
178
+
179
+ if _is_line(c1) and _is_line(c2):
180
+ if hint is not None:
181
+ center, t1, t2 = geo.fillet_line_line(c1, c2, radius, toward=hint)
182
+ else:
183
+ center, t1, t2 = geo.fillet_corner(c1, c2, radius, along1=prev_ref, along2=next_ref)
184
+ elif _is_line(c1) or _is_line(c2):
185
+ if _is_line(c1):
186
+ line, circle, line_ref, circle_ref = c1, c2, prev_ref, next_ref
187
+ else:
188
+ line, circle, line_ref, circle_ref = c2, c1, next_ref, prev_ref
189
+ center, t_line, t_circle = _fillet_line_circle_auto(
190
+ line, circle, radius, vertex, line_ref, circle_ref, hint=hint
191
+ )
192
+ t1, t2 = (t_line, t_circle) if _is_line(c1) else (t_circle, t_line)
193
+ else:
194
+ raise NotImplementedError("Fillets between two circle carriers are not supported.")
195
+
196
+ approach = [("line", t1)] if _is_line(c1) else [("arc", (c1[0], c1[1]), t1)]
197
+ steps.append((approach + [("arc", center, t2)], t2))
198
+
199
+ last = carriers[-1]
200
+ final_step = ("line", end) if _is_line(last) else ("arc", (last[0], last[1]), end)
201
+
202
+ all_steps = [s for junction_steps, _ in steps for s in junction_steps] + [final_step]
203
+ return chain_contour(start, all_steps)