Source code for upxo.geoEntities.mulsline2d

"""
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 extract_nodes(self): """ Extract all unique nodes from polyline lines. Returns ------- list List of Point2d nodes (including closing node if closed). """ nodes = [line.pnta for line in self.lines] if self.closed: nodes.append(self.nodes[0]) else: nodes.append(self.lines[-1].pntb) return nodes
[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.")