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
54
55
56
57
58
59
60
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_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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
112
113
114
115
116
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_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
139
140
141
142
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
144
145
146
147
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_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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
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]
            distance_result = self.point_distance(
                span_index, obstacle_coords
            )
            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
175
176
177
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
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
179
180
181
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
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
158
159
160
161
162
163
164
165
166
167
168
169
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
171
172
173
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
62
63
64
65
66
67
68
69
70
71
72
73
74
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.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,
    )

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
183
184
185
186
187
188
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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
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
272
273
274
275
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
309
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
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
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
102
103
104
105
106
107
108
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()