Source code for upxo.geoEntities.point2d

"""
2D Point geometric entity module for UPXO (Universal PolyXtal Operations).

This module provides two-dimensional point representations with support for
lightweight (leanest) and feature-rich implementations. It includes geometric
operations (distance, translation, rotation), type conversions (UPXO, Shapely,
VTK, PyVista, GMSH), and point array generation methods.

Usage
-----
    from upxo.geoEntities.point2d import p2d_leanest
    from upxo.geoEntities.point2d import Point2d
    from upxo.geoEntities.point2d import Point2d as p2d

Classes
-------
p2d_leanest
    Minimal 2D point container (private coordinates, no features).
Point2d
    Full-featured 2D point with attachable features and extensive
    geometric operations.
_coord_
    Internal 3D coordinate storage used for z-level tracking.

Dependencies
------------
Core (module-level):
    numpy (np), math, copy.deepcopy,
    upxo._sup.dataTypeHandlers,
    upxo.geoEntities.bases,
    upxo.geoEntities.featmake,
    upxo._sup.validation_values,
    upxo.geoEntities.point3d

Optional (loaded on demand):
    numpy.matlib — used in ``array_by_angles()``.
    shapely.geometry — used in ``make_shape()`` and ``rettype='shapely'`` clustering.
    vtk — used in ``make_vtk_point()``.
    pyvista — used in clustering with ``rettype='pyvista'``.
    gmsh — used in clustering with ``rettype='gmsh'``.

Notes
-----
``p2d_leanest`` is stable and should not be extended further; use
``Point2d`` for all new feature development.

Limitations
-----------
- Pydantic validation is intentionally excluded to keep instantiation fast.
- ABC inheritance was removed to eliminate per-object overhead.

See Also
--------
upxo.geoEntities.sline2d : 2D line segments.
upxo.geoEntities.point3d : 3D points.

Examples
--------
Leanest (minimal) point:

>>> from upxo.geoEntities.point2d import p2d_leanest
>>> p = p2d_leanest(1.0, 2.0)
>>> p._x, p._y
(1.0, 2.0)

Full-featured point with distance:

>>> from upxo.geoEntities.point2d import Point2d as p2d
>>> p1, p2 = p2d(0, 0), p2d(3, 4)
>>> p1.distance(p2)
5.0

Generate a point array around a centroid:

>>> center = p2d(5, 5)
>>> points = center.array_by_clustering(n=10, r=2.0)
>>> len(points)
10

Rotate a point 90 degrees about the origin:

>>> p = p2d(1, 0)
>>> rotated = p.rotate_about_point(p2d(0, 0), 90, degree=True)

Convert to a Shapely Point:

>>> p = p2d(1, 1)
>>> sp = p.make_shape()
"""

import math
import numpy as np
from copy import deepcopy
import upxo._sup.dataTypeHandlers as dth
from upxo._sup.dataTypeHandlers import opt as OPT, strip_str as SSTR
from upxo.geoEntities.bases import UPXO_Point, UPXO_Edge
# from upxo._sup.validation_values import find_pnt_spec_type_2d
np.seterr(divide='ignore')
from upxo.geoEntities.featmake import make_p2d, make_p3d
from upxo._sup.validation_values import find_spec_of_points
from upxo._sup.validation_values import val_point_and_get_coord
from upxo._sup.validation_values import isinstance_many
import upxo.geoEntities.featmake as fmake
from upxo.geoEntities.point3d import Point3d


NUMBERS, ITERABLES = dth.dt.NUMBERS, dth.dt.ITERABLES

class _coord_():
    __slots__ = ('x', 'y', 'z')
    def __init__(self, x, y, z):
        """Store raw 3D coordinate values in ``x``, ``y``, ``z``."""
        self.x, self.y, self.z = x, y, z

[docs] class p2d_leanest(): """ Minimal 2D point container for lightweight, high-frequency operations. Stores coordinates as private attributes (``_x``, ``_y``) and provides compact distance and radius-membership checks with minimal overhead. Intended for inner-loop use where only core geometric predicates are required; use ``Point2d`` when richer features are needed. Usage ----- from upxo.geoEntities.point2d import p2d_leanest Parameters ---------- x : float X-coordinate of the point. y : float Y-coordinate of the point. Attributes ---------- _x : float X-coordinate (private). _y : float Y-coordinate (private). Limitations ----------- - This class is frozen: do not add new methods or attributes. - No feature dictionary, plane tracking, or type-conversion methods. - No tolerance-aware equality; use ``Point2d`` for that. Examples -------- >>> from upxo.geoEntities.point2d import p2d_leanest >>> a = [p2d_leanest(1, 2), p2d_leanest(3, 4)] >>> a[0]._x 1 """ __slots__ = ('_x', '_y') def __init__(self, x, y): """ Initialize lean 2D point. Parameters ---------- x, y : float Coordinates of the point. """ self._x, self._y = x, y def __repr__(self): """Return string representation of self.""" return f'Lean 2D point at ({self._x}, {self._y})'
[docs] def squared_distance_to_coord(self, x, y): """ Compute squared Euclidean distance to a coordinate. Parameters ---------- x, y : float Target coordinate. Returns ------- float Squared distance `(self._x-x)^2 + (self._y-y)^2`. """ return (self._x-x)**2 + (self._y-y)**2
[docs] def is_coord_within_cor(self, x, y, cor=1E-8, on_cor_flag=True): """ Check if a coordinate is inside or on a circle centred at self. Parameters ---------- x, y : float Coordinate to test. cor : float, optional Circle radius. on_cor_flag : bool, optional If True, include circle boundary (`<= cor`), else strict inside (`< cor`). Returns ------- bool True if coordinate satisfies circle-membership criterion. Examples -------- .. code-block:: python from upxo.geoEntities.point2d import p2d_leanest p2d_leanest(0, 0).is_coord_within_cor(0, 1, 1E-8) """ if on_cor_flag: return math.sqrt((self._x-x)**2+(self._y-y)**2) <= cor else: return math.sqrt((self._x-x)**2+(self._y-y)**2) < cor
[docs] def is_p2dl_within_cor(self, point, cor=1E-8, on_cor_flag=True): """ Check if a lean-point is inside or on a circle centred at self. Parameters ---------- point : p2d_leanest Lean point to test. cor : float, optional Circle radius. on_cor_flag : bool, optional If True, include circle boundary (`<= cor`), else strict inside (`< cor`). Returns ------- bool True if point satisfies circle-membership criterion. Examples -------- .. code-block:: python from upxo.geoEntities.point2d import p2d_leanest p2d_leanest(0, 0).is_p2dl_within_cor(p2d_leanest(0, 0), 1E-8) """ if on_cor_flag: return math.sqrt((self._x-point._x)**2+(self._y-point._y)**2) <= cor else: return math.sqrt((self._x-point._x)**2+(self._y-point._y)**2) < cor
[docs] def is_p2d_within_cor(self, point, cor=1E-8, on_cor_flag=True): """ Check if a Point2d is inside or on a circle centred at self. Parameters ---------- point : Point2d UPXO Point2d object to test. cor : float, optional Circle radius. on_cor_flag : bool, optional If True, include circle boundary (`<= cor`), else strict inside (`< cor`). Returns ------- bool True if point satisfies circle-membership criterion. Examples -------- .. code-block:: python from upxo.geoEntities.point2d import p2d_leanest, Point2d p2d_leanest(0, 0).is_p2d_within_cor(Point2d(0, 0), 1E-8) """ if on_cor_flag: return math.sqrt((self._x-point.x)**2+(self._y-point.y)**2) <= cor else: return math.sqrt((self._x-point.x)**2+(self._y-point.y)**2) < cor
[docs] class Point2d(): """ Full-featured 2D point for UPXO geometry workflows. The primary 2D point representation in UPXO. Supports rich geometric operations (distance, rotation, reflection, translation), equality and comparison utilities with tolerance, feature-dictionary attachment, and interoperability with Shapely, VTK, PyVista, and GMSH. Usage ----- from upxo.geoEntities.point2d import Point2d from upxo.geoEntities.point2d import Point2d as p2d Parameters ---------- x : float First coordinate of the point (interpreted according to ``plane``). y : float Second coordinate of the point (interpreted according to ``plane``). plane : str, optional Plane identifier — one of ``'xy'``, ``'yx'``, ``'yz'``, ``'zy'``, ``'xz'``, ``'zx'``. Determines how ``x`` and ``y`` map to 3D axes in mixed-plane workflows. Default is ``'xy'``. Raises ------ ValueError Raised by ``__eq__`` / ``__ne__`` when ``plist`` is empty or has an unrecognised point specification. Limitations ----------- - ABC inheritance was removed to eliminate per-object overhead; subclassing is possible but not recommended. - Pydantic validation is excluded intentionally to keep instantiation fast. Notes ----- Refer to the Jupyter notebook demos in the repository for guided usage examples covering clustering, meshing, and crystal workflows. Examples -------- >>> from upxo.geoEntities.point2d import Point2d >>> Point2d(10, 12) uxpo-p2d (10,12) Intersection of two 2D lines: >>> from upxo.geoEntities.point2d import Point2d >>> from upxo.geoEntities.sline2d import Sline2d as sl2d >>> la = sl2d.by_coord([0, 0], [1, 1]) >>> lb = sl2d.by_coord([0.1, 0.1], [1.8, 1.8]) >>> Point2d.from_intersection_two_lines(la, lb, tool='upxo') Point at a parametric position along a line: >>> from upxo.geoEntities.point2d import Point2d >>> from upxo.geoEntities.sline2d import Sline2d >>> Point2d.from_line_factor(Sline2d(-1, -1, 1, 1), 0.25) """ ε = 1E-8 # __slots__ = UPXO_Point.__slots__ + ('f', 'plane') __slots__ = ('x', 'y', 'f', 'plane') def __init__(self, x, y, plane='xy'): """ Initialize a 2D UPXO point. Parameters ---------- x, y : float Point coordinates. plane : str, optional Plane identifier (`xy`, `yx`, `yz`, `zy`, `xz`, `zx`) used for interpreting coordinate semantics in mixed-plane workflows. """ # super().__init__(x, y) self.x = x self.y = y self.plane = plane def __repr__(self): """Instance object string representation.""" return f"uxpo-p2d ({self.x},{self.y})" def __eq__(self, plist): """ Element-wise equality between self and one or more point representations. Parameters ---------- plist : Point2d, p2d_leanest, list, or tuple Single point object, a list/tuple of point objects, or a nested coordinate array. Accepted specification types are documented in ``eq_fast``. Returns ------- list of bool One boolean per comparison pair. Raises ------ ValueError If ``plist`` is empty or has an unrecognised point specification. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d, p2d_leanest >>> p2d(3, 4) == p2d_leanest(3, 4) [True] >>> p2d(3, 4) == [p2d_leanest(1, 4), p2d_leanest(3, 4)] [False, True] >>> p2d(3, 4) == [[1, 2], [3, 4], [5, 6]] [False, True, False] """ if not plist: raise ValueError("plist is empty.") spec_found = False # --------------------------------------------------------- if find_spec_of_points(plist) == 'Point2d': cmp, spec_found = [(self.x, self.y) == (plist.x, plist.y)], True # --------------------------------------------------------- elif find_spec_of_points(plist) == '[Point2d]': cmp, spec_found = [(self.x, self.y) == (p.x, p.y) for p in plist], True # --------------------------------------------------------- elif find_spec_of_points(plist) == 'p2d_leanest': cmp, spec_found = [(self.x, self.y) == (plist._x, plist._y)], True # --------------------------------------------------------- elif find_spec_of_points(plist) == '[p2d_leanest]': cmp, spec_found = [(self.x, self.y) == (p._x, p._y) for p in plist], True # --------------------------------------------------------- elif find_spec_of_points(plist) == 'type-[1,2]': cmp, spec_found = [(self.x, self.y) == tuple(plist)], True # --------------------------------------------------------- elif find_spec_of_points(plist) == 'type-[[1,2]]': cmp, spec_found = [(self.x, self.y) == tuple(plist[0])], True # --------------------------------------------------------- elif find_spec_of_points(plist) == 'type-[[1,2],[3,4],[5,6]]': cmp = [self.x == p[0] and self.y == p[1] for p in plist] spec_found = True # --------------------------------------------------------- elif find_spec_of_points(plist) == 'type-[[1,2,3,4],[5,6,7,8]]': cmp = [self.x == _x and self.y == _y for _x, _y in zip(plist[0], plist[1])] spec_found = True elif find_spec_of_points(plist) == 'type-shapely': pass elif find_spec_of_points(plist) == 'type-[shapely]': pass elif find_spec_of_points(plist) == 'type-gmsh': pass elif find_spec_of_points(plist) == 'type-[gmsh]': pass elif find_spec_of_points(plist) == 'type-vtk': pass elif find_spec_of_points(plist) == 'type-[vtk]': pass elif find_spec_of_points(plist) == 'type-pyvista': pass elif find_spec_of_points(plist) == 'type-[pyvista]': pass # --------------------------------------------------------- if spec_found: return cmp else: raise ValueError('Invalid points list provided.')
[docs] def eq(self, plist, *, use_tol=False): """ Named alias for ``__eq__``, with optional tolerance support. Parameters ---------- plist : Point2d, p2d_leanest, list, or tuple Point(s) to compare against self. See ``__eq__`` for accepted specification types. use_tol : bool, optional If ``True``, apply tolerance-aware comparison. Default is ``False`` (exact comparison via ``__eq__``). Returns ------- list of bool One boolean per comparison pair. Warnings -------- Tolerance-aware comparison (``use_tol=True``) is not yet implemented and will silently return ``None``. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d, p2d_leanest >>> p2d(3, 4).eq(p2d_leanest(3, 4)) [True] >>> p2d(3, 4).eq([p2d(1, 2), p2d(3, 4)]) [False, True] """ if not use_tol: return self.__eq__(plist) else: # TODO pass
[docs] def eq_fast(self, plist, use_tol=False, point_spec=1): """ Fast equality check skipping type detection — caller must supply spec. Unlike ``__eq__``, no runtime type detection is performed; the caller declares the format of ``plist`` via ``point_spec``. This avoids the overhead of ``find_spec_of_points()`` in tight loops. Parameters ---------- plist : various Point(s) to compare. Format must match ``point_spec``. use_tol : bool, optional Reserved for future tolerance-aware comparison. Default ``False``. point_spec : int, optional Integer ID declaring the type/format of ``plist``: ===== ================================== ID Format ===== ================================== 1 ``Point2d`` 2 ``[Point2d]`` or ``(Point2d, …)`` 3 ``p2d_leanest`` 4 ``[p2d_leanest]`` 5 ``[x, y]`` (flat, two numbers) 6 ``[[x, y]]`` 7 ``[[x1,y1], [x2,y2], …]`` 8 ``[[x1,x2,…], [y1,y2,…]]`` 9–16 Shapely/GMSH/VTK/PyVista variants ===== ================================== Default is ``1``. Returns ------- list of bool or None Comparison result(s), or ``None`` for unimplemented spec IDs. Warnings -------- No input validation is performed. Passing a ``plist`` whose format does not match ``point_spec`` will raise an ``AttributeError`` or return a wrong answer silently. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d, p2d_leanest >>> p2d(3, 4).eq_fast(p2d(3, 4), point_spec=1) [True] >>> p2d(3, 4).eq_fast([p2d(1, 2), p2d(3, 4)], point_spec=2) [False, True] >>> p2d(3, 4).eq_fast([3, 4], point_spec=5) [True] """ cmp = None if point_spec == 1: """1: Point2d""" cmp = [(self.x, self.y) == (plist.x, plist.y)] elif point_spec == 2: """2: [Point2d]""" cmp = [(self.x, self.y) == (p.x, p.y) for p in plist] elif point_spec == 3: """3: p2d_leanest""" cmp = [(self.x, self.y) == (plist._x, plist._y)] elif point_spec == 4: """4: [p2d_leanest]""" cmp = [(self.x, self.y) == (p._x, p._y) for p in plist] elif point_spec == 5: """5: type-[1,2]""" cmp = [(self.x, self.y) == tuple(plist)] elif point_spec == 6: """6: type-[[1,2]]""" cmp = [(self.x, self.y) == tuple(plist[0])] elif point_spec == 7: """7: type-[[1,2],[3,4],[5,6]]""" cmp = [self.x == p[0] and self.y == p[1] for p in plist] elif point_spec == 8: """8: type-[[1,2,3,4],[5,6,7,8]]""" cmp = [self.x == _x and self.y == _y for _x, _y in zip(plist[0], plist[1])] elif point_spec == 9: '''9: shapely''' pass elif point_spec == 10: '''[shapely]''' pass elif point_spec == 11: '''gmsh''' pass elif point_spec == 12: '''[gmsh]''' pass elif point_spec == 13: '''vtk''' pass elif point_spec == 14: '''[vtk]''' pass elif point_spec == 15: '''pyvista''' pass elif point_spec == 16: '''[pyvista]''' pass return cmp
def __ne__(self, plist): """ Element-wise inequality between self and one or more point representations. Parameters ---------- plist : Point2d, p2d_leanest, list, or tuple Point(s) to compare. See ``__eq__`` for accepted formats. Returns ------- list of bool Logical negation of the result of ``__eq__``. Examples -------- >>> from upxo.geoEntities.point2d import Point2d, p2d_leanest >>> Point2d(3, 4) != p2d_leanest(3, 4) [False] >>> Point2d(3, 4) != [Point2d(1, 2), Point2d(3, 4)] [True, False] """ return [not eq for eq in self.__eq__(plist)]
[docs] def ne(self, plist, *, use_tol=False): """ Named alias for ``__ne__``, with optional tolerance support. Parameters ---------- plist : Point2d, p2d_leanest, list, or tuple Point(s) to compare. See ``__eq__`` for accepted formats. use_tol : bool, optional If ``True``, apply tolerance-aware comparison. Default ``False``. Returns ------- list of bool Inequality results. Warnings -------- Tolerance-aware comparison (``use_tol=True``) is not yet implemented. Examples -------- >>> from upxo.geoEntities.point2d import Point2d, p2d_leanest >>> Point2d(3, 4).ne(p2d_leanest(3, 4)) [False] """ if not use_tol: return self.__ne__(plist) else: pass
[docs] def add(self, d, update=True, throw=False, mydecatlen2NUM='b'): """ Translate self by distance(s) ``d``, in-place and/or returning new point(s). Parameters ---------- d : float, list of float, list of list, or list of point objects Displacement to apply. Accepted forms: - ``scalar`` — added to both ``x`` and ``y``. - ``[dx]`` — added to both ``x`` and ``y``. - ``[dx, dy]`` — interpretation depends on ``mydecatlen2NUM``. - ``[[dx, dy], …]`` — one new point per sub-list. - ``[[x1,x2,…], [y1,y2,…]]`` — column-wise displacement arrays. - ``[point_obj, …]`` — list of UPXO/Shapely/VTK/PyVista/GMSH 2D or 3D point objects. update : bool, optional If ``True`` and ``d`` is a single scalar or ``(dx, dy)`` pair, update ``self.x`` and ``self.y`` in-place. Default ``True``. throw : bool, optional If ``True``, return a new ``Point2d`` (or ``deepcopy`` of self when ``update=True``). Default ``False``. mydecatlen2NUM : {'b', 'atxy', 'a', 'ta2sd'}, optional Disambiguation rule when ``d`` is a length-2 list of numbers: - ``'b'`` / ``'atxy'`` — treat as ``[dx, dy]`` (add to x and y separately). Default. - ``'a'`` / ``'ta2sd'`` — treat as two separate scalar displacements; returns two new points. Returns ------- Point2d or list of Point2d or None When ``throw=True`` returns the translated point(s). When ``update=True, throw=False`` returns ``None`` (side-effect only). Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> A = p2d(10, 12) >>> A.add(5) >>> A uxpo-p2d (15,17) Translate by separate dx/dy: >>> A = p2d(10, 12) >>> A.add([3, 4], mydecatlen2NUM='b') >>> A uxpo-p2d (13,16) Return a new point without mutating self: >>> A = p2d(10, 12) >>> B = A.add(5, update=False, throw=True) >>> B uxpo-p2d (15,17) """ NUMBERS, ITERABLES = dth.dt.NUMBERS, dth.dt.ITERABLES if type(d) in NUMBERS: ''' Ex. @ d --> 10 from upxo.geoEntities.point2d import Point2d as p2d d = 10 # -------------------------------- # Case A # ------ A = p2d(10, 12) A.add(d) print(A) # -------------------------------- # Case B # ------ A = p2d(10, 12) A.add(d, update=True, throw=False) # Same as case A print(A) # -------------------------------- # Case C # ------ A = p2d(10, 12) A.add(d, update=True, throw=True) print(A) # -------------------------------- # Case D # ------ A = p2d(10, 12) A.add(d, update=False, throw=True) print(A) # -------------------------------- ''' # add d to both x and y if update: self.x += d self.y += d if update and throw: return deepcopy(self) if not update and throw: return Point2d(self.x+d, self.y+d) # ======================================================= if type(d) in ITERABLES: # add d contents seperatrely to x and y # ................ # CASE - 1 if len(d) == 1 and type(d[0]) in NUMBERS: ''' Ex. @ d --> [10] from upxo.geoEntities.point2d import Point2d as p2d d = [10] # -------------------------------- # Case A # ------ A = p2d(10, 12) A.add(d) print(A) # -------------------------------- # Case B # ------ A = p2d(10, 12) A.add(d, update=True, throw=False) # Same as case A print(A) # -------------------------------- # Case C # ------ A = p2d(10, 12) A.add(d, update=True, throw=True) print(A) # -------------------------------- # Case D # ------ A = p2d(10, 12) A.add(d, update=False, throw=True) print(A) # -------------------------------- ''' if update: self.x += d[0] self.y += d[0] if update and throw: return deepcopy(self) if not update and throw: return Point2d(self.x+d[0], self.y+d[0]) # ................ # CASE - 2 if len(d) == 1 and type(d[0]) in ITERABLES and len(d[0]) == 2: """ from upxo.geoEntities.point2d import Point2d as p2d d = [[10, 12]] # -------------------------------- # Case A1 # ------- A = p2d(10, 12) A.add(d) print(A) # -------------------------------- # Case B1 # Same as Case - a # ------- A = p2d(10, 12) A.add(d, update=True, throw=False, mydecatlen2NUM='b') print(A) # -------------------------------- # Case C1 # ------- A = p2d(10, 12) A.add(d, update=True, throw=True, mydecatlen2NUM='b') print(A) # -------------------------------- # Case D1 # ------- A = p2d(10, 12) A.add(d, update=False, throw=True, mydecatlen2NUM='b') print(A) # -------------------------------- # Case A2 # ------- A = p2d(10, 12) A.add(d, update=True, throw=False, mydecatlen2NUM='a') # throw and update ignored. # results will be returned by default. print(A) # Remains unaltere3d as update is ignored # -------------------------------- """ if mydecatlen2NUM in ('a', 'ta2sd'): return [Point2d(self.x+_, self.y+_) for _ in d[0]] if mydecatlen2NUM in ('b', 'atxy'): if update: self.x += d[0][0] self.y += d[0][1] if update and throw: return deepcopy(self) if not update and throw: return Point2d(self.x+d[0][0], self.y+d[0][1]) # ................ # CASE - 3 if len(d) == 2 and all(isinstance_many(d, NUMBERS)): """ from upxo.geoEntities.point2d import Point2d as p2d d = [10, 12] # -------------------------------- # Case A1 # ------- A = p2d(10, 12) A.add(d) print(A) # -------------------------------- # Case B1 # Same as Case - a # ------- A = p2d(10, 12) A.add(d, update=True, throw=False, mydecatlen2NUM='b') print(A) # -------------------------------- # Case C1 # ------- A = p2d(10, 12) A.add(d, update=True, throw=True, mydecatlen2NUM='b') print(A) # -------------------------------- # Case D1 # ------- A = p2d(10, 12) A.add(d, update=False, throw=True, mydecatlen2NUM='b') print(A) # -------------------------------- # Case A2 # ------- A = p2d(10, 12) A.add(d, update=True, throw=False, mydecatlen2NUM='a') # throw and update ignored. # results will be returned by default. print(A) # Remains unaltere3d as update is ignored # -------------------------------- """ if mydecatlen2NUM in ('a', 'ta2sd'): return [Point2d(self.x+_, self.y+_) for _ in d] if mydecatlen2NUM in ('b', 'atxy'): if update: self.x += d[0] self.y += d[1] if update and throw: return deepcopy(self) if not update and throw: return Point2d(self.x+d[0], self.y+d[1]) # ................ if all(_.__class__.__name__ == 'p2d_leanest' for _ in d): """ from upxo.geoEntities.point2d import Point2d as p2d from upxo.geoEntities.point2d import p2d_leanest P = [p2d_leanest(-10, -12), p2d_leanest(-2, 2)] EXAMPLE CASES ------------- # Only case possible # update and throw input arguments will be ignored. # -------------------------------- # Case A # ------ A = p2d(10, 12) A.add(P, update=True, throw=False) print(A) # -------------------------------- """ return [Point2d(self.x+_._x, self.y+_._y) for _ in d] # ................ if all(_.__class__.__name__ == 'Point2d' for _ in d): """ from upxo.geoEntities.point2d import Point2d as p2d P = [p2d(-10, -12), p2d(-2, 2)] EXAMPLE CASES ------------- # Only case possible # update and throw input arguments will be ignored. # -------------------------------- # Case A # ------ A = p2d(10, 12) A.add(P, update=True, throw=False) print(A) # -------------------------------- """ return [Point2d(self.x+_.x, self.y+_.y) for _ in d] # ................ if len(d) == 2 and all(isinstance_many(d, ITERABLES)): if len(d[0]) == 1 and len(d[1]) == 1: """ from upxo.geoEntities.point2d import Point2d as p2d d = [[10], [12]] # Case A # ------ A = p2d(10, 12) A.add(d) print(A) # -------------------------------- # Case A1 # Same as case A # ------ A = p2d(10, 12) A.add(d, update=True, throw=False, mydecatlen2NUM='b') print(A) # -------------------------------- # Case B # ------ A = p2d(10, 12) A.add(d, update=True, throw=True, mydecatlen2NUM='b') print(A) # -------------------------------- # Case C # ------ A = p2d(10, 12) A.add(d, update=False, throw=True, mydecatlen2NUM='b') print(A) # -------------------------------- # Case D # NOTHING CHANGES! # ------ A = p2d(10, 12) A.add(d, update=False, throw=False, mydecatlen2NUM='b') print(A) # -------------------------------- """ if update: self.x += d[0][0] self.y += d[1][0] if update and throw: return deepcopy(self) if not update and throw: return Point2d(self.x+d[0][0], self.y+d[1][0]) elif len(d[0]) > 1 and (len(d[0]) == len(d[1])): """ from upxo.geoEntities.point2d import Point2d as p2d d = [[10, 11, 12, 13], [12, 13, 14, 15]] EXAMPLE CASES ------------- # Only case possible # update and throw input arguments will be ignored. # -------------------------------- # Case A # ------ A = p2d(10, 12) A.add(d) print(A) # -------------------------------- """ return [Point2d(self.x+_x, self.y+_y) for _x, _y in zip(d[0], d[1])] # ................ # CASE - 4 if len(d) > 2 and all(isinstance_many(d, NUMBERS)): """ from upxo.geoEntities.point2d import Point2d as p2d d = [10, 11, 12, 13] EXAMPLE CASES ------------- # Only case possible # update and throw input arguments will be ignored. # -------------------------------- # Case A # ------ A = p2d(10, 12) A.add(d) print(A) # -------------------------------- """ return [Point2d(self.x+_d, self.y+_d) for _d in d] # ................ # CASE - 5 if len(d) > 2 and all(isinstance_many(d, ITERABLES)): if all(len(_) == 2 for _ in d): print('i am in') """ from upxo.geoEntities.point2d import Point2d as p2d d = [[2, 3], [4, 5], [5, 6], [0, 10]] EXAMPLE CASES ------------- # Only case possible # update and throw input arguments will be ignored. # -------------------------------- # Case A # ------ A = p2d(10, 12) A.add(d) print(A) # -------------------------------- """ return make_p2d(d, return_type='Point2d') # return [Point2d(self.x+_d[0], self.y+_d[1]) for _d in d] else: '''Ex. @ d --> [[2, 3, 5], [4, 5], [5, 6], [0, 10]]''' '''Ex. @ d --> [[2, 3, 6], [4], [5, 6], [0, 10]]''' '''Ex. @ d --> [[2, 3, 6], [4, 5, 6], [0, 5, 10]]''' raise ValueError('Invalid distances.') # ======================================================= if d.__class__.__name__ == 'Point2d': """ from upxo.geoEntities.point2d import Point2d as p2d P = p2d(-10, -12) # -------------------------------- # Case A # ------ A = p2d(10, 12) A.add(P, update=True, throw=False) print(A) # -------------------------------- # Case B # ------ A = p2d(10, 12) A.add(P, update=True, throw=True) print(A) # -------------------------------- # Case C # ------ A = p2d(10, 12) A.add(P, update=False, throw=True) print(A) # -------------------------------- # Case D # ------ A = p2d(10, 12) A.add(P, update=False, throw=False) print(A) # -------------------------------- """ if update: self.x += d.x self.y += d.y if update and throw: return deepcopy(self) if not update and throw: return Point2d(self.x+d.x, self.y+d.y) # ======================================================= if d.__class__.__name__ == 'p2d_leanest': """ from upxo.geoEntities.point2d import Point2d as p2d from upxo.geoEntities.point2d import p2d_leanest P = p2d_leanest(-10, -12) # -------------------------------- # Case A # ------ A = p2d(10, 12) A.add(P, update=True, throw=False) print(A) # -------------------------------- # Case B # ------ A = p2d(10, 12) A.add(P, update=True, throw=True) print(A) # -------------------------------- # Case C # ------ A = p2d(10, 12) A.add(P, update=False, throw=True) print(A) # -------------------------------- # Case D # ------ A = p2d(10, 12) A.add(P, update=False, throw=False) print(A) """ if update: self.x += d._x self.y += d._y if update and throw: return deepcopy(self) if not update and throw: return Point2d(self.x+d._x, self.y+d._y)
def __mul__(self, f=1.0): """ Scale self point in-place by scalar or per-axis factor. Parameters ---------- f : float or list of float Scalar: multiply both ``x`` and ``y`` by ``f``. Length-2 list ``[fx, fy]``: multiply ``x`` by ``fx`` and ``y`` by ``fy`` independently. Returns ------- None Modifies self in-place. Raises ------ TypeError If ``f`` is neither a number nor a length-2 iterable. Examples -------- >>> from upxo.geoEntities.point2d import Point2d >>> a = Point2d(-10, -12) >>> a * 2 >>> a uxpo-p2d (-20,-24) >>> a = Point2d(-10, -12) >>> a * [2, 4] >>> a uxpo-p2d (-20,-48) """ if type(f) in NUMBERS: self.x *= f self.y *= f elif type(f) in ITERABLES and len(f) == 2: self.x *= f[0] self.y *= f[1] else: raise TypeError('Invald factor')
[docs] def mul(self, f=1.0, update=True, throw=False): """ Named alias for ``__mul__`` with optional mutation and return control. Parameters ---------- f : float or list of float Scaling factor. Scalar applies to both axes; an iterable of scalars applies each element as a separate scalar (one result per element). update : bool, optional If ``True``, scale self in-place. Default ``True``. throw : bool, optional If ``True``, return the scaled point or a ``deepcopy`` of self. Default ``False``. Returns ------- Point2d or list of Point2d or None Returns scaled point(s) when ``throw=True``, else ``None``. Raises ------ TypeError If ``f`` is not a number or iterable. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> point = p2d(-10, -12) >>> point.mul(f=-2.5, update=False, throw=True) uxpo-p2d (25.0,30.0) """ valid = False # -------------------------------- if isinstance(f, NUMBERS): valid = True if update: self.x *= f self.y *= f if update and throw: return deepcopy(self) if not update and throw: return Point2d(self.x*f, self.y*f) # -------------------------------- if isinstance(f, ITERABLES): valid = True # Validations here return [self.mul(f=_f_[0], update=False, throw=True) for _f_ in f] # -------------------------------- if not valid: raise TypeError('Invalid factor.')
[docs] @classmethod def from_intersection_two_lines(cls, la, lb, tool='upxo', return_type='upxo'): """ Construct a point at the intersection of two 2D lines. The return value is a list so callers handle collinear cases (multiple points or infinitely many) uniformly. Parameters ---------- la : Sline2d First 2D straight-line segment. lb : Sline2d Second 2D straight-line segment. tool : {'upxo', 'shapely'}, optional Backend to use for the intersection calculation. Default ``'upxo'``. return_type : str, optional Format of returned point(s). Default ``'upxo'``. Returns ------- list of Point2d Intersection point(s). Empty list if lines do not intersect. Notes ----- Collinear lines (same direction) return an empty list even when they overlap, as no unique intersection point exists. Examples -------- >>> from upxo.geoEntities.sline2d import Sline2d >>> from upxo.geoEntities.point2d import Point2d >>> la = Sline2d.by_coord([0, 0], [1, 1]) >>> lb = Sline2d.by_coord([0, 1], [1, 0]) >>> Point2d.from_intersection_two_lines(la, lb, tool='upxo') Collinear lines (no unique intersection): >>> la = Sline2d.by_coord([0, 0], [1, 1]) >>> lb = Sline2d.by_coord([0.1, 0.1], [1.8, 1.8]) >>> Point2d.from_intersection_two_lines(la, lb, tool='upxo') [] """ if tool == 'upxo': return fmake.intersect_slines2d(la, lb, Point2d, return_type=return_type) if tool == 'shapely': from shapely.geometry import LineString lineA = LineString([(la[0], la[1]), (la[2], la[3])]) lineB = LineString([(lb[0], lb[1]), (lb[2], lb[3])]) if lineA.intersects(lineB): return lineA.intersection(lineB)
[docs] @classmethod def from_line_factor(cls, line, factor): """ Construct a point at a parametric position along a line. Computes ``(x0 + factor*dx, y0 + factor*dy)`` where ``(x0, y0)`` is the line start and ``(dx, dy)`` is its direction vector. Parameters ---------- line : Sline2d UPXO 2D straight-line segment. factor : float Parametric factor. ``0`` → start point; ``1`` → end point; values outside ``[0, 1]`` extrapolate beyond the segment. Returns ------- Point2d Point at position ``start + factor * direction``. Examples -------- >>> from upxo.geoEntities.sline2d import Sline2d >>> from upxo.geoEntities.point2d import Point2d >>> Point2d.from_line_factor(Sline2d(-1, -1, 1, 1), 0.25) >>> Point2d.from_line_factor(Sline2d(-1, -1, 1, 1), -0.25) >>> Point2d.from_line_factor(Sline2d(-1, -1, 1, 1), 1.25) """ # Validations return Point2d(line.x0 + factor*line.dx, line.y0 + factor*line.dy)
[docs] @classmethod def from_intersection_lines_regions(cls, lines, regions): """ Instantiate point(s) from intersections between lines and regions. Parameters ---------- lines : iterable Collection of line-like entities. regions : iterable Collection of region-like entities. Notes ----- To be developed. """ raise NotImplementedError("from_intersection_lines_regions is not yet implemented.")
@property def coords(self): """ X and Y coordinates as a NumPy array. Returns ------- numpy.ndarray, shape (2,) ``[x, y]``. Examples -------- >>> from upxo.geoEntities.point2d import Point2d >>> A = Point2d(1.125, 0.456) >>> A.coords array([1.125, 0.456]) """ return np.array([self.x, self.y]) @property def shapely(self): """ Shapely Point representation of self. Returns ------- shapely.geometry.Point Point at ``(self.x, self.y)``. Examples -------- >>> from upxo.geoEntities.point2d import Point2d >>> A = Point2d(1.125, 0.456) >>> A.shapely <POINT (1.125 0.456)> """ from shapely.geometry import Point as ShPnt return ShPnt(self.x, self.y)
[docs] def inside_line(self, line, consider_ends=True, consider_tol=False, tol=0.0): """ Test whether self lies on (within) a 2D line segment. Parameters ---------- line : Sline2d UPXO 2D straight-line segment to test against. consider_ends : bool, optional If ``True``, points coinciding with the segment endpoints count as inside (``<=`` comparison). If ``False``, endpoints are excluded (``<`` comparison). Default ``True``. consider_tol : bool, optional Reserved for tolerance-aware testing. Not yet implemented. tol : float, optional Tolerance value for future use. Default ``0.0``. Returns ------- bool ``True`` if self lies on the segment, ``False`` otherwise. Warnings -------- Tolerance-aware testing (``consider_tol=True``) is not yet implemented and will have no effect. Examples -------- >>> from upxo.geoEntities.point2d import Point2d >>> from upxo.geoEntities.sline2d import Sline2d >>> line = Sline2d.by_coord([0, 0], [1, 1]) >>> Point2d(0.5, 0.5).inside_line(line) True >>> Point2d(-0.5, -0.5).inside_line(line) False >>> Point2d(0, 0).inside_line(line, consider_ends=False) False """ # Validations # TODO: Include tolerance consideratiopns pointA, pointB = [[line.x0, line.y0], [line.x1, line.y1]] length, inside_line = line.length, None if consider_ends: inside_line = all([d <= length for d in self.distance([pointA, pointB])]) else: inside_line = all([d < length for d in self.distance([pointA, pointB])]) return inside_line
[docs] def squared_distance(self, plist=None): """ Compute squared Euclidean distances from self to one or more points. Parameters ---------- plist : point spec or list of point specs Accepted formats match those of ``__eq__``: single or list of ``Point2d``, ``p2d_leanest``, ``[x, y]``, ``[[x, y], …]``, or column arrays ``[[x1,…], [y1,…]]``. Returns ------- numpy.ndarray Squared distances ``(self.x - X)² + (self.y - Y)²`` for each target point. Raises ------ ValueError If ``plist`` is empty. Limitations ----------- - Only 2D point targets are supported; 3D, Shapely, GMSH, and VTK targets are not yet implemented. Examples -------- >>> from upxo.geoEntities.point2d import Point2d, p2d_leanest >>> Point2d(0, 0).squared_distance(p2d_leanest(3, 4)) array([25.]) >>> Point2d(0, 0).squared_distance([Point2d(1, 2), Point2d(3, 4)]) array([ 5., 25.]) >>> Point2d(0, 0).squared_distance([[1, 2, -1, -3], [4, 5, 5, 6]]) array([17., 29., 26., 45.]) """ if type(plist) not in ITERABLES: plist = [plist] if len(plist) == 0: raise ValueError('points list cannot be empty.') plist = make_p2d(plist, return_type='leanest') X, Y = np.array([[p._x, p._y] for p in plist]).T return (self.x-X)**2 + (self.y-Y)**2
[docs] def distance(self, plist=None): """ Compute Euclidean distances from self to one or more points. Parameters ---------- plist : point spec or list of point specs Same accepted formats as ``squared_distance``. Returns ------- numpy.ndarray Euclidean distances ``sqrt((self.x-X)² + (self.y-Y)²)``. Limitations ----------- - Only 2D point targets are supported currently. Examples -------- >>> from upxo.geoEntities.point2d import Point2d, p2d_leanest >>> Point2d(0, 0).distance(p2d_leanest(3, 4)) array([5.]) >>> Point2d(0, 0).distance([Point2d(1, 2), Point2d(3, 4)]) array([2.23606798, 5. ]) """ return np.sqrt(self.squared_distance(plist))
[docs] def translate(self, *, vector=None, dist=None, update=False, throw=True, make3d=False, zloc=0): """ Translate self along ``vector`` by scalar distance ``dist``. The displacement applied is ``(vector / |vector|) * dist``. Parameters ---------- vector : list of float Direction vector; length 2 for 2D translation, length 3 when ``make3d=True`` and a Z-component is needed. dist : float Distance to translate along ``vector``. update : bool, optional If ``True``, update self's coordinates in-place. Default ``False``. throw : bool, optional If ``True``, return a point object. Default ``True``. make3d : bool, optional If ``True``, return a ``Point3d`` instead of ``Point2d``. Default ``False``. zloc : float, optional Z-coordinate for the returned ``Point3d`` when ``make3d=True`` and ``vector`` has length 2. Default ``0``. Returns ------- Point2d or Point3d or None Translated point when ``throw=True``; ``None`` otherwise. Examples -------- >>> from upxo.geoEntities.point2d import Point2d >>> A = Point2d(0, 0) >>> A.translate(vector=[1, 0], dist=5, update=True, throw=True) uxpo-p2d (5.0,0.0) Translate and return a 3D point: >>> A = Point2d(0, 0) >>> A.translate(vector=[-1, -1], dist=5, throw=True, ... make3d=True, zloc=10) """ distances = (np.array(vector) / np.linalg.norm(vector)) * dist if update: if len(vector) != 2 and make3d == False: self.x += distances[0] self.y += distances[1] # self.add(distances, update=update, throw=throw) if update and throw: if not make3d: self.x += distances[0] self.y += distances[1] return deepcopy(self) else: if len(distances) == 2: if type(zloc) in NUMBERS: return Point3d(self.x+distances[0], self.y+distances[1], zloc) else: return Point3d(self.x+distances[0], self.y+distances[1], 0) elif len(distances) == 3: return Point3d(self.x+distances[0], self.y+distances[1], distances[2]) else: raise ValueError('INvalid distance calculation.') if not update and throw: if not make3d: return Point2d(self.x+distances[0], self.y+distances[1]) else: return Point3d(self.x+distances[0], self.y+distances[1], distances[2])
[docs] @staticmethod def val_point_and_get_coord(point): """Validate a single point specification and return its (x, y) coordinates.""" _ = False if not point: raise ValueError('Point OR coord not provided.') if find_spec_of_points(point) == 'Point2d': target_loc_x, target_loc_y, _ = point.x, point.y, True if find_spec_of_points(point) == '[Point2d]': target_loc_x, target_loc_y, _ = point[0].x, point[0].y, True if find_spec_of_points(point) == 'p2d_leanest': target_loc_x, target_loc_y, _ = point._x, point._y, True if find_spec_of_points(point) == '[p2d_leanest]': target_loc_x, target_loc_y, _ = point[0]._x, point[0]._y, True if find_spec_of_points(point) == 'type-[1,2]': target_loc_x, target_loc_y, _ = point[0], point[1], True if find_spec_of_points(point) == 'type-[[1,2]]': target_loc_x, target_loc_y, _ = point[0][0], point[0][1], True if find_spec_of_points(point) == 'type-[[1,2,3,4],[5,6,7,8]]': if len(point[0]) * len(point[1]) == 1: target_loc_x, target_loc_y, _ = point[0][0], point[1][0], True if not _: raise ValueError('Invalid point input.') return target_loc_x, target_loc_y
[docs] @staticmethod def val_points_and_get_coords(points): """Validate a collection of point specifications and return coordinate arrays (x, y).""" if not points: raise ValueError('Points OR coords not provided.') if type(points) in dth.dt.ITERABLES: if len(set(type(point) for point in points)) != 1: raise ValueError('Points array contains multiple datatypes.') else: if find_spec_of_points(points) not in ('Point2d', 'p2d_leanest'): raise ValueError('Invalid datatype of the proivided single point.') points = [points] # -------------------------------------------------- valid = False if find_spec_of_points(points[0]) in 'Point2d': x, y, valid = [p.x for p in points], [p.y for p in points], True if find_spec_of_points(points[0]) == 'p2d_leanest': x, y, valid = [p._x for p in points], [p._y for p in points], True if find_spec_of_points(points) == 'type-[1,2]': x, y, valid = [points[0]], [points[1]], True if find_spec_of_points(points) == 'type-[[1,2]]': x, y, valid = [points[0][0]], [points[0][1]], True if find_spec_of_points(points) == 'type-[[1,2],[3,4],[5,6]]': x, y, valid = [p[0] for p in points], [p[1] for p in points], True if find_spec_of_points(points) == 'type-[[1,2,3,4],[5,6,7,8]]': x, y, valid = points[0], points[1], True if not valid: raise ValueError('Invalid points input.') return x, y
[docs] def translate_to(self, *, point=None, update=False, throw=True): """ Move self to an absolute target position. Unlike ``translate``, which applies a displacement, this method sets self's coordinates to those of ``point`` directly. Parameters ---------- point : Point2d, p2d_leanest, tuple, or list Target position. Any valid UPXO 2D point specification. update : bool, optional If ``True``, overwrite ``self.x`` and ``self.y``. Default ``False``. throw : bool, optional If ``True``, return the moved point. Default ``True``. Returns ------- Point2d or Point3d or None Moved point when ``throw=True``, else ``None``. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> A = p2d(0, 0) >>> A.translate_to(point=p2d(1, 1), update=True, throw=True) uxpo-p2d (1,1) >>> A = p2d(0, 0) >>> A.translate_to(point=(1, 1), update=True, throw=True) uxpo-p2d (1,1) """ coord = val_point_and_get_coord(point, return_type='coord', safe_exit=True) # --------------------------------------------------------- if update and len(coord) == 2: self.x, self.y = coord if throw: return deepcopy(self) elif update and len(coord) == 3: # Retain this here, as the behaviour may change later on self.x, self.y = coord[:-1] if throw: return Point3d(*coord) if not update and throw: if len(coord) == 2: return Point2d(*coord) elif len(coord) == 2: return Point3d(*coord)
[docs] def rotate_about_point(self, point=None, angle=0, *, degree=True, update=False, throw=True, dec=8): """ Rotate self about an arbitrary pivot point by a given angle. Parameters ---------- point : Point2d, p2d_leanest, tuple, or list Pivot point to rotate about. angle : float Rotation angle. Interpreted as degrees when ``degree=True``, radians otherwise. degree : bool, optional If ``True``, treat ``angle`` as degrees. Default ``True``. update : bool, optional If ``True``, update self's coordinates in-place. Default ``False``. throw : bool, optional If ``True``, return the rotated point. Default ``True``. dec : int, optional Decimal places for rounding the output coordinates. Default ``8``. Returns ------- Point2d or None Rotated point when ``throw=True``. A ``deepcopy`` of self is returned when ``update=True, throw=True``; a new ``Point2d`` when ``update=False, throw=True``. Raises ------ ValueError If ``point`` is empty or ``angle`` is not a number. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> p2d(1, 0).rotate_about_point(p2d(0, 0), 90, ... degree=True, update=True, throw=True) uxpo-p2d (0.0,1.0) >>> p2d(1, 0).rotate_about_point((0, 0), 45, ... degree=True, update=True, throw=True) """ if not point: raise ValueError('Must provide point object. Could also be coord.') if type(angle) not in dth.dt.NUMBERS: raise ValueError('Invalid angle.') if degree: A = np.radians(angle) # ---------------------------------------------------- # Coordinates of the point to rotate baout locx, locy = Point2d.val_point_and_get_coord(point) # ---------------------------------------------------- st, ct, delx, dely = math.sin(A), math.cos(A), self.x-locx, self.y-locy xnew, ynew = delx*ct - dely*st + locx, delx*st + dely*ct + locy # ---------------------------------------------------- if update: self.x, self.y = round(xnew, dec), round(ynew, dec) if update and throw: # Retain this here, as the behaviour may change later on return deepcopy(self) if not update and throw: return Point2d(round(xnew, dec), round(ynew, dec))
[docs] def rotate_points(self, points=None, angles=0.0, *, degree=True, dec=8, return_type='p2d'): """ Rotate one or more target points about self as the pivot. Parameters ---------- points : Point2d, p2d_leanest, tuple, list, or column-array tuple Target point(s) to rotate. Accepted forms include single point objects, lists of point objects, ``(x, y)`` tuples, and column-array tuples ``([x1, x2, …], [y1, y2, …])``. angles : float or array-like of float Rotation angle(s). Single scalar applied to all points; array must match the number of target points. degree : bool, optional If ``True``, treat ``angles`` as degrees. Default ``True``. dec : int, optional Decimal places for rounding output coordinates. Default ``8``. return_type : str, optional Reserved for future return-format control. Default ``'p2d'``. Returns ------- numpy.ndarray, shape (N, 2) Rotated coordinates ``[[x1, y1], [x2, y2], …]``. Raises ------ ValueError If ``points`` is empty or ``angles`` length mismatches points. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> p2d(0, 0).rotate_points(p2d(1, 0), 45, degree=True) >>> p2d(0, 0).rotate_points(([1, 2, 3], [0, 0, 0]), 45, degree=True) """ _ITER, _NUMB = dth.dt.ITERABLES, dth.dt.NUMBERS # ---------------------------------------------------- # Validations if not points: raise ValueError('Must provide point object. Could also be coord.') # Coordinates of the point to rotate baout locx, locy = Point2d.val_points_and_get_coords(points) locx, locy = np.array(locx), np.array(locy) # ---------------------------------------------------- # Validations if type(angles) in _ITER: if not all(type(angle) in _NUMB for angle in angles): raise ValueError('Invalid angle.') if angles.size != len(locx): raise ValueError('Invalid angles input length.') if type(angles) in _NUMB: angles = np.array(angles) if angles.size == 1: angles = np.tile(angles, len(locx)) if degree: angles = np.radians(angles) # ---------------------------------------------------- st, ct = np.sin(angles), np.cos(angles) delx, dely = locx-self.x, locy-self.y xnew, ynew = delx*ct - dely*st + self.x, delx*st + dely*ct + self.y return np.array([xnew, ynew]).T
[docs] def attach_feature_(self, *, feature=None, feature_id=None): """ Attach a feature object to the point feature dictionary. Parameters ---------- feature : object Feature instance to attach. feature_id : hashable Key used to store/retrieve the feature within feature class bucket. Raises ------ ValueError If `feature` or `feature_id` is empty. KeyError If `feature_id` already exists for the same feature class. """ if not feature: raise ValueError('feature cannot be empty.') if not feature_id: raise ValueError('feature_id cannot be empty.') fname = feature.__class__.__name__ if not hasattr(self, 'f'): self.f = {} if fname not in self.f: self.f[fname] = {} if feature_id in self.f[fname].keys(): raise KeyError('Cannot attach feature. feature_id: ' f'{feature_id} already in dict {self.f[fname]}.') self.f[fname][feature_id] = feature
[docs] def find_closest_points(self, plist=None, *, plane='xy', on_boundary=True): """ Return indices of the point(s) in ``plist`` closest to self. Delegates to ``find_neigh_points_by_distance`` with ``r=0``. Parameters ---------- plist : list of Point2d, p2d_leanest, or coordinate arrays Collection of candidate points. plane : str, optional Plane specifier. Default ``'xy'``. on_boundary : bool, optional Passed through to ``find_neigh_points_by_distance``. Default ``True``. Returns ------- numpy.ndarray of int Indices of the closest point(s) in ``plist``. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> p2d(0, 0).find_closest_points([p2d(0, 0), p2d(0, 1), p2d(0, 0)]) array([0, 2]) >>> p2d(0, 0).find_closest_points([[1, 2], [2, 3], [0, -5], ... [1, 2], [1, 2]]) """ return self.find_neigh_points_by_distance(plist=plist, plane='xy', r=0, on_boundary=True)
[docs] def find_neigh_points_by_distance(self, plist=None, plane='xy', r=0, on_boundary=True): """ Return indices of points in ``plist`` within radius ``r`` of self. Parameters ---------- plist : list or array-like Candidate points. plane : str, optional Plane specifier. Default ``'xy'``. r : float, optional Search radius. When ``r <= ε`` (≈ 0), returns the index/indices of the globally closest point(s). Default ``0``. on_boundary : bool, optional If ``True``, include points at exactly ``r``. Default ``True``. Returns ------- numpy.ndarray of int Indices of neighbouring points. Raises ------ TypeError If ``r`` is not a number. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> p2d(0, 0).find_neigh_points_by_distance([[1, 2], [10, 12], [0, 0]]) array([2]) """ plist = np.array(plist) if type(r) not in dth.dt.NUMBERS: raise TypeError('Invalid r type.') sd = self.squared_distance(plist) if r <= self.ε: return np.argwhere(sd == sd.min()).squeeze() else: if on_boundary: return np.argwhere(sd <= r) else: return np.argwhere(sd < r)
[docs] def find_neigh_points_by_count(self, plist=None, n=None, plane='xy'): """ Return indices of the ``n`` nearest points in ``plist`` to self. Parameters ---------- plist : list or array-like Candidate points. n : int Number of nearest neighbours to return. plane : str, optional Plane specifier. Default ``'xy'``. Returns ------- tuple of numpy.ndarray Indices of the ``n`` closest points (from ``numpy.where``). Raises ------ TypeError If ``n`` is not a non-zero integer. ValueError If ``n`` exceeds the length of ``plist``. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> p2d(0, 0).find_neigh_points_by_count( ... [[1, 2], [10, 12], [0, -5], [0, 0]], n=2) """ # Validate plist # Validate n if not isinstance(n, int) or n == 0: raise TypeError('n must be an int type and non-zero.') if n > len(plist): raise ValueError('n is greater than len(plist).') sd = self.squared_distance(plist) return np.where(np.in1d(sd, np.sort(sd)[:n]))
[docs] def find_neigh_mulpoint_by_distance(self, *, mplist=None, plane='xy', r=0, tolf=-1): """ Find neighboring multi-points within a distance criterion. Parameters ---------- mplist : iterable Collection of multi-point objects. plane : str, optional Plane specifier. Defaults to `xy`. r : float, optional Search radius. tolf : float, optional Tolerance factor for membership decision. Notes ----- To be developed. """ # Use the ckdtree option. raise NotImplementedError("find_neigh_mulpoint_by_distance is not yet implemented.")
[docs] def find_neigh_edge_by_distance(self, *, elist=None, plane='xy', refloc='starting', r=0): """ Find neighboring edges using distance-based search. Parameters ---------- elist : iterable Collection of edge objects. plane : str, optional Plane specifier. Defaults to `xy`. refloc : str, optional Edge reference location used for distance calculation. r : float, optional Search radius. Notes ----- To be developed. """ raise NotImplementedError("find_neigh_edge_by_distance is not yet implemented.")
[docs] def find_neigh_muledge_by_distance(self, *, melist=None, plane='xy', refloc='starting', r=0): """ Find neighboring multi-edges using distance-based search. Parameters ---------- melist : iterable Collection of multi-edge objects. plane : str, optional Plane specifier. Defaults to `xy`. refloc : str, optional Edge reference location used for distance calculation. r : float, optional Search radius. Notes ----- To be developed. """ raise NotImplementedError("find_neigh_muledge_by_distance is not yet implemented.")
[docs] def find_neigh_xtal_by_distance(self, *, xlist=None, plane='xy', refloc='starting', r=0): """ Find neighboring crystals using distance-based search. Parameters ---------- xlist : iterable Collection of crystal objects. plane : str, optional Plane specifier. Defaults to `xy`. refloc : str, optional Crystal reference location used for distance calculation. r : float, optional Search radius. Notes ----- To be developed. """ raise NotImplementedError("find_neigh_xtal_by_distance is not yet implemented.")
[docs] def set_gmsh_props(self, prop_dict): """ Set Gmsh-related properties for point export/workflows. Parameters ---------- prop_dict : dict Dictionary of Gmsh properties. Notes ----- To be developed. """ raise NotImplementedError("set_gmsh_props is not yet implemented.")
[docs] def array_by_translation(self, ncopies=10, vector=[0, 0, 0], spacing='constant'): """ Create translated copies of the point. Parameters ---------- ncopies : int, optional Number of copies. vector : iterable, optional Translation direction/magnitude specifier. spacing : str, optional Spacing mode. Notes ----- To be developed. """ raise NotImplementedError("array_by_translation is not yet implemented.")
[docs] def array_by_rotation(self, ncopies=10, vector=[0, 0, 0], spacing='constant'): """ Create rotated copies of the point. Parameters ---------- ncopies : int, optional Number of copies. vector : iterable, optional Rotation axis/reference specifier. spacing : str, optional Spacing mode. Notes ----- To be developed. """ raise NotImplementedError("array_by_rotation is not yet implemented.")
[docs] def array_on_arc(self, ncopies=10, r=1, angles=[0.0, 360.0], degree=True): """ Create point copies distributed on an arc. Parameters ---------- ncopies : int, optional Number of copies. r : float, optional Arc radius. angles : iterable, optional Start and end angles. degree : bool, optional If True, interpret angles in degrees. Notes ----- To be developed. """ raise NotImplementedError("array_on_arc is not yet implemented.")
[docs] def array_by_clustering(self, n=10, r=1, distribution='urand', dmin=None, return_type='coord_list', zloc=0.0, gmsh_model_name='Model-1'): """ Generate a random cluster of ``n`` points within radius ``r`` of self. Self acts as the approximate centroid of the cluster. The larger ``n`` is, the closer the actual centroid converges to self. Parameters ---------- n : int, optional Number of points to generate. Default ``10``. r : float, optional Cluster radius. Default ``1``. distribution : {'urand'}, optional Spatial distribution type. Currently only uniform random (``'urand'``) is implemented. Default ``'urand'``. dmin : float or None, optional Minimum inter-point distance constraint. Not yet implemented. return_type : str, optional Format of the returned points. Accepted values: ====================== ============================================ ``'coord_list'`` ``[[x0,…], [y0,…]]`` (default) ``'coords_2d'`` ``[[x0,y0], …]`` ``'upxo_2d'`` List of ``Point2d`` ``'upxo_2d_leanest'`` List of ``p2d_leanest`` ``'coord_list_3d'`` ``[[x0,…], [y0,…], [z0,…]]`` ``'coords_3d'`` ``[[x0,y0,z0], …]`` ``'shapely'`` List of ``shapely.geometry.Point`` ``'gmsh'`` List of GMSH point tags ``'pyvista'`` PyVista PolyData point cloud ``'mulpoint2d'`` UPXO mulpoint2d object ====================== ============================================ zloc : float, optional Z-coordinate for 3D or GMSH output. Default ``0.0``. gmsh_model_name : str, optional GMSH model name created if GMSH is not yet initialised. Default ``'Model-1'``. Returns ------- varies Format depends on ``return_type``; see parameter description. Limitations ----------- - Only ``distribution='urand'`` is implemented. - ``dmin`` constraint is not yet enforced. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> p2d(0, 0).array_by_clustering(n=10, r=1) >>> p2d(0, 0).array_by_clustering(n=10, r=1, return_type='coords_2d') >>> p2d(0, 0).array_by_clustering(n=10, r=1, return_type='upxo_2d') >>> p2d(0, 0).array_by_clustering(n=10, r=1, return_type='shapely') """ if distribution == 'urand': ang = np.random.uniform(0, 2*np.pi, n) rad = np.sqrt(np.random.uniform(0, r, n)) * r # -------------------------------------- xy = np.array([self.x+rad*np.cos(ang), self.y+rad*np.sin(ang)]) # -------------------------------------- if return_type in ('coord_list_2d', 'coord_list'): return xy elif return_type in ('coords_2d', 'coords2d', 'coord'): return xy.T elif return_type in ('upxo_2d', 'upxo2d', 'upxo'): return make_p2d(xy, return_type='p2d') elif return_type in ('upxo_2d_leanest', 'upxo2dleanest', 'upxoleanest', 'lean'): return make_p2d(xy, return_type='p2d_leanest') elif return_type in ('coord_list_3d'): return np.vstack((xy, np.zeros(n))) elif return_type in ('coords_3d', 'coords3d'): return np.vstack((xy, np.zeros(n))).T elif return_type in ('upxo_3d', 'upxo3d'): pass elif return_type in ('shapely'): '''Returns a list of shjapely point obejct.''' from shapely.geometry import Point as ShPnt return [ShPnt(x, y) for x, y in xy.T] elif return_type in ('gmsh'): '''Returnsa list of gmsh piont tags''' import gmsh if not gmsh.isInitialized(): gmsh.initialize() if not gmsh.model.getCurrent(): gmsh.model.add(gmsh_model_name) point_tags = [gmsh.model.geo.addPoint(x, y, zloc) for x, y in xy.T] return point_tags elif return_type in ('pyvista'): import pyvista points = np.vstack((xy, np.zeros(n))).T point_cloud = pyvista.PolyData(points) point_cloud.verts = np.vstack([[1, i] for i in range(n)]).flatten() return point_cloud elif return_type in OPT.name_mulpoint2d: # mulpoint2d from upxo.geoEntities.mulpoint2d import MPoint2d as mp2d return mp2d.from_xy(xy)
[docs] def lies_on_which_edge(self, *, elist=None, consider_ends=True): """ Return indices of edges on which the point lies. Parameters ---------- elist : iterable Collection of edge objects. consider_ends : bool, optional If True, consider edge endpoints as valid hits. Notes ----- To be developed. """ raise NotImplementedError("lies_on_which_edge is not yet implemented.")
[docs] def lies_in_which_xtal(self, *, xlist=None, cosider_boundary=True, consider_boundary_ends=True): """ Return indices of crystals containing this point. Parameters ---------- xlist : iterable Collection of crystal objects. cosider_boundary : bool, optional If True, include boundary checks. consider_boundary_ends : bool, optional If True, include boundary-endpoint checks. Notes ----- To be developed. """ raise NotImplementedError("lies_in_which_xtal is not yet implemented.")
[docs] def set_z(self, z=0): """ Attach a Z-coordinate to this 2D point via the feature dictionary. Stores a ``_coord_`` feature at key ``-1`` under ``self.f``. Parameters ---------- z : float, optional Z-coordinate value to attach. Default ``0``. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> A = p2d(10, 12) >>> A.set_z(z=100) >>> A.f['_coord_'][-1].z 100 """ from upxo.geoEntities.point2d import _coord_ self.attach_feature(feature=_coord_(self.x, self.y, z), feature_id=-1)
[docs] def make_vtk_point(self, z=0): """ Wrap this point as a VTK PolyData point. Parameters ---------- z : float, optional Z-coordinate for the VTK point. If ``self.f`` does not already contain a ``_coord_`` entry, ``set_z(z)`` is called first. Default ``0``. Returns ------- dict ``{'id': int, 'pd': vtkPolyData, 'help': str}`` where ``id`` is the inserted-point index and ``pd`` holds the vtkPolyData. Examples -------- >>> from upxo.geoEntities.point2d import Point2d as p2d >>> vtkobj = p2d(10, 12).make_vtk_point(z=100) >>> x, y, z = vtkobj['pd'].GetPoint(vtkobj['id']) """ if not hasattr(self, 'f'): self.set_z(z=z) import vtk points = vtk.vtkPoints() point_id = points.InsertNextPoint(self.x, self.y, self.f['_coord_'][-1].z) poly_data = vtk.vtkPolyData() poly_data.SetPoints(points) return {'id': point_id, 'pd': poly_data, 'help': "return['pd'].GetPoint(return['id'])"}
[docs] def make_shape(self): """ Create geometric shape representation of point. Notes ----- To be developed. """ raise NotImplementedError("make_shape is not yet implemented.")
[docs] def all_isinstance(dtype, *args): """ Check whether all provided arguments are instances of a given type. Parameters ---------- dtype : type Target Python/UPXO type to validate against. *args : various Objects to test. Returns ------- bool or None ``True`` if every argument is an instance of ``dtype``; ``None`` if no arguments are provided. Examples -------- >>> from upxo.geoEntities.point2d import all_isinstance, p2d_leanest >>> all_isinstance(p2d_leanest, p2d_leanest(0, 0), p2d_leanest(1, 1)) True """ if len(args) > 0: print(args) return all(isinstance(arg, dtype) for arg in args)