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.

Files changed (68) hide show
  1. FiberFusing/__init__.py +15 -0
  2. FiberFusing/_version.py +16 -0
  3. FiberFusing/background.py +92 -0
  4. FiberFusing/base_fused.py +520 -0
  5. FiberFusing/buffer.py +163 -0
  6. FiberFusing/components/__init__.py +0 -0
  7. FiberFusing/components/base_class.py +442 -0
  8. FiberFusing/components/linestring.py +106 -0
  9. FiberFusing/components/point.py +91 -0
  10. FiberFusing/components/polygon.py +228 -0
  11. FiberFusing/components/utils.py +59 -0
  12. FiberFusing/configuration/__init__.py +7 -0
  13. FiberFusing/configuration/line/__init__.py +10 -0
  14. FiberFusing/configuration/line/configuration_01x01.py +14 -0
  15. FiberFusing/configuration/line/configuration_02x02.py +18 -0
  16. FiberFusing/configuration/line/configuration_03x03.py +18 -0
  17. FiberFusing/configuration/line/configuration_04x04.py +18 -0
  18. FiberFusing/configuration/line/configuration_05x05.py +18 -0
  19. FiberFusing/configuration/ring/__init__.py +15 -0
  20. FiberFusing/configuration/ring/configuration_01x01.py +14 -0
  21. FiberFusing/configuration/ring/configuration_02x02.py +18 -0
  22. FiberFusing/configuration/ring/configuration_03x03.py +18 -0
  23. FiberFusing/configuration/ring/configuration_04x04.py +18 -0
  24. FiberFusing/configuration/ring/configuration_05x05.py +18 -0
  25. FiberFusing/configuration/ring/configuration_06x06.py +18 -0
  26. FiberFusing/configuration/ring/configuration_07x07.py +20 -0
  27. FiberFusing/configuration/ring/configuration_10x10.py +31 -0
  28. FiberFusing/configuration/ring/configuration_12x12.py +30 -0
  29. FiberFusing/configuration/ring/configuration_19x19.py +34 -0
  30. FiberFusing/connection.py +366 -0
  31. FiberFusing/connection_optimization.py +246 -0
  32. FiberFusing/coordinate_system.py +256 -0
  33. FiberFusing/directories.py +34 -0
  34. FiberFusing/fiber/__init__.py +0 -0
  35. FiberFusing/fiber/catalogue.py +165 -0
  36. FiberFusing/fiber/fiber_files/1550BHP.yaml +14 -0
  37. FiberFusing/fiber/fiber_files/DCF13.yaml +18 -0
  38. FiberFusing/fiber/fiber_files/DCF1300S_20.yaml +17 -0
  39. FiberFusing/fiber/fiber_files/DCF1300S_26.yaml +18 -0
  40. FiberFusing/fiber/fiber_files/DCF1300S_33.yaml +17 -0
  41. FiberFusing/fiber/fiber_files/DCF1300S_42.yaml +18 -0
  42. FiberFusing/fiber/fiber_files/F2028M12.yaml +18 -0
  43. FiberFusing/fiber/fiber_files/F2028M21.yaml +18 -0
  44. FiberFusing/fiber/fiber_files/F2028M24.yaml +18 -0
  45. FiberFusing/fiber/fiber_files/F2058G1.yaml +18 -0
  46. FiberFusing/fiber/fiber_files/F2058L1.yaml +18 -0
  47. FiberFusing/fiber/fiber_files/HI1060.yaml +14 -0
  48. FiberFusing/fiber/fiber_files/HP630.yaml +13 -0
  49. FiberFusing/fiber/fiber_files/SM1950.yaml +14 -0
  50. FiberFusing/fiber/fiber_files/SMF28.yaml +14 -0
  51. FiberFusing/fiber/fiber_files/fluorine_doped_1%_capillary.yaml +9 -0
  52. FiberFusing/fiber/fiber_files/fluorine_doped_2%_capillary.yaml +9 -0
  53. FiberFusing/fiber/fiber_files/test_fiber.yaml +14 -0
  54. FiberFusing/fiber/fiber_files/test_multimode_fiber.yaml +14 -0
  55. FiberFusing/fiber/generic_fiber.py +413 -0
  56. FiberFusing/fiber/loader.py +120 -0
  57. FiberFusing/fiber/structure_collection.py +173 -0
  58. FiberFusing/fiber_structure.py +210 -0
  59. FiberFusing/geometry.py +458 -0
  60. FiberFusing/helper.py +59 -0
  61. FiberFusing/optical_structure.py +96 -0
  62. FiberFusing/plottings.py +43 -0
  63. FiberFusing/utils.py +108 -0
  64. FiberFusing-1.5.0.0.post0.dist-info/LICENSE +21 -0
  65. FiberFusing-1.5.0.0.post0.dist-info/METADATA +193 -0
  66. FiberFusing-1.5.0.0.post0.dist-info/RECORD +68 -0
  67. FiberFusing-1.5.0.0.post0.dist-info/WHEEL +5 -0
  68. 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
+ # -