Skip to content

position_engine

PositionEngine

PositionEngine(balance_engine: BalanceEngine)

Bases: Observer, Notifier

PositionEngine computes point positions, distances, and coordinates.

Observes a BalanceEngine and updates its internal geometry state whenever the balance engine notifies observers. It is also a Notifier itself, so downstream observers (e.g. a PlotEngine) are automatically notified after every update.

Users can work with a PositionEngine directly — without any Plotly dependency — to obtain span points, support positions, obstacle coordinates, and point-to-cable distances.

Parameters:

Name Type Description Default

balance_engine

BalanceEngine

BalanceEngine to observe.

required

Examples:

1
2
3
4
5
6
>>> from mechaphlowers.core.geometry.position_engine import PositionEngine
>>> pos_engine = PositionEngine(balance_engine)
>>> pos_engine.get_supports_points()
array(...)
>>> pos_engine.get_spans_points(frame="section")
array(...)
Source code in src/mechaphlowers/core/geometry/position_engine.py
58
59
60
61
62
63
64
def __init__(self, balance_engine: BalanceEngine) -> None:
    Notifier.__init__(self)
    balance_engine.bind_to(self)

    self.distance_engine = DistanceEngine()
    self.initialize_engine(balance_engine)
    self.reset(balance_engine=balance_engine)

beta property

beta: ndarray

Load angle (\(\beta\)) for each span, in radians.

add_additional_point

add_additional_point(
    name: str,
    span_index: int,
    coords: ndarray,
    support_reference: Literal['left', 'right'] = 'left',
    span_length: ndarray | None = None,
)

Delegate to ObstacleArray.add_obstacle.

Source code in src/mechaphlowers/core/geometry/position_engine.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def add_additional_point(
    self,
    name: str,
    span_index: int,
    coords: np.ndarray,
    support_reference: Literal['left', 'right'] = 'left',
    span_length: np.ndarray | None = None,
):
    """Delegate to [`ObstacleArray.add_obstacle`][mechaphlowers.entities.arrays.ObstacleArray.add_obstacle]."""
    self.additional_points_array.add_obstacle(
        name,
        span_index,
        coords,
        object_type="additional",
        support_reference=support_reference,
        span_length=span_length,
    )
    self.coords_calculator.refresh_additional_points()

add_additional_points_array

add_additional_points_array(
    additional_points_array: ObstacleArray,
) -> None

Attach an ObstacleArray for additional points coordinate computation.

Source code in src/mechaphlowers/core/geometry/position_engine.py
155
156
157
158
159
160
161
162
163
def add_additional_points_array(
    self, additional_points_array: ObstacleArray
) -> None:
    """Attach an `ObstacleArray` for additional points coordinate computation."""
    self.additional_points_array = additional_points_array
    self.coords_calculator.additional_points_array = (
        self.additional_points_array
    )
    self.coords_calculator.refresh_additional_points()

add_obstacle

add_obstacle(
    name: str,
    span_index: int,
    coords: ndarray,
    object_type: str = 'ground',
    support_reference: Literal['left', 'right'] = 'left',
    span_length: ndarray | None = None,
)

Delegate to ObstacleArray.add_obstacle.

Source code in src/mechaphlowers/core/geometry/position_engine.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def add_obstacle(
    self,
    name: str,
    span_index: int,
    coords: np.ndarray,
    object_type: str = "ground",
    support_reference: Literal['left', 'right'] = 'left',
    span_length: np.ndarray | None = None,
):
    """Delegate to [`ObstacleArray.add_obstacle`][mechaphlowers.entities.arrays.ObstacleArray.add_obstacle]."""
    self.obstacle_array.add_obstacle(
        name,
        span_index,
        coords,
        object_type,
        support_reference,
        span_length,
    )
    # Replace by a Notifier/Observer pattern?
    self.coords_calculator.refresh_obstacles()

add_obstacle_array

add_obstacle_array(obstacle_array: ObstacleArray) -> None

Attach an ObstacleArray for coordinate computation.

Source code in src/mechaphlowers/core/geometry/position_engine.py
118
119
120
121
122
def add_obstacle_array(self, obstacle_array: ObstacleArray) -> None:
    """Attach an `ObstacleArray` for coordinate computation."""
    self.obstacle_array = obstacle_array
    self.coords_calculator.obstacle_array = self.obstacle_array
    self.coords_calculator.refresh_obstacles()

delete_additional_point

delete_additional_point(
    names_to_delete: str | list[str],
) -> None

Delegate to ObstacleArray.delete_obstacle.

Source code in src/mechaphlowers/core/geometry/position_engine.py
184
185
186
187
188
189
def delete_additional_point(
    self, names_to_delete: str | list[str]
) -> None:
    """Delegate to [`ObstacleArray.delete_obstacle`][mechaphlowers.entities.arrays.ObstacleArray.delete_obstacle]."""
    self.additional_points_array.delete_obstacle(names_to_delete)
    self.coords_calculator.refresh_additional_points()

delete_additional_point_by_index

delete_additional_point_by_index(
    name: str, point_index: int
) -> None

Delegate to ObstacleArray.delete_point.

Source code in src/mechaphlowers/core/geometry/position_engine.py
191
192
193
194
195
196
def delete_additional_point_by_index(
    self, name: str, point_index: int
) -> None:
    """Delegate to [`ObstacleArray.delete_point`][mechaphlowers.entities.arrays.ObstacleArray.delete_point]."""
    self.additional_points_array.delete_point(name, point_index)
    self.coords_calculator.refresh_additional_points()

delete_obstacle

delete_obstacle(
    obs_names_to_delete: str | list[str],
) -> None

Delegate to ObstacleArray.delete_obstacle.

Source code in src/mechaphlowers/core/geometry/position_engine.py
145
146
147
148
def delete_obstacle(self, obs_names_to_delete: str | list[str]) -> None:
    """Delegate to [`ObstacleArray.delete_obstacle`][mechaphlowers.entities.arrays.ObstacleArray.delete_obstacle]."""
    self.obstacle_array.delete_obstacle(obs_names_to_delete)
    self.coords_calculator.refresh_obstacles()

delete_point

delete_point(obs_name: str, point_index: int) -> None

Delegate to ObstacleArray.delete_point.

Source code in src/mechaphlowers/core/geometry/position_engine.py
150
151
152
153
def delete_point(self, obs_name: str, point_index: int) -> None:
    """Delegate to [`ObstacleArray.delete_point`][mechaphlowers.entities.arrays.ObstacleArray.delete_point]."""
    self.obstacle_array.delete_point(obs_name, point_index)
    self.coords_calculator.refresh_obstacles()

get_additional_points

get_additional_points() -> ndarray

Return additional points coordinates transformed to the section frame.

Source code in src/mechaphlowers/core/geometry/position_engine.py
232
233
234
235
236
237
238
def get_additional_points(self) -> np.ndarray:
    """Return additional points coordinates transformed to the section frame."""
    return (
        self.coords_calculator.compute_additional_points_coords().points(
            True
        )
    )

get_distances_from_obstacles

get_distances_from_obstacles() -> (
    dict[str, dict[int, DistanceResult]]
)

Compute distances for all obstacles to their respective spans.

Only in absolute coordiantes.

{ 'obs_0': {0: DistanceResult, 1: DistanceResult}, 'obs_1': {0: DistanceResult} }

Returns:

Name Type Description
dict dict[str, dict[int, DistanceResult]]

dictionary of DistanceResult, sorted by obstacles

Source code in src/mechaphlowers/core/geometry/position_engine.py
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
def get_distances_from_obstacles(
    self,
) -> dict[str, dict[int, DistanceResult]]:
    """Compute distances for all obstacles to their respective spans.

    Only in absolute coordiantes.

    {
        'obs_0': {0: DistanceResult, 1: DistanceResult},
        'obs_1': {0: DistanceResult}
    }

    Returns:
        dict: dictionary of DistanceResult, sorted by obstacles
    """

    distance_dict_result = {}
    # self.coord_calculator.compute_obstacle_coords()?
    obstacle_sparse_points = self.coords_calculator.obstacles_points
    # index of obstacle point among total points
    loop_index = 0
    for (
        obstacle_name,
        obstacle_coords_array,
    ) in obstacle_sparse_points.dict_coords().items():
        current_distance_result = {}
        # create dict {0: DistanceResult, 1:: DistanceResult, ...} per obstacle
        for obstacle_coords in obstacle_coords_array:
            span_index = obstacle_sparse_points.span_index[loop_index]
            point_index = obstacle_sparse_points.point_index[loop_index]
            try:
                distance_result = self.point_distance(
                    span_index, obstacle_coords
                )
            except NoIntersectionPlaneForDistanceError as error:
                message = (
                    f"No intersection found between obstacle '{obstacle_name}' "
                    f"(point index {point_index}) and span {span_index} - "
                    f"skipping distance computation for this point. ({error})"
                )
                logger.warning(message)
                warnings.warn(
                    message, NoIntersectionPlaneWarning, stacklevel=2
                )
                loop_index += 1
                continue
            current_distance_result[point_index] = distance_result
            loop_index += 1
        distance_dict_result[obstacle_name] = current_distance_result

    return distance_dict_result

get_insulators_points

get_insulators_points() -> ndarray

Return insulator attachment points (absolute section frame).

Source code in src/mechaphlowers/core/geometry/position_engine.py
224
225
226
def get_insulators_points(self) -> np.ndarray:
    """Return insulator attachment points (absolute section frame)."""
    return self.coords_calculator.get_insulators().points(True)

get_loads_coords

get_loads_coords(
    project: bool = False, frame_index: int = 0
) -> dict

Return a dictionary of load coordinates indexed by span.

If loads exist on spans 0 and 2, the result looks like: {0: [x0, y0, z0], 2: [x2, y2, z2]}.

Parameters:

Name Type Description Default

project

bool

True to project all objects into a support frame (for 2-D graphs). Defaults to False.

False

frame_index

int

Index of the support frame used for projection. Must be in [0, nb_supports - 1]. Unused when project is False. Defaults to 0.

0

Returns:

Type Description
dict

Dict mapping span index (int) to coordinate array of shape

dict

(3,).

Source code in src/mechaphlowers/core/geometry/position_engine.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def get_loads_coords(
    self, project: bool = False, frame_index: int = 0
) -> dict:
    """Return a dictionary of load coordinates indexed by span.

    If loads exist on spans 0 and 2, the result looks like:
    ``{0: [x0, y0, z0], 2: [x2, y2, z2]}``.

    Args:
        project: ``True`` to project all objects into a support frame
            (for 2-D graphs). Defaults to ``False``.
        frame_index: Index of the support frame used for projection.
            Must be in ``[0, nb_supports - 1]``.  Unused when
            `project` is `False`.  Defaults to ``0``.

    Returns:
        Dict mapping span index (``int``) to coordinate array of shape
        ``(3,)``.
    """
    spans_points, _, _ = self.get_points_for_plot(project, frame_index)
    loads_spans_idx, loads_points_idx = self.span_model.loads_indices
    result_dict: dict = {}
    for index_in_small_array, span_index in enumerate(loads_spans_idx):
        point_index = loads_points_idx[index_in_small_array]
        result_dict[int(span_index)] = spans_points.coords[
            span_index, point_index
        ]
    return result_dict

get_loads_coords_group_points

get_loads_coords_group_points(
    project: bool = False, frame_index: int = 0
) -> dict

Same as get_loads_coords() but uses GroupPoints object

Return a dictionary of load coordinates indexed by span.

If loads exist on spans 0 and 2, the result looks like: {0: [x0, y0, z0], 2: [x2, y2, z2]}.

Parameters:

Name Type Description Default

project

bool

True to project all objects into a support frame (for 2-D graphs). Defaults to False.

False

frame_index

int

Index of the support frame used for projection. Must be in [0, nb_supports - 1]. Unused when project is False. Defaults to 0.

0

Returns:

Type Description
dict

Dict mapping span index (int) to coordinate array of shape

dict

(3,).

Source code in src/mechaphlowers/core/geometry/position_engine.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
def get_loads_coords_group_points(
    self, project: bool = False, frame_index: int = 0
) -> dict:
    """Same as get_loads_coords() but uses GroupPoints object

    Return a dictionary of load coordinates indexed by span.

    If loads exist on spans 0 and 2, the result looks like:
    ``{0: [x0, y0, z0], 2: [x2, y2, z2]}``.

    Args:
        project: ``True`` to project all objects into a support frame
            (for 2-D graphs). Defaults to ``False``.
        frame_index: Index of the support frame used for projection.
            Must be in ``[0, nb_supports - 1]``.  Unused when
            `project` is `False`.  Defaults to ``0``.

    Returns:
        Dict mapping span index (``int``) to coordinate array of shape
        ``(3,)``.
    """
    group_points = self.get_group_points()
    if project:
        group_points = group_points.change_frame(frame_index)
    spans_points = group_points.get_all_objects_dict()["spans"]
    loads_spans_idx, loads_points_idx = self.span_model.loads_indices
    result_dict: dict = {}
    for index_in_small_array, span_index in enumerate(loads_spans_idx):
        point_index = loads_points_idx[index_in_small_array]
        result_dict[int(span_index)] = spans_points.coords[
            span_index, point_index
        ]
    return result_dict

get_obstacles_points

get_obstacles_points() -> ndarray

Return obstacle coordinates transformed to the section frame.

Source code in src/mechaphlowers/core/geometry/position_engine.py
228
229
230
def get_obstacles_points(self) -> np.ndarray:
    """Return obstacle coordinates transformed to the section frame."""
    return self.coords_calculator.compute_obstacle_coords().points(True)

get_points_for_plot

get_points_for_plot(
    project: bool = False, frame_index: int = 0
) -> tuple[Points, Points, Points]

Return Points objects for spans, supports, and insulators.

Parameters:

Name Type Description Default

project

bool

True to project into a support frame (2-D mode).

False

frame_index

int

Index of the support frame for projection.

0

Returns:

Type Description
tuple[Points, Points, Points]

Tuple of (spans, supports, insulators) as Points.

Raises:

Type Description
ValueError

If frame_index is out of range.

Source code in src/mechaphlowers/core/geometry/position_engine.py
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
def get_points_for_plot(
    self, project: bool = False, frame_index: int = 0
) -> tuple[Points, Points, Points]:
    """Return `Points` objects for spans, supports, and insulators.

    Args:
        project: `True` to project into a support frame (2-D mode).
        frame_index: Index of the support frame for projection.

    Returns:
        Tuple of ``(spans, supports, insulators)`` as `Points`.

    Raises:
        ValueError: If `frame_index` is out of range.
    """
    return self.coords_calculator.get_points_for_plot(project, frame_index)

get_spans_points

get_spans_points(
    frame: Literal['section', 'localsection', 'cable'],
) -> ndarray

Return span cable points in the requested coordinate frame.

Parameters:

Name Type Description Default

frame

Literal['section', 'localsection', 'cable']

One of "section", "localsection", or "cable".

required

Returns:

Type Description
ndarray

numpy array of shape (n_points, 3).

Source code in src/mechaphlowers/core/geometry/position_engine.py
207
208
209
210
211
212
213
214
215
216
217
218
def get_spans_points(
    self, frame: Literal["section", "localsection", "cable"]
) -> np.ndarray:
    """Return span cable points in the requested coordinate frame.

    Args:
        frame: One of ``"section"``, ``"localsection"``, or ``"cable"``.

    Returns:
        numpy array of shape ``(n_points, 3)``.
    """
    return self.coords_calculator.get_spans(frame).points(True)

get_supports_points

get_supports_points() -> ndarray

Return support structure points (absolute section frame).

Source code in src/mechaphlowers/core/geometry/position_engine.py
220
221
222
def get_supports_points(self) -> np.ndarray:
    """Return support structure points (absolute section frame)."""
    return self.coords_calculator.get_supports().points(True)

initialize_engine

initialize_engine(balance_engine: BalanceEngine) -> None

Initialise internal references from balance_engine.

Source code in src/mechaphlowers/core/geometry/position_engine.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def initialize_engine(self, balance_engine: BalanceEngine) -> None:
    """Initialise internal references from `balance_engine`."""
    self.span_model = balance_engine.balance_model.nodes_span_model
    self.cable_loads = balance_engine.cable_loads
    self.section_array = balance_engine.section_array
    self.obstacle_array = ObstacleArray.build_empty_array()
    self.additional_points_array = ObstacleArray.build_empty_array()
    self.coords_calculator = CoordsCalculator(
        section_array=self.section_array,
        span_model=self.span_model,
        cable_loads=self.cable_loads,
        get_displacement=balance_engine.get_displacement,
        obstacle_array=self.obstacle_array,
        additional_points_array=self.additional_points_array,
    )

obstacles_dict

obstacles_dict(project=False, frame_index=0) -> dict

Return obstacle coordinates keyed by obstacle name.

Format: {'obs_0': [[x0, y0, z0], [x1, y1, z1], ...]}.

Source code in src/mechaphlowers/core/geometry/position_engine.py
240
241
242
243
244
245
def obstacles_dict(self, project=False, frame_index=0) -> dict:
    """Return obstacle coordinates keyed by obstacle name.

    Format: ``{'obs_0': [[x0, y0, z0], [x1, y1, z1], ...]}``.
    """
    return self.coords_calculator.obstacles_dict(project, frame_index)

point_distance

point_distance(
    span_index: int, point: ndarray
) -> DistanceResult

Compute the minimum distance from point to a cable span.

Parameters:

Name Type Description Default

span_index

int

Span index in [0, num_supports - 2].

required

point

ndarray

Absolute coordinates of shape (3,).

required

Returns:

Type Description
DistanceResult

DistanceResult with the distance value and closest-point coordinates.

Raises:

Type Description
IndexError

If span_index is out of range.

ValueError

If point does not have shape (3,).

Source code in src/mechaphlowers/core/geometry/position_engine.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
def point_distance(
    self, span_index: int, point: np.ndarray
) -> DistanceResult:
    """Compute the minimum distance from `point` to a cable span.

    Args:
        span_index: Span index in ``[0, num_supports - 2]``.
        point: Absolute coordinates of shape ``(3,)``.

    Returns:
        `DistanceResult` with the distance value and closest-point coordinates.

    Raises:
        IndexError: If `span_index` is out of range.
        ValueError: If `point` does not have shape ``(3,)``.
    """
    point = np.asarray(point)
    if point.shape != (3,):
        raise ValueError("point must be a 1D array of shape (3,)")

    ground_supports = self.coords_calculator.supports_ground_coords.copy()
    if span_index < 0 or span_index >= len(ground_supports) - 1:
        raise IndexError(
            f"span_index {span_index} out of range"
            f" [0, {len(ground_supports) - 2}]"
        )

    self.distance_engine.add_span_frame(
        ground_supports[span_index], ground_supports[span_index + 1]
    )
    self.distance_engine.add_curves(
        self.coords_calculator.get_spans(frame="section").coords[
            span_index
        ]
    )
    return self.distance_engine.plane_distance(point, frame="section")

point_relative_to_absolute

point_relative_to_absolute(
    span_index: int, point_relative: ndarray
) -> ndarray

Convert a point from the span-local frame to absolute coordinates.

Span-local frame definition:

  • X: along the span direction projected onto the XY plane
  • Y: perpendicular to X in the XY plane
  • Z: vertical (global Z)

Parameters:

Name Type Description Default

span_index

int

Span index in [0, num_supports - 2].

required

point_relative

ndarray

Coordinate [x, y, z] in the span-local frame.

required

Returns:

Type Description
ndarray

Absolute coordinate array of shape (3,).

Raises:

Type Description
IndexError

If span_index is out of range.

ValueError

If point_relative does not have shape (3,).

Source code in src/mechaphlowers/core/geometry/position_engine.py
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
def point_relative_to_absolute(
    self, span_index: int, point_relative: np.ndarray
) -> np.ndarray:
    """Convert a point from the span-local frame to absolute coordinates.

    Span-local frame definition:

    - X: along the span direction projected onto the XY plane
    - Y: perpendicular to X in the XY plane
    - Z: vertical (global Z)

    Args:
        span_index: Span index in ``[0, num_supports - 2]``.
        point_relative: Coordinate ``[x, y, z]`` in the span-local frame.

    Returns:
        Absolute coordinate array of shape ``(3,)``.

    Raises:
        IndexError: If `span_index` is out of range.
        ValueError: If `point_relative` does not have shape ``(3,)``.
    """
    point_relative = np.asarray(point_relative)
    if point_relative.shape != (3,):
        raise ValueError("point_relative must be a 1D array of shape (3,)")

    ground_supports = self.coords_calculator.supports_ground_coords
    if span_index < 0 or span_index >= len(ground_supports) - 1:
        raise IndexError(
            f"span_index {span_index} out of range"
            f" [0, {len(ground_supports) - 2}]"
        )

    return change_local_frame(
        ground_supports[span_index],
        ground_supports[span_index + 1],
        point_relative,
    )

reset

reset(balance_engine: BalanceEngine) -> None

Reset geometry state from balance_engine.

Called automatically by update when the balance engine notifies; can also be called manually after direct modifications to the section array.

Raises:

Type Description
TypeError

If balance_engine is not a BalanceEngine.

Source code in src/mechaphlowers/core/geometry/position_engine.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def reset(self, balance_engine: BalanceEngine) -> None:
    """Reset geometry state from `balance_engine`.

    Called automatically by `update` when the balance engine
    notifies; can also be called manually after direct modifications to
    the section array.

    Raises:
        TypeError: If `balance_engine` is not a `BalanceEngine`.
    """
    if not isinstance(balance_engine, BalanceEngine):
        raise TypeError(
            "balance_engine must be an instance of BalanceEngine"
        )
    if balance_engine.initialized is False:
        self.initialize_engine(balance_engine)
    self.coords_calculator.reset()

update

update(notifier: Notifier) -> None

Observer callback — invoked by BalanceEngine on state change.

Source code in src/mechaphlowers/core/geometry/position_engine.py
108
109
110
111
112
113
114
def update(self, notifier: Notifier) -> None:
    """Observer callback — invoked by `BalanceEngine` on state change."""
    logger.debug("Position engine notified from balance engine.")
    if isinstance(notifier, BalanceEngine):
        self.reset(balance_engine=notifier)
        # Propagate the notification to any downstream observers (e.g. PlotEngine).
        self.notify()