"""
Multi-straight line 2D geometric entity module for UPXO.
This module provides multi-straight-line (polyline) representations in 2D space.
It supports closed and open polylines with operations for node/line management,
spatial queries, adjacent relationship detection, and geometric transformations.
Core Classes
------------
- ``MSline2d`` : Multi-straight-line in 2D (collection of connected line segments).
- ``ring2d`` : Ring structure in 2D (to be further developed).
- ``mulring2d`` : Multiple rings in 2D (to be further developed).
Key Features
------------
- Line chain construction from various input formats (node points, coordinates,
existing lines).
- Node and line enumeration, spacing, and connectivity checks.
- Adjacency and spatial relationship detection between polylines.
- Centroid and length calculations.
- Open/closed topology management.
- Node coordinate access and updates.
- Spatial tree indexing for nearest-neighbor queries.
- Point location finding within polyline node sequences.
- Plotting and visualization support.
Applications
------------
- Non-conformal to conformal geometry conversion.
- Hierarchical grain structure feature generation.
- General geometry operations and polyline manipulation.
- Boundary and edge representation in meshing and simulation.
Usage
-----
::
from upxo.geoEntities.mulsline2d import MSline2d
Coordinate System
-----------------
Standard 2D Cartesian with notation (X+, Y+) = (right, up):
Y+
|
|
|
|
X- | X+
-----+-----+-----O-----+-----+-----
|
|
|
|
Y-
@author: Dr. Sunil Anandatheertha
Examples
--------------
**Construct from line objects:**
>>> from upxo.geoEntities.mulsline2d import MSline2d
>>> from upxo.geoEntities.sline2d import Sline2d
>>> lines = [
... Sline2d(0, 0, 1, 1),
... Sline2d(1, 1, 2, 2),
... Sline2d(2, 2, 3, 1)
... ]
>>> msl = MSline2d.from_lines(lines, close=False)
>>> msl.nlines, msl.nnodes
(3, 4)
**Construct from coordinates:**
>>> coords = [(0, 0), (1, 1), (2, 2), (3, 1)]
>>> msl = MSline2d.by_coords(coords, close=True)
>>> msl.length
>>> msl.centroid
**Query relationships:**
>>> msl1 = MSline2d.by_coords([(0, 0), (1, 1)])
>>> msl2 = MSline2d.by_coords([(1, 1), (2, 2)])
>>> msl1.do_i_precede(msl2) # Check if msl1 comes before msl2
True
**Find closest nodes:**
>>> from upxo.geoEntities.point2d import Point2d
>>> msl = MSline2d.by_coords([(0, 0), (2, 0), (4, 0)])
>>> closest = msl.find_closest_nodes(Point2d(1.5, 0.5))
See Also
--------
- ``upxo.geoEntities.sline2d`` : Single straight-line entity.
- ``upxo.geoEntities.point2d`` : 2D point entity.
- ``upxo.geoEntities.polygon2d`` : 2D polygon entity.
"""
import math
import numpy as np
from copy import deepcopy
from scipy.spatial import cKDTree
import vtk
from shapely.geometry import Point as ShPnt, Polygon as ShPol
from functools import wraps
import matplotlib.pyplot as plt
import upxo._sup.dataTypeHandlers as dth
from scipy.spatial import distance
from scipy.spatial.distance import cdist
from upxo.geoEntities.point2d import Point2d
from upxo.geoEntities.bases import UPXO_Point, UPXO_Edge
np.seterr(divide='ignore')
from upxo._sup.validation_values import isinstance_many
from upxo.geoEntities.sline2d import Sline2d as sl2d
from upxo.geoEntities.point2d import p2d_leanest
from shapely.geometry import Polygon, MultiPolygon
from upxo._sup.data_ops import mean_coordinates
NUMBERS = dth.dt.NUMBERS
ITERABLES = dth.dt.ITERABLES
[docs]
class MSline2d():
"""
Multi-straight-line (polyline) entity in 2D for UPXO.
A connected chain of :class:`~upxo.geoEntities.sline2d.Sline2d` line
segments sharing end-nodes. Supports open and closed topologies.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
e0 = sl2d(0.0, 0.0, 1.0, 1.0)
e1 = sl2d(1.0, 1.0, 1.5, 1.5)
e2 = sl2d(1.5, 1.5, 2.5, 2.5)
e3 = sl2d(2.5, 2.5, 4.0, 4.0)
e4 = sl2d(4.0, 4.0, 4.0, 6.0)
me = msl2d.from_lines([e0, e1, e2, e3, e4], close=False)
"""
__slots__ = ('lines', 'nodes', 'features', 'closed')
EPS_coord_coincide = 1E-8
def __init__(self, nodes=None, llist=None, closed=None):
"""
Initialize a multi-straight-line 2D entity.
Parameters
----------
nodes : list, optional
Points (Point2d) forming the polyline chain.
llist : list, optional
List of Sline2d (straight line 2D) objects forming the polyline.
closed : bool, optional
If True, polyline is closed; if False, polyline is open.
"""
self.lines = llist
self.nodes = nodes
self.closed = closed
self.features = {'neigh_gids': None}
def __repr__(self):
"""
Return string representation of multi-straight-line object.
Returns
-------
str
Concise representation including number of lines, object ID, and
start/end node coordinates.
"""
return f"MSL2. nln={len(self.lines)}. ID: {id(self)}: {self.nodes[0]}, {self.nodes[-1]}"
def __iter__(self):
"""
Return an iterable of the line segments in self.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d
from upxo.geoEntities.sline2d import Sline2d
lines = [Sline2d(0.0,0.0, 1.0,1.0), Sline2d(1.0,1.0, 1.5,1.5),
Sline2d(1.5,1.5, 2.5,2.5), Sline2d(2.5,2.5, 4.0,4.0),
Sline2d(4.0,4.0, 4.0,6.0)]
MULLINE = MSline2d.from_lines(lines, close=True)
lines = [line for line in MULLINE.lines]
print(lines)
"""
return iter(self.lines)
def __getitem__(self, i):
"""
Return the line at index ``i``.
Parameters
----------
i : int
Zero-based index into ``self.lines``.
Returns
-------
Sline2d
Line segment at position ``i``.
Raises
------
ValueError
If ``i`` exceeds the number of lines.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d
from upxo.geoEntities.sline2d import Sline2d
lines = [Sline2d(0.0,0.0, 1.0,1.0), Sline2d(1.0,1.0, 1.5,1.5),
Sline2d(1.5,1.5, 2.5,2.5), Sline2d(2.5,2.5, 4.0,4.0),
Sline2d(4.0,4.0, 4.0,6.0)]
MULLINE = MSline2d.from_lines(lines, close=True)
MULLINE[4]
"""
if i >= self.nlines:
raise ValueError('Index exceeds maximum number of lines.')
return self.lines[i]
[docs]
@classmethod
def from_lines(cls, llist, close=True):
"""
Construct a multi-straight-line 2D from a list of Sline2d objects.
Parameters
----------
llist : list of Sline2d
Ordered line segments forming the polyline chain.
close : bool, optional
If True, append a closing segment from the last node back to the
first node.
Returns
-------
MSline2d
Assembled polyline.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
e0 = sl2d(0.0, 0.0, 1.0, 1.0)
e1 = sl2d(1.0, 1.0, 1.5, 1.5)
e2 = sl2d(1.5, 1.5, 2.5, 2.5)
e3 = sl2d(2.5, 2.5, 4.0, 4.0)
e4 = sl2d(4.0, 4.0, 4.0, 6.0)
me_closed = msl2d.from_lines([e0, e1, e2, e3, e4], close=True)
me_open = msl2d.from_lines([e0, e1, e2, e3, e4], close=False)
"""
llist, closed = llist, False
nodes = [line.pnta for line in llist]
nodes.append(llist[-1].pntb)
if close:
fl = llist[0] # First line
ll = llist[-1] # Last line
llist.append(sl2d(ll.x1, ll.y1, fl.x0, fl.y0))
nodes.append(nodes[0])
closed = True
return cls(nodes=nodes, llist=llist, closed=closed)
[docs]
@classmethod
def by_nodes(cls, nodes, close=True):
"""
Construct a multi-straight-line 2D from a list of Point2d nodes.
Parameters
----------
nodes : list of Point2d
Ordered nodes of the polyline.
close : bool, optional
If True, append a closing segment.
Returns
-------
MSline2d
Assembled polyline.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d
from upxo.geoEntities.point2d import Point2d
nodes = [Point2d(0, 0), Point2d(1, 1), Point2d(2, 2),
Point2d(3, 3), Point2d(5, 5)]
MSline2d.by_nodes(nodes).lines
"""
if type(nodes) not in ITERABLES:
raise ValueError('Invalid nodes input.')
if len(nodes) < 2:
raise ValueError('Invalid nodes iterable length. Must be >= 2.')
if type(close) != bool:
raise ValueError('Invalid close input.')
# ----------------------------------------
llist = [sl2d(nodes[i].x, nodes[i].y, nodes[i+1].x, nodes[i+1].y)
for i in range(len(nodes)-1)]
nodes = [line.pnta for line in llist]
# print(llist[-1].pntb)
# print(nodes)
nodes.append(llist[-1].pntb)
if close:
llist.append(sl2d(nodes[-1].x, nodes[-1].y,
nodes[0].x, nodes[0].y))
nodes.append(nodes[0])
return cls(nodes=nodes, llist=llist, closed=close)
[docs]
@classmethod
def by_coords(cls, coords, close=True):
"""
Construct a multi-straight-line 2D from a sequence of (x, y) coordinates.
Parameters
----------
coords : array-like of shape (n, 2)
Ordered (x, y) coordinate pairs.
close : bool, optional
If True, append a closing segment.
Returns
-------
MSline2d
Assembled polyline.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d
import numpy as np
coords = np.array([(0,0), (1,1), (2,2), (3,3), (5,5)])
msl = MSline2d.by_coords(coords)
msl.lines
msl.nodes
"""
if type(coords) not in ITERABLES:
raise ValueError('Invalid nodes input.')
if len(coords) < 2:
raise ValueError('Invalid nodes iterable length. Must be >= 2.')
# ----------------------------------------
llist = [sl2d(coords[i][0], coords[i][1], coords[i+1][0], coords[i+1][1])
for i in range(len(coords)-1)]
nodes = [line.pnta for line in llist]
if close:
llist.append(sl2d(nodes[-1].x, nodes[-1].y,
nodes[0].x, nodes[0].y))
nodes.append(nodes[0])
return cls(nodes=nodes, llist=llist, closed=close)
[docs]
@classmethod
def by_walk(self, var_l='constant', var_ang='constant',
specs = {'n': 5,
'max_total_length': 10,
'min_total_length': 8,
'mean_length': 1,}
):
"""
Generate polyline by "walking" with variable step length/angle.
Parameters
----------
var_l : str, optional
Length variation type (e.g., 'constant', 'random').
var_ang : str, optional
Angle variation type (e.g., 'constant', 'random').
specs : dict, optional
Specifications dict with keys: 'n' (count), 'max_total_length',
'min_total_length', 'mean_length'.
Notes
-----
To be developed.
"""
raise NotImplementedError("by_random_walk is not yet implemented.")
@property
def nlines(self):
"""Return number of lines."""
return len(self.lines)
@property
def centroid(self):
"""
Compute and return centroid of the polyline.
Returns
-------
numpy.ndarray
Mean coordinate (x, y) of all nodes.
"""
return self.get_node_coords().mean(axis=0)
@property
def centroid_p2dl(self):
"""
Return centroid as p2d_leanest point object.
Returns
-------
p2d_leanest
Lightweight point object at polyline centroid.
"""
return p2d_leanest(*self.centroid)
@property
def length(self):
"""
Return total length of all lines in polyline.
Returns
-------
float
Sum of all individual line lengths.
"""
return sum([line.length for line in self.lines])
@property
def lengths(self):
"""
Return individual line lengths as list.
Returns
-------
list
Lengths of each line in the polyline.
"""
return [line.length for line in self.lines]
@property
def nnodes(self):
"""Return number of lines."""
return len(self.nodes[:-1]) if self.closed else len(self.nodes)
@property
def coords(self):
"""
Get all node coordinates as 2D array.
Returns
-------
numpy.ndarray
Array of shape (n_nodes, 2) with (x, y) coordinates.
"""
return np.array([[p.x, p.y] for p in self.nodes])
@property
def tree(self):
"""
Build spatial KD-tree index for neighbor queries.
Returns
-------
scipy.spatial.cKDTree
KD-tree constructed from node coordinates.
"""
return cKDTree(self.coords)
@property
def length_mean(self):
"""
Return mean length of all line segments.
Returns
-------
float
Total length divided by number of lines.
"""
return self.length/self.nlines
@property
def gradients(self):
"""
Return gradient of every line.
"""
return [line.gradient for line in self.lines]
@property
def get_nodes(self):
"""Return unique list of nodes in the multi-straight-line object."""
nodes = [[line.x0, line.y0] for line in self.lines]
nodes += [[line.x1, line.y1] for line in self.lines]
return np.unique(nodes, axis=0)
[docs]
def get_node_coords(self):
"""
Extract and return all node coordinates as array.
Returns
-------
numpy.ndarray
Array of shape (n_nodes, 2) with (x, y) coordinates of each node.
"""
return np.array([[node.x, node.y] for node in self.extract_nodes()])
@property
def mid_nodes(self):
"""
Return midpoint nodes of all lines.
Returns
-------
list
Mid-point of each line segment in the polyline.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
MULLINE = msl2d.from_lines(lines, close=True)
MULLINE.mid_nodes
MULLINE.centroid_p2dl
"""
return [line.mid_point for line in self.lines]
@property
def line_ids(self):
"""Return memory id of each line."""
return [id(line) for line in self.lines]
[docs]
def flip(self, saa=True, throw=False):
"""
Reverse the order of lines in the polyline.
Parameters
----------
saa : bool, optional
If True, modify self in place (save-and-apply). If False, operate
on a deep copy.
throw : bool, optional
If True, return the (possibly copied) result.
Returns
-------
MSline2d or None
Flipped polyline if ``throw`` is True, else ``None``.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
MULLINE = msl2d.from_lines(lines, close=True)
MULLINE.flip()
MULLINE.lines
MULLINE.nodes
"""
if saa:
self.lines = list(np.flip(self.lines, axis=0))
for line in self.lines:
line.flip()
self.update_nodes()
if throw:
return self
if not saa:
mulslines2d = deepcopy(self)
mulslines2d.lines = list(np.flip(mulslines2d.lines, axis=0))
for line in mulslines2d.lines:
line.flip()
mulslines2d.update_nodes()
if throw:
return mulslines2d
[docs]
def do_i_precede(self, multisline2d):
"""
Check if self spatially precedes the input multisline2d.
Parameters
----------
multisline2d : MSline2d
Candidate successor polyline.
Returns
-------
i_precede : bool
True if self immediately precedes ``multisline2d``.
flip_needed : bool
True if ``multisline2d`` must be flipped to achieve nodal
continuity with self.
"""
condition_a = self.nodes[-1].eq_fast(multisline2d.nodes[0])[0]
condition_b = self.nodes[-1].eq_fast(multisline2d.nodes[-1])[0]
if condition_a:
i_precede, flip_needed = True, False
if condition_b:
i_precede, flip_needed = True, True
if not condition_a and not condition_b:
i_precede, flip_needed = False, False
return i_precede, flip_needed
[docs]
def do_i_proceed(self, multisline2d):
"""
Check if self spatially comes after the input multisline2d.
Parameters
----------
multisline2d : MSline2d
Candidate predecessor polyline.
Returns
-------
i_proceed : bool
True if self immediately succeeds ``multisline2d``.
flip_needed : bool
True if ``multisline2d`` must be flipped to achieve nodal
continuity with self.
"""
condition_a = self.nodes[0].eq_fast(multisline2d.nodes[0])[0]
condition_b = self.nodes[0].eq_fast(multisline2d.nodes[-1])[0]
if condition_a:
i_precede, flip_needed = True, False
if condition_b:
i_precede, flip_needed = True, True
if not condition_a and not condition_b:
i_precede, flip_needed = False, False
return i_precede, flip_needed
[docs]
def is_adjacent(self, multisline2d):
"""
Check spatial adjacency with another multiline.
Parameters
----------
multisline2d : MSline2d
Another multi-straight-line object to test.
Returns
-------
tuple
(is_adjacent_bool, left_relation, right_relation) tuple.
Notes
-----
Checks if multiline is immediately preceded or succeeded by input.
"""
left = self.do_i_proceed(multisline2d) # msl2d is to the left of self
right = self.do_i_precede(multisline2d) # msl2d is to the rght of self
return any(left[0], right[0]), left, right
[docs]
def find_spatially_next_multisline2d(self, multislines2d):
"""
From a list of multisline2d objects, multislines2d, find the ones which
come spatially immediately after self.
"""
precedes = []
for msl in multislines2d:
precedes.append(self.do_i_precede(msl))
return precedes
[docs]
def has_coord(self, coord, return_flags=False):
"""
Check if coordinate exists in polyline node sequence.
Parameters
----------
coord : array-like
2D coordinate (x, y) to search for.
return_flags : bool, optional
If True, return boolean flags array.
Returns
-------
bool or (bool, numpy.ndarray)
If return_flags=False: boolean indicating existence.
If return_flags=True: (existence_bool, flags_array) tuple.
"""
# Validations: coord must be np.array 2D coordfinate
_coords_ = self.get_node_coords()
flags = (_coords_[:, 0] == coord[0]) & (_coords_[:, 1] == coord[1])
if return_flags:
return any(flags), flags
else:
return any(flags)
[docs]
def find_coord_location(self, coord):
"""
Find index location of coordinate in node sequence.
Parameters
----------
coord : array-like
2D coordinate (x, y) to search for.
Returns
-------
numpy.ndarray or None
Index array if coordinate found, None if not found.
"""
# Validations: coord must be np.array 2D coordfinate
exists, flags = self.has_coord(coord, return_flags=True)
if exists:
return np.argwhere(flags)
else:
return None
[docs]
def update_nodes(self):
"""
Update internal node list from current line definitions.
Notes
-----
Reconstructs self.nodes from line start/end points, respecting
open/closed topology.
"""
self.nodes = [line.pnta for line in self.lines]
if self.closed:
self.nodes.append(self.nodes[0])
else:
self.nodes.append(self.lines[-1].pntb)
[docs]
def close(self, reclose=False):
"""
Close the self multi-straight line object.
Examples
--------
a.
"""
if not self.closed:
fl, ll = self[0], self[-1]
self.lines.append(sl2d(ll.x1, ll.y1, fl.x0, fl.y0))
else:
if not reclose:
# Nothing to do here as self already closed and recvlose is
# False
pass
if reclose:
fl, ll = self[0], self[-1]
self.lines.append(sl2d(ll.x1, ll.y1, fl.x0, fl.y0))
[docs]
def unclose(self):
"""Remove the closing line."""
del self.lines[-1]
[docs]
def distances_nodes(self, points):
"""
Compute distances from every node to each given point.
Parameters
----------
points : numpy.ndarray of shape (m, 2)
Query points.
Returns
-------
numpy.ndarray of shape (n_nodes, m)
Euclidean distance from each node to each query point.
Notes
-----
Uses vectorised broadcasting: ``points[:, np.newaxis]`` is broadcast
against the node array so all pairwise distances are computed in one
operation.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
import numpy as np
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
MULLINE = msl2d.from_lines(lines, close=True)
points = np.random.random((2, 2))
MULLINE.distances_nodes(points)
"""
# Validation
# -------------------------------------
nodes = np.array(self.nodes)
"""
nodes = np.random.random((4, 2))
points = np.random.random((10,2))
distances = np.sqrt(np.sum((points[:, np.newaxis] - nodes) ** 2,
axis=2))
points[:, np.newaxis] broadcasts the points array to have shape
(10, 1, 3), effectively creating a third dimension for broadcasting.
points[:, np.newaxis] - nodes broadcasts nodes to shape (10, 4, 3) and
subtracts each node from each point, resulting in an array of
differences.
np.sum(... ** 2, axis=2) squares each difference element-wise, sums
along the third axis (axis=2), and then takes the square root to get
the Euclidean distances.
"""
distances = np.sqrt(np.sum((points[:, np.newaxis] - nodes) ** 2,
axis=2)).T
return distances
[docs]
def find_closest_nodes(self, point):
"""
Find the closest node index (or indices) to a query point.
Parameters
----------
point : array-like of shape (2,)
Query point coordinates.
Returns
-------
int or list of int
Index of the closest node, or a list of indices if multiple nodes
are equidistant.
Examples
--------
**Example 1** — random query point:
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
import numpy as np
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5)]
MULLINE = msl2d.from_lines(lines, close=True)
point = np.random.random(2) * np.random.randint(10)
MULLINE.find_closest_nodes(point)
**Example 2** — midpoint of first line:
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5)]
MULLINE = msl2d.from_lines(lines, close=True)
point = lines[0].mid
MULLINE.find_closest_nodes(point)
"""
# Validation
# -------------------------------------
distances = self.distances_nodes(np.array(point)[:, np.newaxis].T).T.squeeze()
closest_points = np.argwhere(distances == distances.min()).T.squeeze().tolist()
return closest_points
[docs]
def add_nodes(self, nodes):
"""
Insert new nodes into the polyline by splitting existing lines.
Parameters
----------
nodes : list of Point2d
New nodes to insert. Each node is located on an existing line
segment and splits it.
Notes
-----
If a node does not lie on any line segment it is silently ignored.
The internal node list is updated after each insertion.
Examples
--------
**Example 1** — insert two nodes on a 5-segment polyline:
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
from upxo.geoEntities.point2d import Point2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
mulline = msl2d.from_lines(lines, close=True)
mulline.add_nodes([Point2d(0.5, 0.5), Point2d(2.0, 2.0)])
mulline.lines
**Example 2** — insert three nodes on a 6-segment polyline:
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
from upxo.geoEntities.point2d import Point2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,5.0,4.0), sl2d(5.0,4.0,5.0,0.0)]
mulline = msl2d.from_lines(lines, close=True)
mulline.add_nodes([Point2d(0.5, 0.5), Point2d(2.0, 2.0),
Point2d(1.75, 0)])
mulline.lines
"""
for node in nodes:
line_indices = []
for i, line in enumerate(self.lines, start=0):
if line.fully_contains_point(p2d=node, method='through'):
line_indices.append(i)
if len(line_indices) != 0:
line = self.lines[line_indices[0]]
new_line = line.split(method='p2d', divider=node,
saa=True, throw=True, update='pntb')[1]
self.lines.insert(line_indices[0]+1, new_line)
self.update_nodes()
[docs]
def splice_nodes_and_lines(self, method='points', points=None, perform_checks=True):
"""
Splice/interpolate nodes and lines at specified points.
Parameters
----------
method : str, optional
Method for splicing. Options include 'points' (default).
points : list, optional
Points at which to splice.
perform_checks : bool, optional
Whether to perform validation checks.
Notes
-----
To be developed.
"""
raise NotImplementedError("splice is not yet implemented.")
[docs]
def roll(self, roll_distance):
"""
Roll/rotate line order within the polyline chain.
Parameters
----------
roll_distance : int
Number of positions to roll lines.
Notes
-----
Updates internal node list after rolling. Useful for changing
polyline starting point while preserving connectivity.
"""
self.lines = list(np.roll(self.lines, -roll_distance))
self.update_nodes()
[docs]
def sub_divide(self, line_number=0, f=0.5):
"""
Sub-divide a single line in ``self.lines`` at a fractional position.
Parameters
----------
line_number : int, optional
Index in ``self.lines`` to subdivide (zero-based).
f : float, optional
Fractional position along the target line in ``(0, 1)`` where the
split is made.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
me = msl2d.from_lines(lines, close=False)
me.sub_divide(line_number=0, f=0.25)
me.sub_divide(line_number=3, f=0.50)
for i in range(5):
me.sub_divide(line_number=i, f=0.50)
me.lines
"""
if not isinstance(line_number, int):
raise TypeError('Invalid line number type.')
if line_number > len(self.lines):
raise ValueError('Invalid line number spacification.')
if line_number == 0:
new_lines = self.lines[0].split(f=f, saa=False, throw=True)
self.lines = new_lines + self.lines[1:]
elif line_number == len(self.lines):
new_lines = self.lines[-1].split(f=f, saa=False, throw=True)
self.lines = self.lines[:-1] + new_lines
else:
# end result: left--line0--line1--right
left = [line for line in self.lines[:line_number]]
# Divisiozn operation stzrts
line01 = self.lines[line_number].split(f=f, saa=False, throw=True)
# Division operation ends
right = [line for line in self.lines[line_number+1:]]
self.lines = left + line01 + right
[docs]
def remove_point_by_index(self, index=2, remove='previous_line'):
"""
Remove a node at the given index by merging its adjacent line segments.
Parameters
----------
index : int, optional
Zero-based node index to remove.
remove : {'previous_line', 'next_line', 'both'}
Which adjacent line to delete:
``'previous_line'`` — extend the next line backward;
``'next_line'`` — extend the previous line forward;
``'both'`` — replace both adjacent lines with a new direct line.
Examples
--------
**Example 1** — remove via previous line:
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
MULLINE = msl2d.from_lines(lines, close=True)
MULLINE.remove_point_by_index(index=2, remove='previous_line')
MULLINE.lines
**Example 2** — remove via next line:
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
MULLINE = msl2d.from_lines(lines, close=True)
MULLINE.remove_point_by_index(index=2, remove='next_line')
MULLINE.lines
**Example 3** — remove both adjacent lines:
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
MULLINE = msl2d.from_lines(lines, close=True)
MULLINE.remove_point_by_index(index=2, remove='both')
MULLINE.lines
"""
# Validations
# -------------------------------------
previous_line, next_line = index-1, index
# -------------------------------------
if remove == 'previous_line':
# Next line will be updayed and previous line will be removed
self.lines[next_line].move_i(self.lines[previous_line].coord_i)
del self.lines[previous_line]
elif remove == 'next_line':
# Next line will be removed and previous line will be updated
self.lines[previous_line].move_j(self.lines[next_line].coord_j)
del self.lines[next_line]
elif remove == 'both':
# Next line and previous line will be removed and a new line will
# be made in its place
x0, y0 = self.lines[previous_line].coord_i
x1, y1 = self.lines[next_line].coord_j
new_line = sl2d(x0, y0, x1, y1)
self.lines = self.lines[:previous_line] + [new_line] + self.lines[next_line+1:]
else:
raise ValueError('Invalid update specirfication.')
[docs]
def remove_point_by_location(self, location=(None, None, None),
remove='previous_line'):
"""
Remove the node closest to a given location.
Parameters
----------
location : array-like of shape (2,)
Query coordinates used to find the closest node.
remove : {'previous_line', 'next_line', 'both'}
Passed through to :meth:`remove_point_by_index`.
Notes
-----
If multiple nodes tie for closest, all are removed recursively.
When only 2 lines remain and the closing line is redundant, the
closing line is automatically deleted.
Examples
--------
**Example 1** — remove by endpoint coordinate:
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
MULLINE = msl2d.from_lines(lines, close=True)
MULLINE.remove_point_by_location(location=lines[0].coord_i,
remove='previous_line')
MULLINE.lines
**Example 2** — remove by midpoint coordinate:
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
MULLINE = msl2d.from_lines(lines, close=True)
MULLINE.remove_point_by_location(location=lines[0].mid,
remove='previous_line')
MULLINE.lines
**Example 3** — remove by random location:
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
import numpy as np
lines = [sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5), sl2d(2.5,2.5,4.0,4.0),
sl2d(4.0,4.0,4.0,6.0)]
MULLINE = msl2d.from_lines(lines, close=True)
location = np.random.random(2) * np.random.randint(10)
MULLINE.remove_point_by_location(location=location,
remove='previous_line')
MULLINE.lines
"""
# Validations
# -------------------------------------
if self.n == 1:
"""
When there is a single line in the multi-line, no point can
be removed. Function call will exit.
"""
return
# -------------------------------------
indices = self.find_closest_nodes(location)
# -------------------------------------
if isinstance(indices, int):
self.remove_point_by_index(index=indices, remove=remove)
elif isinstance(indices, list) and len(indices) > 1:
self.remove_point_by_index(index=indices[0], remove=remove)
for i, index in enumerate(indices[1:]):
self.remove_point_by_location(location=location, remove=remove)
# print('Multiple nodes were removed.')
# -------------------------------------
if self.n == 2:
"""
In some cases, only 2 lines remain, of which the second one will
usually be the closing line. So, its just the closing line sitting
on top of original lines. As the closing line, in this case, jhas
the same 'end'-points but is just flipped in direction, they both
are essentially the same lines. In this case, the closing line
will be removed after the chack has confirmed the existence of
such a closing line.
"""
# Check for equal length
point0, point1 = self.lines[1].coord_list
same_endpoints = [self.lines[0].is_point_endpoint(point0),
self.lines[0].is_point_endpoint(point1),
]
if all(same_endpoints):
"""Retain the original lines and trim the closing line."""
del self.lines[1]
[docs]
def plot(self, ax=None, connect_ends=False):
"""
Plot polyline on matplotlib axes.
Parameters
----------
ax : matplotlib.axes.Axes, optional
Axes object to plot on. If None, creates new figure/axes.
connect_ends : bool, optional
If True, draw line connecting polyline start and end points.
Returns
-------
matplotlib.axes.Axes
Axes object with plotted polyline.
Examples
--------
>>> msl = MSline2d.by_coords([(0, 0), (1, 1), (2, 0)])
>>> ax = msl.plot()
"""
coords = self.get_node_coords()
if ax is None:
fig, ax = plt.subplots()
ax.plot(coords[:, 0], coords[:, 1], '-o')
if connect_ends:
if self.closed and len(coords) > 2:
ax.plot(coords[0, :], coords[-2, :], '-o')
elif not self.closed and len(coords) > 1:
ax.plot(coords[0, :], coords[-1, :], '-o')
return ax
[docs]
def check_overlaping_points(self, tolerance=1E-8):
"""
Check for overlapping/duplicate nodes within tolerance.
Parameters
----------
tolerance : float, optional
Maximum distance for considering points duplicates.
Default: 1E-8.
Returns
-------
bool
True if overlapping points found, False otherwise.
"""
coords = self.get_node_coords()
d = cdist(coords, coords)
if np.argwhere(d <= tolerance).shape[0] > coords.shape[0]:
return True
else:
return False
[docs]
def check_overlaping_lines(self):
"""
Check for overlapping line segments in polyline.
Returns
-------
bool or None
True if overlapping lines detected, None if to be completed.
Notes
-----
To be developed. Current implementation collects midpoints and
gradients but does not perform overlap checks.
"""
midpoints = [line.mid_point for line in self.lines]
gradients = [line.gradient for line in self.lines]
[docs]
def smooth(self, max_smooth_level=2):
"""
Smooth the polyline by replacing node coordinates with local means.
Parameters
----------
max_smooth_level : int, optional
Number of smoothing passes applied via
:func:`~upxo._sup.data_ops.mean_coordinates`.
Notes
-----
Terminal nodes (first and last) are preserved. If fewer than 3 nodes
are present, no smoothing is performed.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d as msl2d
from upxo.geoEntities.point2d import Point2d
nodes = [Point2d(0.0, 0.0), Point2d(1.0, 0.0),
Point2d(1.0, 1.0), Point2d(2.5, 2.0),
Point2d(4.0, 2.0), Point2d(4.0, 6.0),
Point2d(4.0, 8.0), Point2d(2.0, 8.0)]
ml = msl2d.by_nodes(nodes, close=False)
ax = ml.plot()
ml.smooth(max_smooth_level=3)
ax.plot(ml.coords[:, 0], ml.coords[:, 1])
"""
# ---------------------------------------------------
smoothing_carried_out = True
if self.nnodes > 4:
coords = mean_coordinates(self.coords, max_smooth_level)
elif self.nnodes == 4:
coords = mean_coordinates(self.coords, 3)
elif self.nnodes == 3:
coords = mean_coordinates(self.coords, 2)
else:
smoothing_carried_out = False
# ---------------------------------------------------
if smoothing_carried_out:
new_nodes = [Point2d(c[0], c[1]) for c in coords[1:-1]]
new_nodes_full = [self.nodes[0]] + new_nodes + [self.nodes[-1]]
new_lines = [sl2d(new_nodes_full[i].x, new_nodes_full[i].y,
new_nodes_full[i+1].x, new_nodes_full[i+1].y)
for i in range(len(new_nodes_full)-1)]
# ---------------------------------------------------
self.lines = new_lines
self.nodes = new_nodes_full
[docs]
class ring2d():
"""
Ring structure in 2D—closed polyline representation from multiple segments.
This class manages a collection of connected MSline2d (multi-straight-line)
segments that form a closed ring/loop structure. It handles segment ordering,
flipping, spatial continuity checks, and polygon conversion.
Attributes
----------
segments : list
Collection of MSline2d objects forming the ring.
segids : list
Segment identifiers.
segflips : list
Boolean flags indicating if segments were flipped for continuity.
nsegs : int
Number of segments in the ring.
closed : bool
Whether ring is topologically closed.
conn0 : bool
First-order connectivity status (start-end closure).
conn1 : dict
Second-order connectivity status (inter-segment continuity).
Notes
-----
``ring2d`` is in active development. Closure enforcement, reordering, and
polygon generation are implemented; polygon meshing is not yet available.
Examples
--------
.. code-block:: python
from upxo.geoEntities.mulsline2d import MSline2d, ring2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
msl1 = MSline2d.from_lines(
[sl2d(0.0,0.0,1.0,1.0), sl2d(1.0,1.0,1.5,1.5),
sl2d(1.5,1.5,2.5,2.5)], close=False)
msl2 = MSline2d.from_lines(
[sl2d(2.5,2.5,4.0,4.0), sl2d(4.0,4.0,4.0,6.0)], close=False)
msl3 = MSline2d.from_lines(
[sl2d(4.0,6.0,4.0,8.0), sl2d(4.0,8.0,10.0,10.0)], close=False)
msl4 = MSline2d.from_lines(
[sl2d(0,0,20,10), sl2d(20,10,10,10)], close=False)
R = ring2d([msl1, msl2, msl3, msl4])
R.assess_spatial_continuity()
"""
__slots__ = ('segments', 'segids', 'segflips', 'nsegs',
'coords', 'closed', 'conn0', 'conn1')
EPS_coord_coincide = 1E-8
def __init__(self, segments=None, segids=None, segflips=None):
"""
Initialize ring2d from segment collection.
Parameters
----------
segments : list, optional
List of MSline2d objects forming the ring.
segids : list, optional
Segment identifiers.
segflips : list, optional
Boolean flags indicating segment flip status.
"""
self.segments = segments
self.segids = segids
self.segflips = segflips
self.nsegs = len(self.segments)
#self.set_coords()
# -----------------------
#_closure_level_0_ = self.connectivity0()
#self.conn0, last_seg_flipped, flip_possible = _closure_level_0_
## -----------------------
#if self.conn0 or flip_possible:
# self.connectivity1()
# -----------------------
# . . . . . . .
'''
overlaps = self.assess_segment_point_overlaps(segments) # False: Proceed
continuities, flips = self.assess_spatial_continuity(segments)
if not all(continuities):
if self.assess_possibility_of_continuity(segments):
self.get_continuity_enforcement_indices()
self.set_spatial_continuity(enforce_indices)
else:
raise ValueError('Invalid segments morphology passed.')'''
# --------------------------------------
# NOw that validations have been done, we will proceed.
def __repr__(self):
"""
Return string representation of ring2d object.
Returns
-------
str
Brief representation with segment count and object ID.
"""
return f'UPXO ring. nseg={len(self.segments)}. MID: {id(self)}'
[docs]
def add_segment_unsafe(self, segment):
"""
Add segment to ring without validation.
Parameters
----------
segment : MSline2d
Segment to append (no continuity checks).
"""
self.segments.append(segment)
[docs]
def add_segid(self, segid):
"""
Add segment identifier to tracking list.
Parameters
----------
segid : hashable
Segment identifier to track.
"""
self.segids.append(segid)
[docs]
def add_segflip(self, segflip):
"""
Add segment flip status flag.
Parameters
----------
segflip : bool
True if segment was flipped, False otherwise.
"""
self.segflips.append(segflip)
[docs]
def check_closed(self):
"""
Check if ring is topologically closed.
Returns
-------
bool
True if end node of last segment connects to start node of first
segment (within EPS_coord_coincide tolerance).
"""
if self.segflips[self.segids.index(max(self.segids))]:
START = self.segments[0].nodes[0]
END = self.segments[max(self.segids)].nodes[-1]
else:
START = self.segments[0].nodes[0]
END = self.segments[max(self.segids)].nodes[0]
return START.eq_fast(END)[0]
'''startseg, endseg = min(self.segids), max(self.segids)
segflip = self.segflips[self.segids.index(endseg)]
# ------------------------
startnode = self.segments[startseg].nodes[0]
if segflip:
endnode = self.segments[endseg].nodes[-1]
else:
endnode = self.segments[endseg].nodes[0]
return startnode.eq_fast(endnode)'''
[docs]
def close(self):
"""
Enforce topological closure by connecting end to start.
Notes
-----
Adds first segment's start node as last segment's end node and
updates internal node list.
"""
self.segments[-1].nodes.append(self.segments[0].nodes[0])
self.segments[-1].update_nodes()
self.closed = True
[docs]
def connectivity0(self, flip_if_possible=True):
'''
Assess closure of 1st & last segment with option to close if possible.
Return
------
closed: True, if closd or has been closed.
last_seg_flipped: True, if seg was originally found open but closed.
flip_possible: Whether flipped or not, if True, indicates that
the segments can be closed between first and last ones.
'''
closed, last_seg_flipped, flip_possible = False, False, False
if self.segments[0].nodes[0].eq_fast(self.segments[-1].nodes[-1])[0]:
closed = True
elif self.segments[0].nodes[0].eq_fast(self.segments[-1].nodes[0])[0]:
flip_possible = True
if flip_if_possible:
self.segments[-1].flip()
closed, last_seg_flipped = True, True
return closed, last_seg_flipped, flip_possible
[docs]
def connectivity1(self):
'''
Assess closure of all intermediate segments.
'''
self.conn1 = {}
for i in range(self.nsegs):
if i < self.nsegs-1:
c = self.segments[i].nodes[-1].eq_fast(self.segments[i+1].nodes[0])[0]
self.conn1[(i, i+1)] = c
else:
c = self.segments[i].nodes[-1].eq_fast(self.segments[0].nodes[0])[0]
self.conn1[(i, 0)] = c
[docs]
def assess_segment_point_overlaps(self, line_check=False):
"""
Check all segments for duplicate/overlapping nodes.
Parameters
----------
line_check : bool, optional
If True, also check for overlapping lines (not yet operational).
Returns
-------
list
Boolean list for each segment indicating overlap presence.
"""
ol_points = [seg.check_overlaping_points() for seg in self.segments]
# ---------------------------
line_check=False # Currently not operational.
if line_check:
ol_lines = [seg.check_overlaping_lines() for seg in self.segments]
[docs]
def assess_reorder_requirement(self):
"""
Check if reordering or flipping of segments is needed.
Notes
-----
To be developed. Currently evaluates closure status and potential
flips needed to enforce continuity.
"""
if self.check_closed():
self.close()
# NO re-ordering needed
self.check_sorted()
pass
else:
# Flips may be needed
pass
[docs]
def set_coords(self):
"""
Build and cache coordinate array from all segments.
Notes
-----
Combines node coordinates of all segments, skipping duplicate
nodes at segment junctions.
"""
self.coords = self.segments[0].get_node_coords()
for seg in self.segments[1:]:
self.coords = np.vstack((self.coords, seg.get_node_coords()[1:]))
[docs]
def get_coords(self):
"""
Extract and return full coordinate array from ring.
Returns
-------
numpy.ndarray
Coordinates of all ring nodes.
"""
return self.create_coords_from_segments()
@property
def centroid(self):
"""
Compute centroid of the ring shape.
Returns
-------
numpy.ndarray
Mean (x, y) coordinate of all ring nodes.
"""
return np.mean(self.get_coords(), axis=0)
[docs]
def create_coords_from_segments(self, force_close=False):
"""
Build coordinate array from all segments, respecting flips.
Parameters
----------
force_close : bool, optional
If True, append first coordinate to end to ensure closure.
Returns
-------
numpy.ndarray
Coordinates from all segments, concatenated in order.
"""
# segments = gbsegs
coords = self.segments[0].get_node_coords()
for i, seg in enumerate(self.segments[1:], start=1):
if self.segflips[i]:
thissegcoords = np.flip(seg.get_node_coords(), axis=0)
coords = np.vstack((coords, thissegcoords[1:]))
else:
coords = np.vstack((coords, seg.get_node_coords()[1:]))
if force_close:
coords = self.force_close_coordinates(coords, assess_first=True)
return coords
[docs]
def force_close_coordinates(self, coord, assess_first=True):
"""
Ensure coordinate array is closed (end connects to start).
Parameters
----------
coord : numpy.ndarray
Input coordinate array.
assess_first : bool, optional
If True, check if already closed before forcing.
Returns
-------
numpy.ndarray
Closed coordinate array.
Notes
-----
Unsafe. Not intended for user. Uses EPS_coord_coincide tolerance.
"""
if assess_first:
if abs((coord[0]-coord[-1]).sum()) > self.EPS_coord_coincide:
print('Coord not closed. Force closing.')
coord = np.vstack((coord, coord[0]))
else:
pass
else:
coord = np.vstack((coord, coord[0]))
return coord
[docs]
def create_polygon_from_segments(self):
"""
Convert ring to Shapely Polygon object.
Returns
-------
shapely.geometry.Polygon
Polygon representation of the ring.
"""
coords = self.create_coords_from_segments()
return Polygon(self.create_coords_from_segments())
[docs]
def create_polygon_from_coords(self):
"""
Create Shapely Polygon from coordinates.
Returns
-------
shapely.geometry.Polygon
Polygon from extracted ring coordinates.
"""
return Polygon(self.create_coords_from_segments())
@property
def area(self):
"""
Compute area enclosed by ring polygon.
Returns
-------
float
Area of ring polygon.
"""
return self.create_polygon_from_segments().area
@property
def perimeter(self):
"""
Compute perimeter (boundary length) of ring.
Returns
-------
float
Total perimeter of ring polygon.
"""
return self.create_polygon_from_segments().length
@property
def is_closed(self):
"""
Check if ring is topologically closed.
Returns
-------
bool
True if ring forms a closed loop.
"""
return self.check_closed()
@property
def nsegments(self):
"""
Return number of segments in ring.
Returns
-------
int
Count of constituent MSline2d segments.
"""
return self.nsegs
@property
def ncoords(self):
"""
Return total number of nodes in ring.
Returns
-------
int
Count of unique coordinate nodes.
"""
return self.create_coords_from_segments().shape[0]
@property
def tree(self):
"""
Build spatial KD-tree index for ring coordinates.
Returns
-------
scipy.spatial.cKDTree
KD-tree for neighbor/distance queries on ring nodes.
"""
return cKDTree(self.create_coords_from_segments())
[docs]
def assess_spatial_continuity(self):
"""
From list of multisline2d, multislines2d, do all (i+1)^th multisline2d
follow i^th multisline2d? with or without the need for flips.
Return
------
continuity: final result, whethwr all make a chain or not, irrespective
of the need for flip.
flip_needed: True if any of the multisline2d in the list neede to be
flipped to enure spatial continuity. The actual e,ements needing
flips can be found in second element opf every
subliest in i_precede_chain.
i_precede_chain: list of do_i_precede results for every i:i+1 pair
"""
i_precede_chain = []
for i in range(len(self.segments)-1):
if i == 0:
i_precede_chain.append(self.segments[i].do_i_precede(self.segments[i+1]))
else:
thismsl = self.segments[i]
nextmsl = self.segments[i+1]
i_precede_chain.append(thismsl.do_i_precede(nextmsl))
continuity = all([flag[0] for flag in i_precede_chain])
flip_needed = any([flag[1] for flag in i_precede_chain])
return continuity, flip_needed, i_precede_chain
[docs]
def plot_segs(self,
plot_centroid=False, centroid_text='',
plot_coord_order=False,
visualize_flip_req=False):
"""
Visualize ring segments with optional annotations.
Parameters
----------
plot_centroid : bool, optional
If True, plot ring centroid marker and label.
centroid_text : str, optional
Label text for centroid.
plot_coord_order : bool, optional
If True, plot coordinate connectivity with dashed line.
visualize_flip_req : bool, optional
If True, use dashed lines for flipped segments.
Returns
-------
matplotlib.axes.Axes
Axes object with plotted segments (color-coded).
"""
fig, ax = plt.subplots()
FS = 8
for gbsegcount, gbseg in enumerate(self.segments, start=0):
coords = gbseg.get_node_coords()
color = np.random.random(3)
if not visualize_flip_req:
ax.plot(coords[:, 0], coords[:, 1], color=color,
linewidth=3)
if visualize_flip_req:
if self.segflips[gbsegcount]:
ax.plot(coords[:, 0], coords[:, 1], color=color,
linewidth=3, linestyle='--')
else:
ax.plot(coords[:, 0], coords[:, 1], color=color,
linewidth=3)
centroid = gbseg.centroid
ax.plot(centroid[0], centroid[1], 'kx')
ax.text(centroid[0], centroid[1], gbsegcount, color='red',
fontsize=12, fontweight='bold')
fs = FS + (gbsegcount % 2)*2
offset = 0.1*(gbsegcount % 2)
for coord_count, coord in enumerate(coords, start=0):
ax.text(coord[0]+offset, coord[1]+offset,
coord_count, color=color, fontsize=fs)
if plot_centroid:
centroid = self.centroid
plt.plot(centroid[0], centroid[1], 'k+',
ms=15, mfc='black', mew=2, alpha=1.0)
plt.text(centroid[0], centroid[1], str(centroid_text),
fontsize=15, color='orange', fontweight='bold')
if plot_coord_order:
C = self.create_coords_from_segments()
ax.plot(C[:,0], C[:,1], '-.k', linewidth=0.75)
return ax
[docs]
def get_coords_newdef(self):
"""
Extract coordinates in new definition format.
Returns
-------
tuple
(segment_coords_list, combined_coords) where:
- segment_coords_list: List of coordinate arrays per segment
- combined_coords: Concatenated coordinate array from all segments
"""
coordinates = []
for gbsegcount, gbseg in enumerate(self.segments, start=0):
coords = gbseg.get_node_coords()
coordinates.append(coords)
C = self.create_coords_from_segments()
return coordinates, C
[docs]
class mulring2d():
__slots__ = ('rings', 'jp', 'ip')
"""
Explanations
------------
'rings': individual ring objects
'jp': Junction points
'ip': All interface points
'ippure': Pure interface points = {ip} - {jp}
# Requirements
1. Provide a self-sustained
"""
def __init__(self, rings):
"""Initialise from a list of ring objects."""
self.rings = rings
[docs]
def build_points_list(self):
"""Build a flat list of all points from the ring collection. Not yet implemented."""
raise NotImplementedError("build_points_list is not yet implemented.")
[docs]
def set_coords(self):
"""Set coordinate arrays from the ring collection. Not yet implemented."""
raise NotImplementedError("set_coords is not yet implemented.")
[docs]
def export_abaqus_for_meshing(self):
"""Export the geometry in Abaqus mesh-input format. Not yet implemented."""
raise NotImplementedError("export_abaqus_for_meshing is not yet implemented.")
[docs]
def mesh(self):
"""Generate a mesh for the ring collection. Not yet implemented."""
raise NotImplementedError("mesh is not yet implemented.")