FiberFusing 1.5.0.post0__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.
Potentially problematic release.
This version of FiberFusing might be problematic. Click here for more details.
- FiberFusing/__init__.py +15 -0
- FiberFusing/_version.py +16 -0
- FiberFusing/background.py +92 -0
- FiberFusing/base_fused.py +520 -0
- FiberFusing/buffer.py +163 -0
- FiberFusing/components/__init__.py +0 -0
- FiberFusing/components/base_class.py +442 -0
- FiberFusing/components/linestring.py +106 -0
- FiberFusing/components/point.py +91 -0
- FiberFusing/components/polygon.py +228 -0
- FiberFusing/components/utils.py +59 -0
- FiberFusing/configuration/__init__.py +7 -0
- FiberFusing/configuration/line/__init__.py +10 -0
- FiberFusing/configuration/line/configuration_01x01.py +14 -0
- FiberFusing/configuration/line/configuration_02x02.py +18 -0
- FiberFusing/configuration/line/configuration_03x03.py +18 -0
- FiberFusing/configuration/line/configuration_04x04.py +18 -0
- FiberFusing/configuration/line/configuration_05x05.py +18 -0
- FiberFusing/configuration/ring/__init__.py +15 -0
- FiberFusing/configuration/ring/configuration_01x01.py +14 -0
- FiberFusing/configuration/ring/configuration_02x02.py +18 -0
- FiberFusing/configuration/ring/configuration_03x03.py +18 -0
- FiberFusing/configuration/ring/configuration_04x04.py +18 -0
- FiberFusing/configuration/ring/configuration_05x05.py +18 -0
- FiberFusing/configuration/ring/configuration_06x06.py +18 -0
- FiberFusing/configuration/ring/configuration_07x07.py +20 -0
- FiberFusing/configuration/ring/configuration_10x10.py +31 -0
- FiberFusing/configuration/ring/configuration_12x12.py +30 -0
- FiberFusing/configuration/ring/configuration_19x19.py +34 -0
- FiberFusing/connection.py +366 -0
- FiberFusing/connection_optimization.py +246 -0
- FiberFusing/coordinate_system.py +256 -0
- FiberFusing/directories.py +34 -0
- FiberFusing/fiber/__init__.py +0 -0
- FiberFusing/fiber/catalogue.py +165 -0
- FiberFusing/fiber/fiber_files/1550BHP.yaml +14 -0
- FiberFusing/fiber/fiber_files/DCF13.yaml +18 -0
- FiberFusing/fiber/fiber_files/DCF1300S_20.yaml +17 -0
- FiberFusing/fiber/fiber_files/DCF1300S_26.yaml +18 -0
- FiberFusing/fiber/fiber_files/DCF1300S_33.yaml +17 -0
- FiberFusing/fiber/fiber_files/DCF1300S_42.yaml +18 -0
- FiberFusing/fiber/fiber_files/F2028M12.yaml +18 -0
- FiberFusing/fiber/fiber_files/F2028M21.yaml +18 -0
- FiberFusing/fiber/fiber_files/F2028M24.yaml +18 -0
- FiberFusing/fiber/fiber_files/F2058G1.yaml +18 -0
- FiberFusing/fiber/fiber_files/F2058L1.yaml +18 -0
- FiberFusing/fiber/fiber_files/HI1060.yaml +14 -0
- FiberFusing/fiber/fiber_files/HP630.yaml +13 -0
- FiberFusing/fiber/fiber_files/SM1950.yaml +14 -0
- FiberFusing/fiber/fiber_files/SMF28.yaml +14 -0
- FiberFusing/fiber/fiber_files/fluorine_doped_1%_capillary.yaml +9 -0
- FiberFusing/fiber/fiber_files/fluorine_doped_2%_capillary.yaml +9 -0
- FiberFusing/fiber/fiber_files/test_fiber.yaml +14 -0
- FiberFusing/fiber/fiber_files/test_multimode_fiber.yaml +14 -0
- FiberFusing/fiber/generic_fiber.py +413 -0
- FiberFusing/fiber/loader.py +120 -0
- FiberFusing/fiber/structure_collection.py +173 -0
- FiberFusing/fiber_structure.py +210 -0
- FiberFusing/geometry.py +458 -0
- FiberFusing/helper.py +59 -0
- FiberFusing/optical_structure.py +96 -0
- FiberFusing/plottings.py +43 -0
- FiberFusing/utils.py +108 -0
- FiberFusing-1.5.0.0.post0.dist-info/LICENSE +21 -0
- FiberFusing-1.5.0.0.post0.dist-info/METADATA +193 -0
- FiberFusing-1.5.0.0.post0.dist-info/RECORD +68 -0
- FiberFusing-1.5.0.0.post0.dist-info/WHEEL +5 -0
- FiberFusing-1.5.0.0.post0.dist-info/top_level.txt +1 -0
FiberFusing/buffer.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from typing import Tuple, Optional
|
|
5
|
+
from pydantic.dataclasses import dataclass
|
|
6
|
+
from pydantic import ConfigDict
|
|
7
|
+
import shapely.geometry as geo
|
|
8
|
+
from shapely import affinity
|
|
9
|
+
|
|
10
|
+
from FiberFusing.components.utils import interpret_to_point
|
|
11
|
+
from FiberFusing.components.polygon import Polygon
|
|
12
|
+
|
|
13
|
+
config_dict = ConfigDict(extra='forbid', strict=True, kw_only=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(config=config_dict)
|
|
17
|
+
class Circle(Polygon):
|
|
18
|
+
"""
|
|
19
|
+
Represents a circular geometry centered at a specified position with a given radius.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
position : Tuple[float, float]
|
|
24
|
+
The center position of the circle.
|
|
25
|
+
radius : float
|
|
26
|
+
The radius of the circle.
|
|
27
|
+
resolution : Optional[int], optional
|
|
28
|
+
The resolution of the circle's edge. Higher values yield smoother circles. Default is 128.
|
|
29
|
+
index : Optional[float], optional
|
|
30
|
+
The refractive index of the circle. Default is None.
|
|
31
|
+
"""
|
|
32
|
+
position: Tuple[float, float]
|
|
33
|
+
radius: float
|
|
34
|
+
resolution: Optional[int] = 128
|
|
35
|
+
index: Optional[float] = None
|
|
36
|
+
|
|
37
|
+
def __post_init__(self):
|
|
38
|
+
"""
|
|
39
|
+
Initialize the circular geometry based on the specified position and radius.
|
|
40
|
+
|
|
41
|
+
Notes
|
|
42
|
+
-----
|
|
43
|
+
- The geometry is created using a buffer method applied to the position.
|
|
44
|
+
- The position is interpreted and converted to a `shapely` point.
|
|
45
|
+
"""
|
|
46
|
+
self.position = interpret_to_point(self.position)
|
|
47
|
+
|
|
48
|
+
# Validate radius
|
|
49
|
+
if self.radius <= 0:
|
|
50
|
+
raise ValueError("Radius must be a positive value.")
|
|
51
|
+
|
|
52
|
+
# Create a circular geometry using buffer
|
|
53
|
+
circle = self.position._shapely_object.buffer(
|
|
54
|
+
self.radius,
|
|
55
|
+
resolution=self.resolution
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self._shapely_object = circle
|
|
59
|
+
self.core = self.center.copy()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(config=config_dict)
|
|
63
|
+
class Ellipse(Polygon):
|
|
64
|
+
"""
|
|
65
|
+
Represents an elliptical geometry centered at a specified position with a given radius and axis ratio.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
position : tuple
|
|
70
|
+
The center position of the ellipse.
|
|
71
|
+
radius : float
|
|
72
|
+
The radius along the major axis.
|
|
73
|
+
resolution : Optional[int], optional
|
|
74
|
+
The resolution of the ellipse's edge. Higher values yield smoother edges. Default is 128.
|
|
75
|
+
index : Optional[float], optional
|
|
76
|
+
The refractive index of the ellipse. Default is None.
|
|
77
|
+
ratio : Optional[float], optional
|
|
78
|
+
The ratio between the minor and major axes. Default is 1 (circular shape).
|
|
79
|
+
"""
|
|
80
|
+
position: tuple
|
|
81
|
+
radius: float
|
|
82
|
+
resolution: Optional[int] = 128
|
|
83
|
+
index: Optional[float] = None
|
|
84
|
+
ratio: Optional[float] = 1 # Default ratio of 1 makes this a circle
|
|
85
|
+
|
|
86
|
+
def __post_init__(self):
|
|
87
|
+
"""
|
|
88
|
+
Initialize the elliptical geometry based on the specified position, radius, and ratio.
|
|
89
|
+
|
|
90
|
+
Notes
|
|
91
|
+
-----
|
|
92
|
+
- The ellipse is scaled using the specified ratio after creating a circular base shape.
|
|
93
|
+
- The position is interpreted and converted to a `shapely` point.
|
|
94
|
+
"""
|
|
95
|
+
self.position = interpret_to_point(self.position)
|
|
96
|
+
|
|
97
|
+
# Validate radius and ratio
|
|
98
|
+
if self.radius <= 0:
|
|
99
|
+
raise ValueError("Radius must be a positive value.")
|
|
100
|
+
if self.ratio <= 0:
|
|
101
|
+
raise ValueError("Ratio must be a positive value.")
|
|
102
|
+
|
|
103
|
+
ellipse = self.position._shapely_object.buffer(
|
|
104
|
+
self.radius,
|
|
105
|
+
resolution=self.resolution
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Scale the ellipse according to the ratio
|
|
109
|
+
ellipse = affinity.scale(
|
|
110
|
+
ellipse,
|
|
111
|
+
xfact=1,
|
|
112
|
+
yfact=self.ratio,
|
|
113
|
+
origin=(0, 0)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
super().__init__(instance=ellipse)
|
|
117
|
+
self.core = self.center.copy()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(config=config_dict)
|
|
121
|
+
class Square(Polygon):
|
|
122
|
+
"""
|
|
123
|
+
Represents a square geometry centered at a specified position with a given side length.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
position : tuple
|
|
128
|
+
The center position of the square.
|
|
129
|
+
length : float
|
|
130
|
+
The length of the square's sides.
|
|
131
|
+
index : Optional[float], optional
|
|
132
|
+
The refractive index of the square. Default is None.
|
|
133
|
+
"""
|
|
134
|
+
position: tuple
|
|
135
|
+
length: float
|
|
136
|
+
index: Optional[float] = None
|
|
137
|
+
|
|
138
|
+
def __post_init__(self):
|
|
139
|
+
"""
|
|
140
|
+
Initialize the square geometry based on the specified position and side length.
|
|
141
|
+
|
|
142
|
+
Notes
|
|
143
|
+
-----
|
|
144
|
+
- The square is created using `shapely.geometry.box` method.
|
|
145
|
+
- The position is interpreted and converted to a `shapely` point.
|
|
146
|
+
"""
|
|
147
|
+
self.position = interpret_to_point(self.position)
|
|
148
|
+
|
|
149
|
+
# Validate length
|
|
150
|
+
if self.length <= 0:
|
|
151
|
+
raise ValueError("Length must be a positive value.")
|
|
152
|
+
|
|
153
|
+
square = geo.box(
|
|
154
|
+
self.position.x - self.length / 2,
|
|
155
|
+
self.position.y - self.length / 2,
|
|
156
|
+
self.position.x + self.length / 2,
|
|
157
|
+
self.position.y + self.length / 2
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
super().__init__(instance=square)
|
|
161
|
+
self.core = self.center.copy()
|
|
162
|
+
|
|
163
|
+
# -
|
|
File without changes
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
import numpy as np
|
|
6
|
+
from typing import Iterable, Tuple, Callable
|
|
7
|
+
from shapely import affinity
|
|
8
|
+
import shapely.geometry as geo
|
|
9
|
+
from FiberFusing.coordinate_system import CoordinateSystem
|
|
10
|
+
from shapely.ops import split
|
|
11
|
+
import FiberFusing as ff
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Alteration:
|
|
15
|
+
def in_place_copy(function: Callable) -> Callable:
|
|
16
|
+
"""
|
|
17
|
+
Decorator to manage in-place operations.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
function : Callable
|
|
22
|
+
The function to wrap.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
Callable
|
|
27
|
+
Wrapped function that optionally modifies the object in-place.
|
|
28
|
+
"""
|
|
29
|
+
def wrapper(self, *args, in_place=False, **kwargs):
|
|
30
|
+
instance = self if in_place else self.copy()
|
|
31
|
+
return function(self, instance, *args, **kwargs)
|
|
32
|
+
|
|
33
|
+
return wrapper
|
|
34
|
+
|
|
35
|
+
def copy(self) -> 'Alteration':
|
|
36
|
+
"""
|
|
37
|
+
Create a deep copy of the object.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
Alteration
|
|
42
|
+
A new instance that is a deep copy of the current object.
|
|
43
|
+
"""
|
|
44
|
+
return copy.deepcopy(self)
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Return the string representation of the shapely object.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
str
|
|
53
|
+
The string representation of the shapely object.
|
|
54
|
+
"""
|
|
55
|
+
return repr(self._shapely_object)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_empty(self) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Check if the shapely object is empty.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
bool
|
|
65
|
+
True if the object is empty, False otherwise.
|
|
66
|
+
"""
|
|
67
|
+
return self._shapely_object.is_empty
|
|
68
|
+
|
|
69
|
+
@in_place_copy
|
|
70
|
+
def union(self, output, *others) -> 'Alteration':
|
|
71
|
+
"""
|
|
72
|
+
Perform the union operation with other geometries.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
output : Alteration
|
|
77
|
+
The object to store the result.
|
|
78
|
+
*others : Alteration
|
|
79
|
+
Other geometries to union with.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
Alteration
|
|
84
|
+
The union of the geometries.
|
|
85
|
+
"""
|
|
86
|
+
other_shapes = (o._shapely_object for o in others)
|
|
87
|
+
output._shapely_object = self._shapely_object.union(*other_shapes)
|
|
88
|
+
return output
|
|
89
|
+
|
|
90
|
+
@in_place_copy
|
|
91
|
+
def intersection(self, output, *others) -> 'Alteration':
|
|
92
|
+
"""
|
|
93
|
+
Perform the intersection operation with other geometries.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
output : Alteration
|
|
98
|
+
The object to store the result.
|
|
99
|
+
*others : Alteration
|
|
100
|
+
Other geometries to intersect with.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
Alteration
|
|
105
|
+
The intersection of the geometries.
|
|
106
|
+
"""
|
|
107
|
+
other_shapes = (o._shapely_object for o in others)
|
|
108
|
+
output._shapely_object = self._shapely_object.intersection(*other_shapes)
|
|
109
|
+
return output
|
|
110
|
+
|
|
111
|
+
@in_place_copy
|
|
112
|
+
def scale(self, output, factor: float, origin: Tuple[float, float] = (0, 0)) -> 'Alteration':
|
|
113
|
+
"""
|
|
114
|
+
Scale the geometry by a given factor.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
output : Alteration
|
|
119
|
+
The object to store the result.
|
|
120
|
+
factor : float
|
|
121
|
+
The scaling factor.
|
|
122
|
+
origin : tuple of float, optional
|
|
123
|
+
The origin point for scaling, by default (0, 0).
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
Alteration
|
|
128
|
+
The scaled geometry.
|
|
129
|
+
"""
|
|
130
|
+
origin_point = ff.components.utils.interpret_to_point(origin)
|
|
131
|
+
output._shapely_object = affinity.scale(self._shapely_object, xfact=factor, yfact=factor, origin=origin_point._shapely_object)
|
|
132
|
+
return output
|
|
133
|
+
|
|
134
|
+
@in_place_copy
|
|
135
|
+
def translate(self, output, shift: Tuple[float, float]) -> 'Alteration':
|
|
136
|
+
"""
|
|
137
|
+
Translate the geometry by a given shift.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
output : Alteration
|
|
142
|
+
The object to store the result.
|
|
143
|
+
shift : tuple of float
|
|
144
|
+
The (x, y) shift values.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
Alteration
|
|
149
|
+
The translated geometry.
|
|
150
|
+
"""
|
|
151
|
+
shift_point = ff.components.utils.interpret_to_point(shift)
|
|
152
|
+
output._shapely_object = affinity.translate(self._shapely_object, shift_point.x, shift_point.y)
|
|
153
|
+
return output
|
|
154
|
+
|
|
155
|
+
@in_place_copy
|
|
156
|
+
def rotate(self, output, angle: float, origin: Tuple[float, float] = (0, 0)) -> 'Alteration':
|
|
157
|
+
"""
|
|
158
|
+
Rotate the geometry by a specified angle around an origin.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
output : Alteration
|
|
163
|
+
The object to store the result.
|
|
164
|
+
angle : float
|
|
165
|
+
The rotation angle in degrees.
|
|
166
|
+
origin : tuple of float, optional
|
|
167
|
+
The origin point for rotation, by default (0, 0).
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
Alteration
|
|
172
|
+
The rotated geometry.
|
|
173
|
+
"""
|
|
174
|
+
origin_point = ff.components.utils.interpret_to_point(origin)
|
|
175
|
+
output._shapely_object = affinity.rotate(self._shapely_object, angle=angle, origin=origin_point._shapely_object)
|
|
176
|
+
return output
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class BaseArea(Alteration):
|
|
180
|
+
@property
|
|
181
|
+
def is_iterable(self) -> bool:
|
|
182
|
+
"""
|
|
183
|
+
Check if the shapely object is iterable.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
bool
|
|
188
|
+
True if the object is iterable, False otherwise.
|
|
189
|
+
"""
|
|
190
|
+
return isinstance(self._shapely_object, Iterable)
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def is_multi(self) -> bool:
|
|
194
|
+
"""
|
|
195
|
+
Check if the shapely object is a MultiPolygon.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
bool
|
|
200
|
+
True if the object is a MultiPolygon, False otherwise.
|
|
201
|
+
"""
|
|
202
|
+
return isinstance(self._shapely_object, geo.MultiPolygon)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def is_empty(self) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
Check if the shapely object is empty.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
bool
|
|
212
|
+
True if the object is empty, False otherwise.
|
|
213
|
+
"""
|
|
214
|
+
return self._shapely_object.is_empty
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def is_pure_polygon(self) -> bool:
|
|
218
|
+
"""
|
|
219
|
+
Check if the shapely object is a pure Polygon.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
bool
|
|
224
|
+
True if the object is a pure Polygon, False otherwise.
|
|
225
|
+
"""
|
|
226
|
+
return isinstance(self._shapely_object, geo.Polygon)
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def exterior(self):
|
|
230
|
+
"""
|
|
231
|
+
Get the exterior boundary of the polygon.
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
LinearRing
|
|
236
|
+
The exterior boundary of the polygon.
|
|
237
|
+
"""
|
|
238
|
+
return self._shapely_object.exterior
|
|
239
|
+
|
|
240
|
+
def get_rasterized_mesh(self, coordinate_system: CoordinateSystem) -> np.ndarray:
|
|
241
|
+
"""
|
|
242
|
+
Rasterize the shape based on the given coordinate system.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
coordinate_system : CoordinateSystem
|
|
247
|
+
The coordinate system used for rasterization.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
numpy.ndarray
|
|
252
|
+
The rasterized shape as a mesh.
|
|
253
|
+
"""
|
|
254
|
+
raster = self.rasterize(coordinate_system=coordinate_system)
|
|
255
|
+
raster = raster.reshape(coordinate_system.shape)
|
|
256
|
+
return raster.astype(np.float64)
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def convex_hull(self) -> geo.Polygon:
|
|
260
|
+
"""
|
|
261
|
+
Get the convex hull of the shape.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
Polygon
|
|
266
|
+
The convex hull as a Polygon.
|
|
267
|
+
"""
|
|
268
|
+
return ff.Polygon(instance=self._shapely_object.convex_hull)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def area(self) -> float:
|
|
272
|
+
"""
|
|
273
|
+
Calculate the area of the shape.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
float
|
|
278
|
+
The area of the shape.
|
|
279
|
+
"""
|
|
280
|
+
return self._shapely_object.area
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def bounds(self) -> Tuple[float, float, float, float]:
|
|
284
|
+
"""
|
|
285
|
+
Get the bounds of the shape.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
tuple of float
|
|
290
|
+
The (minx, miny, maxx, maxy) bounds of the shape.
|
|
291
|
+
"""
|
|
292
|
+
return self._shapely_object.bounds
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def center(self) -> geo.Point:
|
|
296
|
+
"""
|
|
297
|
+
Get the center (centroid) of the shape.
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
Point
|
|
302
|
+
The centroid of the shape as a Point.
|
|
303
|
+
"""
|
|
304
|
+
return ff.Point(position=(self._shapely_object.centroid.x, self._shapely_object.centroid.y))
|
|
305
|
+
|
|
306
|
+
def __add__(self, other: 'BaseArea') -> 'BaseArea':
|
|
307
|
+
"""
|
|
308
|
+
Add two geometries together using union.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
other : BaseArea
|
|
313
|
+
The other geometry to union with.
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
BaseArea
|
|
318
|
+
The union of the two geometries.
|
|
319
|
+
"""
|
|
320
|
+
output = self.copy()
|
|
321
|
+
output._shapely_object = self._shapely_object.union(other._shapely_object)
|
|
322
|
+
return output
|
|
323
|
+
|
|
324
|
+
def __sub__(self, other: 'BaseArea', tolerance: float = 1e-9) -> 'BaseArea':
|
|
325
|
+
"""
|
|
326
|
+
Subtract one geometry from another.
|
|
327
|
+
|
|
328
|
+
Parameters
|
|
329
|
+
----------
|
|
330
|
+
other : BaseArea
|
|
331
|
+
The geometry to subtract.
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
BaseArea
|
|
336
|
+
The difference of the two geometries.
|
|
337
|
+
"""
|
|
338
|
+
output = self.copy()
|
|
339
|
+
|
|
340
|
+
buffered_self = self._shapely_object.buffer(tolerance)
|
|
341
|
+
|
|
342
|
+
if not buffered_self.intersects(other._shapely_object):
|
|
343
|
+
return self
|
|
344
|
+
|
|
345
|
+
output._shapely_object = buffered_self.difference(other._shapely_object)
|
|
346
|
+
|
|
347
|
+
return output
|
|
348
|
+
|
|
349
|
+
def __and__(self, other: 'BaseArea') -> 'BaseArea':
|
|
350
|
+
"""
|
|
351
|
+
Perform the intersection operation.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
other : BaseArea
|
|
356
|
+
The geometry to intersect with.
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
BaseArea
|
|
361
|
+
The intersection of the two geometries.
|
|
362
|
+
"""
|
|
363
|
+
output = self.copy()
|
|
364
|
+
output._shapely_object = self._shapely_object.intersection(other._shapely_object)
|
|
365
|
+
return output
|
|
366
|
+
|
|
367
|
+
def scale_position(self, factor: float) -> 'BaseArea':
|
|
368
|
+
"""
|
|
369
|
+
Scale the position of the shape.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
factor : float
|
|
374
|
+
The scaling factor for the shape's position.
|
|
375
|
+
|
|
376
|
+
Returns
|
|
377
|
+
-------
|
|
378
|
+
BaseArea
|
|
379
|
+
The scaled shape.
|
|
380
|
+
"""
|
|
381
|
+
new_position = affinity.scale(
|
|
382
|
+
self.center._shapely_object,
|
|
383
|
+
xfact=factor,
|
|
384
|
+
yfact=factor,
|
|
385
|
+
origin=(0, 0)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
shift = (new_position.x - self.center.x, new_position.y - self.center.y)
|
|
389
|
+
|
|
390
|
+
self.core.translate(shift=shift, in_place=True)
|
|
391
|
+
self.translate(shift=shift, in_place=True)
|
|
392
|
+
|
|
393
|
+
return self
|
|
394
|
+
|
|
395
|
+
def shift_position(self, shift: Tuple[float, float], inplace: bool = True) -> 'BaseArea':
|
|
396
|
+
"""
|
|
397
|
+
Shift the position of the shape.
|
|
398
|
+
|
|
399
|
+
Parameters
|
|
400
|
+
----------
|
|
401
|
+
shift : tuple of float
|
|
402
|
+
The (x, y) shift values.
|
|
403
|
+
|
|
404
|
+
Returns
|
|
405
|
+
-------
|
|
406
|
+
BaseArea
|
|
407
|
+
The shifted shape.
|
|
408
|
+
"""
|
|
409
|
+
if hasattr(self, 'core'):
|
|
410
|
+
self.core.translate(shift=shift, in_place=inplace)
|
|
411
|
+
self.translate(shift=shift, in_place=inplace)
|
|
412
|
+
return self
|
|
413
|
+
|
|
414
|
+
def split_with_line(self, line, return_largest: bool = True) -> 'ff.Polygon':
|
|
415
|
+
"""
|
|
416
|
+
Split the shape using a line.
|
|
417
|
+
|
|
418
|
+
Parameters
|
|
419
|
+
----------
|
|
420
|
+
line : LineString
|
|
421
|
+
The line used for splitting the shape.
|
|
422
|
+
return_largest : bool, optional
|
|
423
|
+
If True, returns the largest resulting polygon. Otherwise, returns the smallest. Default is True.
|
|
424
|
+
|
|
425
|
+
Returns
|
|
426
|
+
-------
|
|
427
|
+
ff.Polygon
|
|
428
|
+
The resulting polygon based on the split.
|
|
429
|
+
|
|
430
|
+
Raises
|
|
431
|
+
------
|
|
432
|
+
ValueError
|
|
433
|
+
If the shape is not a pure polygon.
|
|
434
|
+
"""
|
|
435
|
+
if not self.is_pure_polygon:
|
|
436
|
+
raise ValueError(f"Error: expected a pure polygon, but got {self._shapely_object.__class__}.")
|
|
437
|
+
|
|
438
|
+
split_geometry = split(self._shapely_object, line.copy().extend(factor=100)._shapely_object).geoms
|
|
439
|
+
areas = [poly.area for poly in split_geometry]
|
|
440
|
+
idx = np.argmax(areas) if return_largest else np.argmin(areas)
|
|
441
|
+
|
|
442
|
+
return ff.Polygon(instance=split_geometry[idx])
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
import numpy as np
|
|
6
|
+
from pydantic.dataclasses import dataclass
|
|
7
|
+
from dataclasses import field
|
|
8
|
+
from pydantic import ConfigDict
|
|
9
|
+
from shapely.affinity import scale
|
|
10
|
+
import shapely.geometry as geo
|
|
11
|
+
from FiberFusing.components.base_class import Alteration
|
|
12
|
+
import FiberFusing as ff
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(config=ConfigDict(extra='forbid', arbitrary_types_allowed=True))
|
|
16
|
+
class LineString(Alteration):
|
|
17
|
+
coordinates: Optional[Tuple] = field(default=None, repr=False)
|
|
18
|
+
instance: Optional[geo.LineString] = field(default=None, repr=False)
|
|
19
|
+
index: Optional[float] = field(default=None)
|
|
20
|
+
|
|
21
|
+
def __post_init__(self):
|
|
22
|
+
"""
|
|
23
|
+
Initialize a LineString object either from coordinates or a Shapely LineString instance.
|
|
24
|
+
"""
|
|
25
|
+
if not ((self.coordinates is None) ^ (self.instance is None)):
|
|
26
|
+
raise ValueError("Must initialize LineString with either 'coordinates' or 'instance', not both.")
|
|
27
|
+
|
|
28
|
+
if self.instance is not None:
|
|
29
|
+
self._shapely_object = self.instance
|
|
30
|
+
else:
|
|
31
|
+
if len(self.coordinates) != 2:
|
|
32
|
+
raise ValueError('LineString must be initialized with exactly two coordinate pairs.')
|
|
33
|
+
_coordinates = (self.coordinates[0]._shapely_object, self.coordinates[1]._shapely_object)
|
|
34
|
+
self._shapely_object = geo.LineString(_coordinates)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def boundary(self) -> Tuple[geo.Point, geo.Point]:
|
|
38
|
+
""" Returns the boundary points of the line string as a tuple of Shapely Point objects. """
|
|
39
|
+
return self._shapely_object.boundary.geoms
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def center(self) -> geo.Point:
|
|
43
|
+
""" Returns the centroid of the line as a Shapely Point object. """
|
|
44
|
+
return self._shapely_object.centroid
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def mid_point(self) -> ff.Point:
|
|
48
|
+
""" Returns the midpoint of the line string as a Shapely Point object. """
|
|
49
|
+
P0, P1 = self.boundary
|
|
50
|
+
return ff.Point(position=[(P0.x + P1.x) / 2, (P0.y + P1.y) / 2])
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def length(self) -> float:
|
|
54
|
+
""" Returns the length of the line string. """
|
|
55
|
+
return self._shapely_object.length
|
|
56
|
+
|
|
57
|
+
def intersect(self, other: 'LineString') -> None:
|
|
58
|
+
""" Intersects this line string with another, updating the shapely object in place. """
|
|
59
|
+
self._shapely_object = self._shapely_object.intersection(other._shapely_object)
|
|
60
|
+
|
|
61
|
+
def get_perpendicular(self) -> 'LineString':
|
|
62
|
+
""" Returns a new LineString object that is perpendicular to this one. """
|
|
63
|
+
mid = self.mid_point
|
|
64
|
+
p0, p1 = self.boundary
|
|
65
|
+
dx, dy = p1.x - p0.x, p1.y - p0.y
|
|
66
|
+
new_coords = ff.Point(position=(mid.x - dy, mid.y + dx)), ff.Point(position=(mid.x + dy, mid.y - dx))
|
|
67
|
+
return LineString(coordinates=new_coords)
|
|
68
|
+
|
|
69
|
+
def get_position_parametrization(self, t: float) -> ff.Point:
|
|
70
|
+
""" Returns the point at parameter t along the line. 0 <= t <= 1. """
|
|
71
|
+
p0, p1 = self.boundary
|
|
72
|
+
x = p0.x * (1 - t) + p1.x * t
|
|
73
|
+
y = p0.y * (1 - t) + p1.y * t
|
|
74
|
+
return ff.Point(position=(x, y))
|
|
75
|
+
|
|
76
|
+
def render_on_axis(self, ax, **kwargs) -> None:
|
|
77
|
+
""" Renders the line string on a given matplotlib axis. """
|
|
78
|
+
x, y = self._shapely_object.xy
|
|
79
|
+
ax.plot(x, y, **kwargs)
|
|
80
|
+
|
|
81
|
+
def make_length(self, length: float) -> 'LineString':
|
|
82
|
+
""" Returns a new LineString scaled to the specified length. """
|
|
83
|
+
return self.extend(factor=length / self.length)
|
|
84
|
+
|
|
85
|
+
def centering(self, center: geo.Point) -> 'LineString':
|
|
86
|
+
""" Centers the line string at a given point. """
|
|
87
|
+
p0, p1 = self.boundary
|
|
88
|
+
mid = self.mid_point
|
|
89
|
+
dx, dy = center.x - mid.x, center.y - mid.y
|
|
90
|
+
new_coords = [(p.x + dx, p.y + dy) for p in [p0, p1]]
|
|
91
|
+
new_coords = ff.Point(position=new_coords[0]), ff.Point(position=new_coords[1])
|
|
92
|
+
return LineString(coordinates=tuple(new_coords))
|
|
93
|
+
|
|
94
|
+
def get_vector(self) -> np.ndarray:
|
|
95
|
+
""" Returns a unit vector representing the direction of the line string. """
|
|
96
|
+
p0, p1 = self.boundary
|
|
97
|
+
dx, dy = p1.x - p0.x, p1.y - p0.y
|
|
98
|
+
norm = np.sqrt(dx**2 + dy**2)
|
|
99
|
+
return np.array([dx, dy]) / norm if norm != 0 else np.array([0, 0])
|
|
100
|
+
|
|
101
|
+
def extend(self, factor: float = 1) -> 'LineString':
|
|
102
|
+
""" Returns a new LineString scaled by the specified factor. """
|
|
103
|
+
scaled_string = scale(self._shapely_object, xfact=factor, yfact=factor, origin='center')
|
|
104
|
+
return LineString(instance=scaled_string)
|
|
105
|
+
|
|
106
|
+
# -
|