"""
2D straight-line entity classes for UPXO.
Provides lightweight and full-featured 2D straight-line classes used in UPXO
geometry construction, neighbourhood operations, meshing support, and geometric
characterisation workflows.
Usage
-----
from upxo.geoEntities.sline2d import Sline2d_leanest
from upxo.geoEntities.sline2d import Sline2d
from upxo.geoEntities.sline2d import Sline2d_leanest as sl2dl
from upxo.geoEntities.sline2d import Sline2d as sl2d
Classes
-------
Sline2d_leanest : Minimal 2D straight line (coordinates only, ``__slots__``).
Sline2d : Full-featured 2D straight line with geometric operations.
Notes
-----
Coordinate system convention (right-hand, Y-up)::
Y+
| Z-
| /
X- | / X+
-----O-----/----------
/|
/ |
Z+ Y-
"""
import math
import numpy as np
from copy import deepcopy
import upxo._sup.dataTypeHandlers as dth
from upxo.geoEntities.point2d import Point2d as p2d
from upxo.geoEntities.point2d import Point2d
np.seterr(divide='ignore')
NUMBERS, ITERABLES = dth.dt.NUMBERS, dth.dt.ITERABLES
[docs]
class Sline2d_leanest():
"""
Lean 2D straight line class.
This is the minimal, lightweight representation of a 2D straight line
in UPXO. It stores only endpoint coordinates `(x0, y0)` and `(x1, y1)`
and provides essential geometric utilities such as:
* line length
* gradient
* endpoint/index access
* point containment checks relative to the finite segment
Compared to :class:`Sline2d`, this class is intentionally compact and is
suitable for high-volume geometry operations where only core line behavior
is required.
Attributes
----------
x0, y0 : float
Coordinates of start point.
x1, y1 : float
Coordinates of end point.
Usage
-----
from upxo.geoEntities.sline2d import Sline2d_leanest as sl2dl
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d_leanest as sl2dl
>>> e = sl2dl(-2, 3, 4, 5)
>>> for coord in e:
... print(coord)
>>> print(e[1])
"""
__slots__ = ('x0', 'y0', 'x1', 'y1')
def __init__(self, x0=0, y0=0, x1=1, y1=0):
"""
Initialize a lean 2D line from two endpoint coordinates.
Parameters
----------
x0, y0 : float, optional
Coordinates of the start point. Defaults to `(0, 0)`.
x1, y1 : float, optional
Coordinates of the end point. Defaults to `(1, 0)`.
"""
self.x0, self.y0 = x0, y0
self.x1, self.y1 = x1, y1
def __repr__(self):
"""Return ``UPXO-sl2d-lean (x0,y0)-(x1,y1)`` with 6 decimal places."""
return f'UPXO-sl2d-lean ({round(self.x0, 6)},{round(self.y0, 6)})-({round(self.x1, 6)},{round(self.y1, 6)})'
def __iter__(self):
"""
Make self iterable over endpoint coordinate pairs.
Returns
-------
generator
Generator yielding two tuples:
1. `(x0, y0)`
2. `(x1, y1)`
"""
return (i for i in ((self.x0, self.y0), (self.x1, self.y1)))
def __getitem__(self, index):
"""
Make self indexable by endpoint position.
Parameters
----------
index : int
Endpoint index.
* `0` -> start point `(x0, y0)`
* `1` -> end point `(x1, y1)`
Returns
-------
tuple
Endpoint coordinates corresponding to index.
Raises
------
IndexError
If index is outside valid bounds.
"""
return ((self.x0, self.y0), (self.x1, self.y1))[index]
[docs]
@classmethod
def by_coord(cls, start, end):
"""
Create Sline2d by specifying end coordinates.
Parameters
----------
start : list of float
Start point as ``[x0, y0]``.
end : list of float
End point as ``[x1, y1]``.
Returns
-------
Sline2d_leanest
Lean line connecting ``start`` and ``end``.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d_leanest as sl2dlean
>>> A = sl2dlean.by_coord([-1, 2], [3, 4])
"""
return cls(start[0], start[1], end[0], end[1])
@property
def length(self):
"""
Return length of self.
Returns
-------
float
Euclidean distance between `(x0, y0)` and `(x1, y1)`.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d_leanest as sl2dl
>>> e = sl2dl(-2, 3, 4, 5)
>>> e.length
"""
return math.sqrt((self.x0-self.x1)**2 + (self.y0-self.y1)**2)
@property
def gradient(self):
"""
Return the gradient of the self line.
Returns
-------
float
Slope ``(y1 - y0) / (x1 - x0)``, or ``math.inf`` for vertical lines.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d_leanest as sl2dl
>>> sl2dl(-2, 3, 4, 5).gradient
>>> sl2dl(0, 1, 0, 2).gradient
"""
if self.x0 == self.x1:
return math.inf
else:
return (self.y1-self.y0)/(self.x1-self.x0)
[docs]
def contains_point(self, obj=None):
"""
Assess relative positioning of a point with respect to self edge.
Output helps determine whether the point:
1. is fully contained inside the self edge
2. is coincident with one of the points of the self edge
3. is located on the extended part of the self edge
4. none of the above. Relative position unknown.
Parameters
----------
obj : coord, UPXO point2d object
Represents a point in space. The default is None.
Notes
-----
Point containment is exact (no tolerance). The method evaluates:
* perpendicular distance of point from the line,
* distances from point to line endpoints,
and then classifies the position.
Returns
-------
intersection : [bool, bool, bool]
Provides the relative position of point with respect to self edge.
1. Contains the point. It coincides with one of the edge points.
The truth values in 'intersection' are [True, False, True]
2. Contains the point. Point is fully inside the edge.
The truth values in 'intersection' are [True, False, False]
3. Point is on the extended edge.
The truth values in 'intersection' are [False, True, False]
4. Relative position of point unknown.
The truth values in 'intersection' are [False, False, False]
Examples
--------
>>> from upxo.geoEntities.point2d import Point2d
>>> from upxo.geoEntities.sline2d import Sline2d_leanest as sl2dlean
>>> e = sl2dlean.by_coord([-1, 0], [1, 0])
>>> e.contains_point([-0.5, 0])
>>> e.contains_point(Point2d(-1.1, 1))
"""
SQRT = math.sqrt
if dth.IS_CPAIR(obj):
# Entered obj is a coordinate pair
if self.y0 == self.y1:
# When edge has zero slope
pdist = abs(obj[1]-self.y0)
elif self.x0 == self.x1:
# When edge has infinite slope
pdist = abs(obj[0]-self.x0)
else:
m = self.slope
# Calculate the y-intercept of the line
yintercept = self.y0 - m * self.x0
# Calculate the perpendicular distance from the point to the line
pdist = abs(m*obj[0] - obj[1] + yintercept) / SQRT(m**2 + 1)
if pdist != 0:
intersection = [False, False, False]
else:
distances = np.array([SQRT((self.x0-obj[0])**2 +
(self.y0-obj[1])**2),
SQRT((self.x1-obj[0])**2 +
(self.y1-obj[1])**2)]
)
done = False
if any(distances == self.length) or any(distances == 0):
# Point coincides with one of the edge points
intersection = [True, False, True]
done = True
if not done:
if any(distances < self.length) and any(distances != self.length):
# Point is fully inside the edge
intersection = [True, False, False]
if any(distances > self.length):
# Point is on the extended edge
intersection = [False, True, False]
elif isinstance(obj, Point2d):
if self.y0 == self.y1:
# When edge has zero slope
pdist = abs(obj.y-self.y0)
elif self.x0 == self.x1:
# When edge has infinite slope
pdist = abs(obj.x-self.x0)
else:
m = self.slope
# Calculate the y-intercept of the line
yintercept = self.y0 - m * self.x0
# Calculate the perpendicular distance from the point to the line
pdist = abs(m*obj.x - obj.y + yintercept) / math.sqrt(m**2 + 1)
if pdist != 0:
intersection = [False, False, False]
else:
distances = np.array([SQRT((self.x0-obj.x)**2 +
(self.y0-obj.y)**2),
SQRT((self.x1-obj.x)**2 +
(self.y1-obj.y)**2)]
)
done = False
if any(distances == self.length) or any(distances == 0):
# Point coincides with one of the edge points
intersection = [True, False, True]
done = True
if not done:
if any(distances < self.length) and any(distances != self.length):
# Point is fully inside the edge
intersection = [True, False, False]
if any(distances > self.length):
# Point is on the extended edge
intersection = [False, True, False]
return intersection
[docs]
class Sline2d():
"""
Sline2d: 2D Straight line object.
Rich 2D line entity used across UPXO geometry, meshing and analysis
workflows. In addition to endpoint coordinates, this class maintains UPXO
point objects (`pnta`, `pntb`) and provides construction helpers,
geometric properties, containment checks, splitting, translation and
neighborhood utilities.
Use this class when mutable point objects, feature extraction or advanced
geometric operations are needed. For lightweight coordinate-only behavior,
use :class:`Sline2d_leanest`.
Points are:
1. start (i.e. i): x0 & y0
2. end (i.e. j): x1 & y1
Following are the creation methods:
* Default creation is by specifying __init__(x0, y0, x1, y1).
* by_coord(start, end).
* by_point_slope(point, slope).
* by_slope_intercept(slope, intercept).
* by_parametric(point1, point2, N).
* by_coeff_const(a, b, c).
* by_vector(point, xyproj).
* by_loc_len_ang(ref='i', loc=[0, 0, 0], length=1, ang=0, degree=True).
* by_perp_bisector(line, point).
* by_transform(refedge=None, shiftxy=[0, 1], rot=+45, degree=True,
rot_pnt_f=0.5).
* by_dist_bw_points(refpoint=None, points=None, f).
Following are the property attributes:
* length: Length of the line
* gradient: Gradient of the line
* mid: midpoint of the line
* ang: angle in radians
* angd: angle in degrees
* vert: True if line is vertical
* horz: True if horizontal
* lean: Return lean representation of self.
Usage
-----
from upxo.geoEntities.sline2d import Sline2d
"""
ε = 1E-8
__slots__ = ('x0', 'y0', 'x1', 'y1', 'f', 'pnta', 'pntb')
def __init__(self, x0=0, y0=0, x1=1, y1=0, pnta=None, pntb=None):
"""
Initialize a 2D line from coordinates or UPXO points.
Parameters
----------
x0, y0 : float, optional
Start point coordinates when `pnta` and `pntb` are not provided.
x1, y1 : float, optional
End point coordinates when `pnta` and `pntb` are not provided.
pnta, pntb : Point2d, optional
Start and end UPXO point objects. If both are provided, they take
precedence over coordinate inputs.
"""
if pnta is None and pntb is None:
self.x0, self.y0, self.x1, self.y1 = x0, y0, x1, y1
self.pnta, self.pntb = Point2d(x0, y0), Point2d(x1, y1)
if pnta is not None and pntb is not None:
self.pnta, self.pntb = pnta, pntb
self.x0, self.y0 = pnta.x, pnta.y
self.x1, self.y1 = pntb.x, pntb.y
def __repr__(self):
"""Repr function."""
return f'UPXO-sl2d ({round(self.x0, 6)},{round(self.y0, 6)})-({round(self.x1, 6)},{round(self.y1, 6)}). {id(self)}'
def __iter__(self):
"""Make self an iterable over its two points."""
return (i for i in ((self.x0, self.y0), (self.x1, self.y1)))
def __getitem__(self, index):
"""Make self indexable. 0: 1st point, 1: 2nd point, other: Error."""
return ((self.x0, self.y0), (self.x1, self.y1))[index]
def __eq__(self, lines):
"""Check for == @length."""
# Validate lines
return [self.length == e.length for e in lines]
def __ne__(self, lines):
"""Check for != @length."""
# No need of validations here.
return self == lines
def __lt__(self, lines):
"""
Check for < @length.
True for a line in lines if self line length is < line length.
"""
# No need of validations here.
length = self.length
return [length < line for line in lines]
def __le__(self, lines):
"""
Check for <= @length.
True for a line in lines if self line length is <= line length.
"""
# No need of validations here.
length = self.length
return [length <= line for line in lines]
def __gt__(self, lines):
"""
Check for > @length.
True for a line in lines if self line length is > line length.
"""
# No need of validations here.
length = self.length
return [length > line for line in lines]
def __ge__(self, lines):
"""
Check for >= @length.
True for a line in lines if self line length is >= line length.
"""
# No need of validations here.
length = self.length
return [length >= line for line in lines]
[docs]
@classmethod
def by_coord(cls, start, end):
"""
Create Sline2d by specifying end coordinates.
Parameters
----------
start : Starting point coordinate [x0, y0]
end : Ending point coordinate [x1, y1]
Returns
-------
Sline2d
Line connecting `start` and `end`.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
A = sl2d.by_coord([-1, 2], [3, 4])
"""
return cls(start[0], start[1], end[0], end[1])
[docs]
@classmethod
def by_p2d(cls, start, end):
"""
Create Sline2d by specifying end UPXO points.
Parameters
----------
start : Starting point
end : Ending point
Returns
-------
Sline2d
Line created from two UPXO point objects.
Examples
--------
>>> from upxo.geoEntities.point2d import Point2d as p2d
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> sl2d.by_p2d(p2d(-1, 2), p2d(3, 4))
"""
return cls(pnta=start, pntb=end)
[docs]
@classmethod
def by_MCL(cls, gradient, intercept, length):
"""
Instantiate the Sline2d using slope, intercept and length.
Parameters
----------
gradient : Slope of the 2D line
intercept : Y-intercept of the straight line
length : Length of the straight line
Returns
-------
Instance of Sline2d
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
Sline2d.by_MCL(1.0, -1, 1)
"""
if np.isinf(gradient):
x0, y0 = 0, intercept
x1, y1 = 0, y0 + length
else:
delta_x = np.sqrt(length**2 / (1 + gradient**2))
delta_y = gradient * delta_x
x0, y0 = 0, intercept
x1, y1 = x0 + delta_x, y0 + delta_y
return cls(x0, y0, x1, y1)
[docs]
@classmethod
def by_MCLC(cls, gradient, intercept, length, centre):
"""
Instantiate Sline2d using m, c and L, centred at centre-(cx, cy).
Notes
-----
MCLC stands for: Gradient (M), Y-intercept (C), Length (L), Centre (C).
Parameters
----------
gradient : Slope of the 2D line
intercept : Y-intercept of the straight line
length : Length of the straight line
centre : Proposed x- and y-location of line midpoint: (cx, cy)
Returns
-------
Instance of Sline2d
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
Sline2d.by_MCLC(1.0, 0, 2.0, (-10.0, -10.0))
Sline2d.by_MCLC(1.0, -1.0, 2.0, (-10.0, -5.0))
"""
if gradient == float('inf'):
x0 = centre[0]
y0 = centre[1] - length / 2
x1 = centre[0]
y1 = centre[1] + length / 2
else:
delta_x = length / (2 * math.sqrt(1 + gradient**2))
delta_y = gradient * delta_x
x0 = centre[0] - delta_x
y0 = centre[1] - delta_y
x1 = centre[0] + delta_x
y1 = centre[1] + delta_y
return cls(x0, y0, x1, y1)
[docs]
@classmethod
def by_parametric(cls, point1, point2, N):
"""
A line can be represented using parametric equations.
line = [[x1 + t * (x2 - x1), y1 + t * (y2 - y1)] for t in range(n)]
Notes
-----
To be developed.
"""
raise NotImplementedError("by_parametric is not yet implemented.")
[docs]
@classmethod
def by_point_dxdy(cls, start_point, dxdy):
"""
A line can be represented using a point on the line and a dir. vector.
line = [[x, y], [dx, dy]]
Parameters
----------
start_point : iterable
Starting coordinate pair `[x, y]`.
dxdy : iterable
Direction increments `[dx, dy]`.
Returns
-------
Sline2d
Line from `start_point` to `start_point + dxdy`.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
Sline2d.by_point_dxdy([1, 1], [2, 3])
"""
return cls(start_point[0], start_point[1],
start_point[0]+dxdy[0], start_point[1]+dxdy[1])
[docs]
@classmethod
def by_LFGL(cls, location=[0, 0], factor=0.0, gradient=0, length=1,
_skip_val_=False):
"""
Create Sline2d by specifying location, factor, gradient and length.
Usage
-----
from upxo.geoEntities.sline2d import Sline2d as sl2d
Notes
-----
Gradient of ``math.inf`` (vertical line) causes a known line-creation issue.
Author: Dr. Sunil Anandatheertha
Returns
-------
Sline2d
Line generated by location-factor-gradient-length specification.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
sl2d.by_LFGL(location=[0, 0], factor=0.0, gradient=0, length=1)
sl2d.by_LFGL(location=[0, 0], factor=0.0, gradient=1, length=1)
sl2d.by_LFGL(location=[0, 0], factor=0.0, gradient=-1, length=1)
sl2d.by_LFGL(location=[0, 0], factor=1.0, gradient=0, length=1)
sl2d.by_LFGL(location=[0, 0], factor=0.0, gradient=math.inf, length=1)
"""
if not _skip_val_:
# Validations
if not type(factor) in dth.dt.NUMBERS or factor < 0.0 or factor > 1.0:
raise ValueError('Invalid factor specification.')
# ---------------------------------
unit_dir = (1, gradient) / np.sqrt(1 + gradient**2)
x0, y0 = location - length*factor*unit_dir
x1, y1 = location + length*(1-factor)*unit_dir
return cls(x0, y0, x1, y1)
[docs]
@classmethod
def by_LFAL(cls, location=[0, 0], factor=0.0, angle=0, length=1,
degree=True):
"""
Create Sline2d by specifying location, factor, angle and length.
Parameters
----------
ref : Specifies which point on the line is used to specify the edge.
loc : Specifies the ref point.
length : Length of the line to be made.
angle : Angle(s) of inclination of the line. If 2D, single value. If 3D,
this specifies a list of three angles. First angle
degree: ang considered in degree if degree is True, in radians if
otherwise.
Returns
-------
Sline2d
Line generated by location-factor-angle-length specification.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
_cdef_ = sl2d.by_LFAL # Class definition i.e. class method
loc = [0, 0]
_cdef_(location=loc, factor=0.0, angle=0, length=1, degree=True)
_cdef_(location=loc, factor=1.0, angle=0, length=1, degree=True)
_cdef_(location=loc, factor=0.5, angle=0, length=1, degree=True)
_cdef_(location=loc, factor=0.0, angle=90, length=1, degree=True)
_cdef_(location=loc, factor=0.0, angle=-90, length=1, degree=True)
_cdef_(location=loc, factor=1.0, angle=-90, length=1, degree=True)
loc = [10, 10]
_cdef_(location=loc, factor=0.0, angle=45, length=1, degree=True)
_cdef_(location=loc, factor=0.0, angle=-45, length=1, degree=True)
_cdef_(location=loc, factor=1.0, angle=45, length=1, degree=True)
_cdef_(location=loc, factor=1.0, angle=-45, length=1, degree=True)
_cdef_(location=loc, factor=0.5, angle=45, length=1, degree=True)
_cdef_(location=loc, factor=0.5, angle=-45, length=1, degree=True)
_cdef_(location=loc, factor=0.2, angle=45, length=1, degree=True)
_cdef_(location=loc, factor=0.8, angle=-45, length=1, degree=True)
"""
# Validations
if not type(factor) in dth.dt.NUMBERS or factor < 0.0 or factor > 1.0:
raise ValueError('Invalid factor specification.')
# ---------------------------------
loc_x, loc_y = location
# ---------------------------------
if degree:
angle = np.radians(angle)
# ---------------------------------
dx, dy = length*np.array([np.cos(angle), np.sin(angle)])
x0, y0 = loc_x-dx*factor, loc_y-dy*factor
x1, y1 = loc_x+dx*(1-factor), loc_y+dy*(1-factor)
return cls(x0, y0, x1, y1)
[docs]
@classmethod
def by_perp_bisector(cls, line, point):
"""
Calculate and make the perpendicular bisector Sline2d b/w line and a
point.
Parameters
----------
e : Edge specification. Preferred: UPXO edge2d_leanest
p : Point specification. Preferred: UPXO point2d_leanest
Examples
--------
from upxo.geoEntities.point2d import Edge2d as e2d
from upxo.geoEntities.point2d import edge2d_leanest
from upxo.geoEntities.point2d import p2d_leanest
e = edge2d_leanest(-2, 3, 4, 5)
e[1]
p = p2d_leanest(1, 2)
from sympy import Point, Segment
s = Segment(Point(e[0], e[1]), Point(e[2], e[3]))
Notes
-----
To be developed.
"""
raise NotImplementedError("by_perp_bisector is not yet implemented.")
@property
def mid_coord(self):
"""
Return midpoint coordinates of the line.
Returns
-------
list
Midpoint as `[xmid, ymid]`.
"""
return [(self.x0+self.x1)/2, (self.y0+self.y1)/2]
@property
def mid_point(self):
"""
Return midpoint as a UPXO point object.
Returns
-------
Point2d
Midpoint represented as `Point2d`.
"""
return Point2d(*self.mid_coord)
@property
def gradient(self):
"""
Return the slope of the line.
Returns
-------
float
Gradient of the line. Returns `math.inf` for vertical lines.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> sl2d.by_coord([-1, 2], [3, 4]).gradient
>>> sl2d.by_coord([-1, -1], [1, 1]).gradient
>>> sl2d.by_coord([0, 0], [0, 1]).gradient
>>> sl2d.by_coord([0, 0], [1, 0]).gradient
"""
if self.x0 == self.x1:
return math.inf
else:
return (self.y1-self.y0)/(self.x1-self.x0)
@property
def dxdy(self):
"""
Return the length increments along x and y.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> a = sl2d(0,0, 1,1)
>>> a.dxdy
"""
return self.dx, self.dy
@property
def dx(self):
"""
Return the length increment along x.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> a = sl2d(0, 0, 1, 1)
>>> a.dx
"""
return self.x1-self.x0
@property
def dy(self):
"""
Return the length increments along y.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> a = sl2d(0, 0, 1, 1)
>>> a.dy
"""
return self.y1-self.y0
@property
def yint(self):
"""
Return the y-intercept of the line.
Returns
-------
float
Y-intercept if finite; `math.inf` for vertical lines.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> sl2d.by_coord([-1, 2], [3, 4]).yint
>>> sl2d.by_coord([-1, -1], [1, 1]).yint
>>> sl2d.by_coord([0, 0], [0, 1]).yint
>>> sl2d.by_coord([0, 0], [1, 0]).yint
>>> sl2d.by_coord([0, -1], [1, -1]).yint
"""
if self.x0 == self.x1:
return math.inf
else:
return self.y0 - self.gradient * self.x0
@property
def ang(self):
"""
Return the ccw + angle in radians.
Returns
-------
float
Angle from positive x-axis to line direction in radians.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> sl2d.by_coord([-1, 2], [3, 4]).ang
>>> sl2d.by_coord([-1, -1], [1, 1]).ang
>>> sl2d.by_coord([0, 0], [0, 1]).ang
>>> sl2d.by_coord([0, 0], [1, 0]).ang
>>> sl2d.by_coord([0, -1], [1, -1]).ang
"""
return math.atan2(self.y1-self.y0, self.x1-self.x0)
@property
def angd(self):
"""
Return the ccw + angle in degrees.
Returns
-------
float
Angle from positive x-axis to line direction in degrees.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> sl2d.by_coord([-1, 2], [3, 4]).angd
>>> sl2d.by_coord([-1, -1], [1, 1]).angd
>>> sl2d.by_coord([0, 0], [0, 1]).angd
>>> sl2d.by_coord([0, 0], [1, 0]).angd
>>> sl2d.by_coord([0, -1], [1, -1]).angd
"""
return math.degrees(self.ang)
@property
def length(self):
"""
Calculate and return self length.
Returns
-------
float
Euclidean distance between line endpoints.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> sl2d.by_coord([-1, 2], [3, 4]).length
>>> sl2d.by_coord([-1, -1], [1, 1]).length
>>> sl2d.by_coord([0, 0], [0, 1]).length
>>> sl2d.by_coord([0, 0], [1, 0]).length
>>> sl2d.by_coord([0, -1], [1, -1]).length
"""
return math.sqrt((self.x0-self.x1)**2 + (self.y0-self.y1)**2)
@property
def vert(self):
"""
Return True if the line is vertical.
Returns
-------
bool
`True` when `|x0 - x1| <= Sline2d.ε`.
"""
return abs((self.x0 - self.x1)) <= Sline2d.ε
@property
def horz(self):
"""
Return True if the line is horizontal.
Returns
-------
bool
`True` when `|y0 - y1| <= Sline2d.ε`.
"""
return abs((self.y0 - self.y1)) <= Sline2d.ε
@property
def lean(self):
"""
Return lean representation of self.
Returns
-------
Sline2d_leanest
Coordinate-only lean representation.
"""
return Sline2d_leanest(self.x0, self.y0, self.x1, self.y1)
@property
def points(self):
"""
Return point – A, mid-point and point – B
Returns
-------
list
`[pnta, mid_point, pntb]` as UPXO point objects.
"""
return [self.pnta, self.mid_point, self.pntb]
@property
def coords(self):
"""
Return coordinate array.
Returns
-------
list
Coordinates `[x0, y0, x1, y1]`.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> a = sl2d(0,0, 1,1)
>>> a.coords
"""
return [self.x0, self.y0, self.x1, self.y1]
@property
def coord_list(self):
"""
Return coordinate array.
Returns
-------
list
Endpoint coordinates `[[x0, y0], [x1, y1]]`.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> a = sl2d(0,0, 1,1)
>>> a.coord_list
"""
return [[self.x0, self.y0], [self.x1, self.y1]]
@property
def coord_i(self):
"""Return coordinates of starting point as `[x0, y0]`."""
return [self.x0, self.y0]
@property
def coord_j(self):
"""Return coordinates of ending point as `[x1, y1]`."""
return [self.x1, self.y1]
@property
def general_form(self):
"""
Return coefficients of the general form of the self.
Returns
-------
list
Coefficients `[A, B, C]` for equation `Ax + By + C = 0`.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
sl2d(0,0,0,1).general_form
sl2d(1,0,0,1).general_form
sl2d(-1.06,+10.6854,0.156,-1.685463).general_form
"""
if self.vert:
A, B, C = 1, 0, -self.x0
else:
gradient = (self.y1 - self.y0) / (self.x1 - self.x0)
A, B, C = -gradient, 1, gradient*self.x0 - self.y0
return [A, B, C]
[docs]
def flip(self):
""" Flip the line coordinates and points. MIDs of point objects
do not change, but only their coordinate values change.
Returns
-------
None
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
a = sl2d(0,0,0,1)
a.flip()
a
a.coord_list
"""
startx, starty = deepcopy(self.x0), deepcopy(self.y0)
endx, endy = deepcopy(self.x1), deepcopy(self.y1)
self.x0, self.y0, self.x1, self.y1 = endx, endy, startx, starty
self.reset_points_to_coords()
[docs]
def reset_coords_to_points(self):
"""When the points coordinates have been changed, either by
changing the coordinates of the individual points or by changing the
points itself, then use this to update the coordinates.
Returns
-------
None
"""
self.x0, self.y0 = self.pnta.x, self.pnta.y
self.x1, self.y1 = self.pntb.x, self.pntb.y
[docs]
def reset_points_to_coords(self):
"""When the coordinates of the line end points have been updated, use
this to update the point objects of the line.
Returns
-------
None
"""
self.pnta.x, self.pnta.y = self.x0, self.y0
self.pntb.x, self.pntb.y = self.x1, self.y1
[docs]
def is_point_endpoint(self, point):
"""
Return True if point is one of the end points on the line.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> sl2d(0,0,1,1).is_point_endpoint((0,0))
>>> sl2d(0,0,1,1).is_point_endpoint([1, 3])
"""
is_endpoint = False
if np.any(np.all(np.array(self.coord_list) == point, axis=1)):
is_endpoint = True
return is_endpoint
[docs]
def invert(self):
"""
Invert start and end points.
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d as sl2d
>>> a = sl2d(0,0,1,1)
>>> a.invert()
>>> a.coord_list
"""
ends = self.coord_list
self.x0, self.y0 = ends[1]
self.x1, self.y1 = ends[0]
self.reset_points_to_coords()
[docs]
def move_i(self, point):
"""
Move start point of the line to a new coordinate.
Parameters
----------
point : iterable
New start-point coordinate as `(x, y)`.
"""
self.x0, self.y0 = point
self.pnta.translate_to(point=p2d(self.x0, self.y0),
update=True, throw=False)
[docs]
def move_j(self, point):
"""
Move end point of the line to a new coordinate.
Parameters
----------
point : iterable
New end-point coordinate as `(x, y)`.
"""
self.x1, self.y1 = point
self.pntb.translate_to(point=p2d(self.x1, self.y1),
update=True, throw=False)
[docs]
def move_to_location(self, coord=None, ref='mid', saa=True, throw=False):
"""
Move the line so that the reference point lands at coord.
Parameters
----------
coord : list of float
Target ``[x, y]`` coordinate.
ref : str, optional
Reference point on the line — ``'mid'``, ``'i'``, or ``'j'``.
saa : bool, optional
If True, modify self in-place.
throw : bool, optional
If True, return the resulting line.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(0, 0, 1, 1)
line.move_to_location(coord=[0,0],ref='mid',saa=True,throw=False)
line
line = sl2d(0, 0, 1, 1)
line.move_to_location(coord=[0,0],ref='mid',saa=False,throw=True)
line
"""
if not saa and not throw:
return
# ---------------------------------------
px, py = coord
# ---------------------------------------
if ref == 'mid':
midx, midy = self.mid_coord
dx, dy = px - midx, py - midy
elif ref == 'i':
ix, iy = self.coord_i
dx, dy = px - ix, py - iy
elif ref == 'j':
jx, jy = self.coord_j
dx, dy = px - jx, py - jy
# ---------------------------------------
if saa:
self.x0, self.x1 = self.x0+dx, self.x1+dx
self.y0, self.y1 = self.y0+dy, self.y1+dy
if throw:
return self
if not saa and throw:
return Sline2d(self.x0+dx, self.x1+dx, self.y0+dy, self.y1+dy)
[docs]
def break_up(self, n):
"""
Break self into n equal sub-lines.
Parameters
----------
n : int
Number of sub-lines to create.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(-3,-5,2,2)
n = 5
line.break_up(n)
"""
n, ang, dx, dy, length = n+1, self.ang, self.dx, self.dy, self.length
x0, y0 = self.coord_i
# Make points on a unit horizontal radius vector
r = np.linspace(0, 1, n)
# Incline these vectors to the same inclination as the self.
# Scale the line.
# Then, apply the shift.
xy = np.array([x0+length*math.cos(ang)*r,
y0+length*math.sin(ang)*r])
return [Sline2d(xy[0][i], xy[1][i], xy[0][i+1], xy[1][i+1])
for i in range(n-1)]
[docs]
def fully_contains_point(self, p2d=None, method='through'):
"""
Return True if p2d lies strictly inside (not at endpoints of) self.
Parameters
----------
p2d : Point2d or coordinate pair
The point to test.
method : str, optional
``'simple'`` or ``'through'`` (default).
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
from upxo.geoEntities.point2d import Point2d
line = sl2d.by_coord([-1, 0], [1, 0])
points = [Point2d(0, 0), Point2d(-1, 0), Point2d(1, 0),
Point2d(0.2, 1), Point2d(0.2, 0), Point2d(0, 1),
Point2d(-1, 1), Point2d(1, 1)]
for point in points:
print('LINE:', line, 'POINT:', point)
print(line.fully_contains_point(point, method='through'))
"""
# Validations
# -------------------------------
if method == 'simple':
if self.perpendicular_distance(p2d) == 0:
conditiona = self.pnta.eq_fast(p2d, use_tol=False, point_spec=1)[0]
conditionb = self.pntb.eq_fast(p2d, use_tol=False, point_spec=1)[0]
if not(conditiona or conditionb):
return True
else:
return False
else:
return False
# ------------------------------
if method == 'through':
flag = self.contains_point(p2d)
if flag[0] and not flag[1] and flag[2]:
'''
Contains the point. It coincides with one of the edge points.
'''
return False
elif flag[0] and not flag[1] and not flag[2]:
'''
Contains the point. Point is fully inside the edge.
'''
return True
elif not flag[0] and flag[1] and not flag[2]:
'''
Point is on the extended edge.
'''
return False
elif not flag[0] and not flag[1] and not flag[2]:
'''
Relative position of point unknown.
'''
return False
[docs]
def contains_point(self, obj=None, return_bools=True):
"""
Assess relative positioning of a point with respect to self edge.
Output helps determine whether the point:
1. is fully contained inside the self edge
2. is coincident with one of the points of the self edge
3. is located on the extended part of the self edge
4. none of the above. Relative position unknown.
Parameters
----------
obj : coord, UPXO point2d object
Represents a point in space. The default is None.
return_bools : bool, optional
If True, return classification as a boolean triplet.
If False, return classification as integer case id in [1, 4].
Notes
-----
This method performs exact geometric checks (no tolerance parameter).
It classifies the point with respect to the finite segment and its
infinite extension.
Returns
-------
intersection : [bool, bool, bool]
Provides the relative position of point with respect to self edge.
1. Contains the point. It coincides with one of the edge points.
The truth values in 'intersection' are [True, False, True].
This is case number 1. A return_bools=False would return
1 if contains check operation results in [True, False, True].
2. Contains the point. Point is fully inside the edge.
The truth values in 'intersection' are [True, False, False].
This is case number 2. A return_bools=False would return
2 if contains check operation results in [True, False, False].
3. Point is on the extended edge.
The truth values in 'intersection' are [False, True, False].
This is case number 3. A return_bools=False would return
3 if contains check operation results in [False, True, False].
4. Relative position of point unknown.
The truth values in 'intersection' are [False, False, False].
This is case number 4. A return_bools=False would return
4 if contains check operation results in [False, False, False].
Examples
--------
.. code-block:: python
from upxo.geoEntities.point2d import Point2d
from upxo.geoEntities.sline2d import Sline2d
pnta, pntb = Point2d(-1, 0), Point2d(1, 0)
e = Sline2d.by_p2d(pnta, pntb)
e.contains_point([-0.5, 0])
e.contains_point([-0.5, 0], return_bools=True)
e.contains_point([-0.5, 0], return_bools=False)
e.contains_point([0, 0])
e.contains_point([-1, 0])
e.contains_point([1, 0])
e.contains_point([-1.1, 0])
e.contains_point([-1.1, 1])
e.contains_point(Point2d(-0.5, 0))
e.contains_point(Point2d(0, 0))
e.contains_point(Point2d(-1, 0))
e.contains_point(Point2d(1, 0))
e.contains_point(Point2d(-1.1, 0))
e.contains_point(Point2d(-1.1, 1))
e = Sline2d(pnta=Point2d(1, 0), pntb=Point2d(1, 0))
e.contains_point([0.8, 0.2], return_bools=False)
e.contains_point(Point2d(0.8, 0.2))
e.contains_point(Point2d(0.8, 0.2), return_bools=False)
"""
SQRT = math.sqrt
if dth.IS_CPAIR(obj):
# Entered obj is a coordinate pair
if self.pnta.y == self.pntb.y:
# When edge has zero slope
pdist = abs(obj[1]-self.pnta.y)
elif self.pnta.x == self.pntb.x:
# When edge has infinite slope
pdist = abs(obj[0]-self.pnta.x)
else:
pdist = self.perpendicular_distance(Point2d(obj[0], obj[1]))
'''
m = self.gradient
# Calculate the y-intercept of the line
yintercept = self.pnta.y - m * self.pnta.x
# Calculate the perpendicular distance from the point to the line
pdist = abs(m*obj[0] - obj[1] + yintercept) / SQRT(m**2 + 1)
'''
if pdist != 0:
intersection = [False, False, False]
else:
distances = np.array([SQRT((self.pnta.x-obj[0])**2 +
(self.pnta.y-obj[1])**2),
SQRT((self.pntb.x-obj[0])**2 +
(self.pntb.y-obj[1])**2)]
)
done = False
if any(distances == self.length) or any(distances == 0):
# Point coincides with one of the edge points
intersection = [True, False, True]
done = True
if not done:
if any(distances < self.length) and any(distances != self.length):
# Point is fully inside the edge
intersection = [True, False, False]
if any(distances > self.length):
# Point is on the extended edge
intersection = [False, True, False]
elif isinstance(obj, Point2d) or obj.__class__.__name__ == 'Point2d':
if self.pnta.y == self.pntb.y:
# When edge has zero slope
pdist = abs(obj.y-self.pnta.y)
elif self.pnta.x == self.pntb.x:
# When edge has infinite slope
pdist = abs(obj.x-self.pnta.x)
else:
pdist = self.perpendicular_distance(Point2d(obj.x, obj.y))
if pdist != 0:
intersection = [False, False, False]
else:
distances = np.array([SQRT((self.pnta.x-obj.x)**2 +
(self.pnta.y-obj.y)**2),
SQRT((self.pntb.x-obj.x)**2 +
(self.pntb.y-obj.y)**2)]
)
done = False
if any(distances == self.length) or any(distances == 0):
# Point coincides with one of the edge points
intersection = [True, False, True]
done = True
if not done:
if any(distances < self.length) and any(distances != self.length):
# Point is fully inside the edge
intersection = [True, False, False]
if any(distances > self.length):
# Point is on the extended edge
intersection = [False, True, False]
if return_bools:
intersection = intersection
else:
choices = np.array([[True, False, True],
[True, False, False],
[False, True, False],
[False, False, False]])
intersection = np.array(intersection)
ind = np.where((choices == intersection).all(axis=1))[0][0]
intersection = ind + 1
return intersection
[docs]
def perpendicular_distance(self, point):
"""
Compute perpendicular distance from a point to the infinite line.
Parameters
----------
point : Point2d
Point for distance evaluation.
Returns
-------
float
Perpendicular distance from `point` to the line through self.
"""
m = self.gradient
# Calculate the y-intercept of the line
yintercept = self.pnta.y - m * self.pnta.x
# Calculate the perpendicular distance from the point to the line
pdist = abs(m*point.x - point.y + yintercept) / math.sqrt(m**2 + 1)
return pdist
[docs]
def contains_sl2d(self, obj=None, otype='sl2d'):
"""
Checks whether an edge is contained in self edge
Parameters
----------
obj : Multiple types
Point object or coordinate pair OR line object or pair of
coordinate pairs. Accepts following types:
* Coordinate pair * Pair of coordinate pair
* UPXO point2d * UPXO edge object
* Shapely point * Shapely line object
* VTK point * VTK line object
* GMSH point * GMSH line object
otype : str
Specify type of the object
Notes
-----
Valid `otype` values used in this method are:
* `clist` for coordinate pair-of-pairs
* `up2d` for list/tuple of two UPXO points
* `sl2d` for `Sline2d` instance
Returns
-------
tuple( list(bool, bool), list(bool, bool) )
Two truth value pairs. Description:
1st value: [bool1, bool2]
2nd value: [bool3, bool4]
All values indicate location of points on the user input edge
bool1:
True if pnta inside self edge
True if pnta coincides with any of two self edge points
False if pnta lies outside self edge
bool2:
True only if pnta of input edge lies on extended self edge
Third tuple item is a single bool indicating whether both end
points of input line are contained in self.
Notes
-----
Set up the reference edge before running examples::
from upxo.geoEntities.point2d import Point2d
from upxo.geoEntities.sline2d import Sline2d
pnta, pntb = Point2d(0, 0), Point2d(1, 0)
e = Sline2d.by_p2d(pnta, pntb)
Examples
--------
**Example 1** — both endpoints inside:
.. code-block:: python
obj = [[0.2, 0], [0.8, 0]]
k = e.contains_sl2d(obj=obj, otype='clist')
# k[0]=[True, False], k[1]=[True, False], k[2]=True
**Example 2** — one endpoint outside:
.. code-block:: python
obj = [[-0.1, 0], [1.0, 0]]
k = e.contains_sl2d(obj=obj, otype='clist')
# k[0]=[False, True], k[1]=[True, False], k[2]=False
**Example 3** — first endpoint off the line:
.. code-block:: python
obj = [[-0.1, 1], [1.0, 0]]
k = e.contains_sl2d(obj=obj, otype='clist')
# k[0]=[False, False], k[1]=[True, False], k[2]=False
**Example 4** — endpoints coincide with self endpoints:
.. code-block:: python
obj = [[0, 0], [1, 0]]
k = e.contains_sl2d(obj=obj, otype='clist')
# k[0]=[True, False], k[1]=[True, False], k[2]=True
**Example 5** — degenerate (same point twice):
.. code-block:: python
obj = [[0, 0], [0, 0]]
k = e.contains_sl2d(obj=obj, otype='clist')
# k[0]=[True, False], k[1]=[True, False], k[2]=True
**Example 6** — UPXO point objects, both inside:
.. code-block:: python
obj = [Point2d(0.2, 0), Point2d(0.8, 0)]
k = e.contains_sl2d(obj=obj, otype='up2d')
# k[0]=[True, False], k[1]=[True, False]
**Example 7** — UPXO point objects, one outside:
.. code-block:: python
obj = [Point2d(-0.1, 0), Point2d(1.0, 0)]
k = e.contains_sl2d(obj=obj, otype='up2d')
# k[0]=[False, True], k[1]=[True, False]
**Example 8** — Sline2d object, inside:
.. code-block:: python
obj = Sline2d(pnta=Point2d(0.1, 0), pntb=Point2d(0.5, 0))
k = e.contains_sl2d(obj=obj, otype='sl2d')
# k[0]=[False, True], k[1]=[True, False]
**Example 9** — Sline2d object, one endpoint outside:
.. code-block:: python
obj = Sline2d(pnta=Point2d(0.1, 0), pntb=Point2d(1.5, 0))
k = e.contains_sl2d(obj=obj, otype='sl2d')
# k[0]=[False, True], k[1]=[True, False]
"""
if obj:
if otype == 'clist':
pnta = Point2d(obj[0][0], obj[0][1])
pntb = Point2d(obj[1][0], obj[1][1])
if otype == 'up2d':
pnta, pntb = obj[0], obj[1]
if otype == 'sl2d':
pnta, pntb = obj.pnta, obj.pntb
# Evaluate contains_point
_pnta_ = self.contains_point(obj=pnta)
_pntb_ = self.contains_point(obj=pntb)
if _pnta_[0] and _pntb_[0]:
_edge_ = True
else:
_edge_ = False
else:
_pnta_, _pntb_, _edge_ = None, None, None
print('Please enter valid inputs')
return (_pnta_, _pntb_, _edge_)
[docs]
def distribute_points(self,
n=5,
spacing='constant',
factor=0,
sub_spacing='constant',
subfactors=[0, 0],
trim_ij=False,
symexp=None,
_coord_rounding_=(False, 8),
_plot_=False
):
"""
Distribute points over a straight line.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
line = Sline2d(0, 0, 0, 1)
line.distribute_points(n=9, spacing='constant', factor=0.5,
trim_ij=True, _plot_=True)
line.distribute_points(n=9, spacing='constant', factor=0,
trim_ij=True, _plot_=True)
line.distribute_points(n=[10, 10],
spacing='constant', factor=0.5,
sub_spacing=['cubic','cubic'],
subfactors=[0, 1],
trim_ij=True,
_coord_rounding_=(True, 8),
_plot_=True)
"""
import matplotlib.pyplot as plt
valid_spacing = ('constant', 'linear', 'quadratic', 'cubic',
'symbolic')
valid_subfactors = (0.0, 1.0)
NUM, ITER = dth.dt.NUMBERS, dth.dt.ITERABLES
# ----------------------------------------------------------------
# Validatios
if type(n) not in (NUM + ITER):
raise ValueError('Invalid n specified.')
# .........
if spacing not in valid_spacing:
raise ValueError('Invalid spacing specified.')
# .........
if factor < 0.0 or factor > 1.0:
raise ValueError('Invalid factor specified.')
# .........
if type(subfactors) not in (NUM + ITER):
raise ValueError('Invalid subfactors specified. 1.')
if type(subfactors) in ITER:
for sf in subfactors:
if sf not in valid_subfactors:
raise ValueError('Invalid subfactors specified. 2.')
if type(subfactors) in NUM:
subfactors = [subfactors, subfactors]
for sf in subfactors:
if sf < 0.0 or sf > 1.0:
raise ValueError('Invalid subfactors specified. 3.')
# ----------------------------------------------------------------
x0, y0 = self.coord_i
ang, length = self.ang, self.length
# .........
def apply_spacing(r, spacing, fac):
"""Map ratio array ``r`` through the chosen spacing function."""
# Valid only for factors = 0.0, 1.0
spacing_actions = {
'constant': lambda r: r,
'linear': lambda r: r,
'quadratic': lambda r: r**2 if fac == 0 else np.flip(1-r**2),
'cubic': lambda r: r**3 if fac == 0 else np.flip(1-r**3)
}
# Default to no-op
return spacing_actions.get(spacing, lambda r: r)(r)
# ----------------------------------------------------------------
if factor == 0.0 or factor == 1.0:
if type(n) in NUM:
n = n + 2
elif type(n) in ITER:
n = n[0] + 2
# Make points on a unit horizontal radius vector
r = np.linspace(0, 1, n)
r = apply_spacing(r, spacing, factor)
# ----------------------------------------------------------------
if factor > 0.0 and factor < 1.0:
# factor = 0.5
_, lines, __ = Sline2d(0, 0, 1, 0).divide_at_ratios(factor)
# .........
if type(n) in NUM:
N = [int(n/2), int(n/2)]
elif type(n) in ITER and len(n) >= 2:
N = [abs(n[0]), abs(n[1])]
# .........
if type(sub_spacing) in ITER and len(sub_spacing) >= 2:
sub_spacing0, sub_spacing1 = sub_spacing
else:
sub_spacing0, sub_spacing1 = [sub_spacing, sub_spacing]
# .........
points0 = lines[0].distribute_points(N[0], spacing=sub_spacing0,
factor=subfactors[0],
trim_ij=False)
# .........
points1 = lines[1].distribute_points(N[1], spacing=sub_spacing1,
factor=subfactors[1],
trim_ij=False)
r = np.vstack((points0, points1[1:, :]))[:, 0]
# ----------------------------------------------------------------
# Incline these vectors to the same inclination as the self.
# Scale the line. Then, apply the shift.
xy = np.array([x0+length*math.cos(ang)*r, y0+length*math.sin(ang)*r])
# ----------------------------------------------------------------
if trim_ij:
xy = xy[:, 1:-1].T
else:
xy = xy.T
# ----------------------------------------------------------------
if _coord_rounding_[0]:
# toldp: Tol on decimal places in rounding
toldp, XY = _coord_rounding_[1], []
for _xy_ in xy:
XY.append([round(_xy_[0], toldp),
round(_xy_[1], toldp)])
xy = np.array(XY)
# ----------------------------------------------------------------
if _plot_:
plt.figure(dpi=60, figsize=(5, 5))
plt.plot(xy[:, 0], xy[:, 1], 'kx', ms=8)
return xy
[docs]
def divide_at_ratios(self, ratios):
"""
Divide self into sub-lines at the specified fractional positions.
Parameters
----------
ratios : float or list of float
Fractional positions along the line in ``[0, 1]``.
Returns
-------
points : list of Point2d
lines : list of Sline2d
mullines : MSline2d
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
LINE = sl2d(-3, -5, 2, 2)
ratios = [0.2, 0.3, 0.4]
points, lines, mullines = LINE.divide_at_ratios(ratios)
mullines.lines
"""
# ---------------------------
# PREVENT CIRCULAR IMPORT
from upxo.geoEntities.mulsline2d import MSline2d
# ---------------------------
if type(ratios) not in dth.dt.ITERABLES:
ratios = [ratios]
# VALIDATIONS
# ---------------------------
ratios = list(np.sort(ratios))
if all([sf >= 0.0 and sf <= 1.0 for sf in ratios]):
if ratios[0] != 0.0:
ratios = [0.0] + ratios
if ratios[-1] != 1.0:
ratios = ratios + [1.0]
else:
raise ValueError('One or more ratios is not in [0.0, 1.0].')
# ---------------------------
points = [Point2d.from_line_factor(self, f) for f in ratios]
POINTS = [[points[i], points[i+1]] for i in range(len(points)-1)]
LINES = [Sline2d.by_p2d(point[0], point[1]) for point in POINTS]
MULLINES = MSline2d.from_lines(LINES, close=False)
return points, LINES, MULLINES
[docs]
def move(self, dx, dy):
"""
Translate line endpoints by uniform x and y increments.
Parameters
----------
dx : float
Translation along x-axis.
dy : float
Translation along y-axis.
"""
self.x0 += dx
self.x1 += dx
self.y0 += dy
self.y1 += dy
[docs]
def is_normal(self, lines, _tol_decplace_=8):
"""
Return Truth value list for normality check between self and lines.
Parameters
----------
lines : Iterable of UPXO lines.
_tol_decplace_ : Number of rounding decimal places for gradient product
check. Defaults to 8.
Examples
--------
from upxo.geoEntities.sline2d import Sline2d as sl2d
LINE = sl2d(-1.25, 1.068, 6.163, -8.012)
nv1 = LINE.normal_vector(ratio=0.0, return_type='sl2d')
nv2 = LINE.normal_vector(ratio=0.5, return_type='sl2d')
nv3 = LINE.normal_vector(ratio=1.0, return_type='sl2d')
LINE.is_normal((nv1, nv2, nv2))
"""
if type(lines) not in dth.dt.ITERABLES:
lines = [lines]
# -------------------------------
_tol_decplace_, normcheck = _tol_decplace_, []
for line in lines:
if round(self.gradient*line.gradient, _tol_decplace_) == -1:
normcheck.append(True)
else:
normcheck.append(False)
return normcheck
[docs]
def normal_vector(self, ratio=0.0, return_type='sl2d'):
"""
Find normal vector centred at starting point.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
LINE = sl2d(-1.25, 1.068, 6.163, -8.012)
nv1 = LINE.normal_vector(ratio=0.0, return_type='sl2d')
nv2 = LINE.normal_vector(ratio=0.5, return_type='sl2d')
nv3 = LINE.normal_vector(ratio=1.0, return_type='sl2d')
LINE.is_normal((nv1, nv2))
normal = LINE.normal_vector(ratio=0.5, return_type='sl2d')
LINE.is_normal(normal)
LINE.distance_to_lines(normal, refi='i', refj='all')
"""
locx, locy = Point2d.from_line_factor(self, ratio).coords
normal = Sline2d(self.x0, self.y0, self.x0-self.dy, self.y0+self.dx)
normal.move(locx-normal.mid_coord[0], locy-normal.mid_coord[1])
'''
@CHECK:
from upxo.geoEntities.sline2d import Sline2d as sl2d
LINE = sl2d(-1.25, 1.068, 6.163, -8.012)
ratio = 0.2
locx, locy = Point2d.from_line_factor(LINE, ratio).coords
normal = Sline2d(LINE.x0, LINE.y0, LINE.x0-LINE.dy, LINE.y0+LINE.dx)
normal.move(locx-normal.mid[0], locy-normal.mid[1])
print(LINE.intersection_lines([normal])[0]) # A
print([locx, locy]) # B
# If A and B are same, we are correct.
'''
# --------------------------------------
if return_type == 'coords':
return normal.coords
elif return_type == 'sl2d':
return normal
[docs]
def generate_factors_0_and_1(self, dx1, dx2, dmean, k=0.0, th_res=1e-3, max_iter=50):
"""
Generate internal split factors between two boundary offsets in [0, 1].
Parameters
----------
dx1, dx2 : float
Left and right boundary offsets from 0 and 1 respectively.
dmean : float
Target mean spacing for generated factors.
k : float, optional
Relative random perturbation factor in [0, 0.25].
th_res : float, optional
Convergence tolerance on final residual.
max_iter : int, optional
Maximum attempts for stochastic convergence.
Returns
-------
numpy.ndarray
Monotonic factors beginning at `dx1` and ending near `1-dx2`.
"""
if not (0 < dx1 < 1 and 0 < dx2 < 1 and dx1 + dx2 < 1):
raise ValueError("dx1 and dx2 must be in (0, 1), and dx1 + dx2 < 1.")
if not (0 <= k <= 0.25):
raise ValueError("k must be in [0, 0.25].")
if dmean <= 0 or th_res <= 0:
raise ValueError("dmean and th_res must be > 0.")
total_length = 1 - dx1 - dx2
for _ in range(max_iter):
n_points = max(2, int(round(total_length / dmean)))
# Create random spacings with perturbation
base_spacing = dmean
perturbations = np.random.uniform(1 - k, 1 + k, size=n_points)
raw_spacings = base_spacing * perturbations
# Normalize to match total length
scale_factor = total_length / np.sum(raw_spacings)
spacings = raw_spacings * scale_factor
# Generate points
points = np.cumsum(spacings) + dx1
if points[-1] > 1 - dx2:
continue # overshot; retry
residual = abs((1 - dx2) - points[-1])
if residual <= th_res:
points = np.append(dx1, points)
return points
raise RuntimeError(f"Unable to converge within {max_iter} iterations. Final residual too high.")
[docs]
def stretch(self, point, sf):
"""
Scale self about a fixed point by the given scale factor.
Parameters
----------
point : list of float
Fixed point ``[x, y]`` about which scaling is applied.
sf : float
Scale factor.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
line = Sline2d(0, 0, 1, 0)
line.stretch([0, 0], 1)
line
line.stretch([0, 0], 0.5)
line
line.stretch([0.5, 0], 0.5)
line
line.stretch([0, 0], 1)
line
"""
v1 = (self.coords[0]-point[0], self.coords[1]-point[1])
v2 = (self.coords[2]-point[0], self.coords[3]-point[1])
v1_scaled = (v1[0]*sf, v1[1]*sf)
v2_scaled = (v2[0]*sf, v2[1]*sf)
x0 = point[0] + v1_scaled[0]
y0 = point[1] + v1_scaled[1]
x1 = point[0] + v2_scaled[0]
y1 = point[1] + v2_scaled[1]
self.move_i((x0, y0))
self.move_j((x1, y1))
[docs]
def distribute_normal_vectors(self,
method='by_spacing',
spacing_opt={'n': [10, 10],
'spacing': 'constant',
'factor': 0.5,
'sub_spacing': ['cubic',
'cubic'],
'subfactors': [0, 1],
'trim_ij': True,
'_coord_rounding_': (True, 8),
'_plot_': True},
points=None,
ratios=None,
perform_checks=False,
_check_eps_=1E-8):
"""
Distribute normal vectors along self at selected positions.
Parameters
----------
method : str, optional
How positions are determined: ``'by_spacing'``, ``'by_points'``,
or ``'by_ratios'``. Defaults to ``'by_spacing'``.
spacing_opt : dict, optional
Options for ``'by_spacing'`` (see Examples).
points : list, optional
Pre-computed positions for ``'by_points'``.
ratios : list of float, optional
Fractional positions in ``[0, 1]`` for ``'by_ratios'``.
perform_checks : bool, optional
Validate normality of computed vectors. Defaults to False.
_check_eps_ : float, optional
Tolerance for normality check. Defaults to 1e-8.
Returns
-------
list of Sline2d
Normal vector lines placed at each selected position.
Examples
--------
**Example 1** — default spacing:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
import numpy as np
line = Sline2d(*np.random.randint(-10, 20, 4))
normals = line.distribute_normal_vectors()
line.plot(sl2d=normals)
**Example 2** — constant sub-spacing:
.. code-block:: python
spacing_opt = {'n': [10, 10], 'spacing': 'constant',
'factor': 0.25, 'sub_spacing': ['constant', 'constant'],
'subfactors': [0, 1], 'trim_ij': True,
'_coord_rounding_': (True, 8), '_plot_': True}
normals = line.distribute_normal_vectors(method='by_spacing',
spacing_opt=spacing_opt)
line.plot(sl2d=normals)
**Example 3** — quadratic sub-spacing:
.. code-block:: python
spacing_opt = {'n': [10, 10], 'spacing': 'constant',
'factor': 0.25, 'sub_spacing': ['constant', 'quadratic'],
'subfactors': [0, 1], 'trim_ij': True,
'_coord_rounding_': (True, 8), '_plot_': True}
normals = line.distribute_normal_vectors(method='by_spacing',
spacing_opt=spacing_opt)
line.plot(sl2d=normals)
**Example 4** — cubic sub-spacing:
.. code-block:: python
spacing_opt = {'n': [10, 50], 'spacing': 'constant',
'factor': 0.25, 'sub_spacing': ['constant', 'cubic'],
'subfactors': [0, 1], 'trim_ij': True,
'_coord_rounding_': (True, 8), '_plot_': True}
normals = line.distribute_normal_vectors(method='by_spacing',
spacing_opt=spacing_opt)
line.plot(sl2d=normals)
**Example 5** — using pre-distributed points:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
line = Sline2d(0, 0, 1, 0)
points = line.distribute_points(n=[10, 10], spacing='constant',
factor=0.5,
sub_spacing=['cubic', 'cubic'],
subfactors=[0, 1], trim_ij=True,
_coord_rounding_=(True, 8),
_plot_=False)
normals = line.distribute_normal_vectors(method='by_points',
points=points)
line.plot(sl2d=normals)
**Example 6** — using stochastic factor spacing:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
import numpy as np
line = Sline2d(*np.random.randint(-10, 20, 4))
f = line.generate_factors_0_and_1(dx1=0.15, dx2=0.15,
dmean=0.15, k=0.2, th_res=0.02)
points, lines, mullines = line.divide_at_ratios(f)
normals = line.distribute_normal_vectors(method='by_points',
points=points)
for normal in normals:
normal.stretch(normal.mid_coord, 10)
line.plot(sl2d=normals)
"""
from upxo._sup.validation_values import val_points_and_get_coords
if method not in ('by_spacing', 'by_points', 'by_ratios'):
return ValueError('Invalid method specified.')
# .........
NORMAL = self.normal_vector(return_type='sl2d')
if perform_checks:
d = self.distance_to_lines(NORMAL,
refi='i',
refj='ij')[0].squeeze()
if abs(d[0] - d[1]) <= _check_eps_ and self.is_normal(NORMAL):
pass
else:
raise ValueError('Calculation unsuccessful. Check _check_eps_ value.')
# ---------------------------------------------
if method == 'by_spacing':
# Define a bunch of default values
'''@developer, maintainer: For internal control only.'''
_DEF_n_ = 3
_DEF_spac_ = 'constant'
_DEF_fac_ = 0.5
_DEF_subspac_ = None
_DEF_subfac_ = None
_DEF_trim_ij_ = False
_DEF_coround_ = (True, 8)
# Distribute points on the line.
coords = self.distribute_points(n=spacing_opt.get('n', _DEF_n_),
spacing=spacing_opt.get('spacing', _DEF_spac_),
factor=spacing_opt.get('factor', _DEF_fac_),
sub_spacing=spacing_opt.get('sub_spacing', _DEF_subspac_),
subfactors=spacing_opt.get('subfactors', _DEF_subfac_),
trim_ij=spacing_opt.get('trim_ij', _DEF_trim_ij_),
_coord_rounding_=spacing_opt.get('_coord_rounding_', _DEF_coround_),
_plot_=False)
# ---------------------------------------------
if method == 'by_points':
coords = val_points_and_get_coords(points)
# ---------------------------------------------
if method == 'by_ratios':
points, _, _ = self.divide_at_ratios(ratios)
coords = val_points_and_get_coords(points[1:-1])
# ---------------------------------------------
normals = [deepcopy(NORMAL) for coord in coords]
for normal, coord in zip(normals, coords):
normal.move(coord[0]-self.x0, coord[1]-self.y0)
return normals
[docs]
def plot(self, p2d=None, sl2d=None):
"""
Quick-plot self line with optional additional points and lines.
Parameters
----------
p2d : Point2d or iterable, optional
Point(s) to overlay.
sl2d : Sline2d or iterable, optional
Line(s) to overlay.
"""
import matplotlib.pyplot as plt
plt.plot([self.x0, self.x1], [self.y0, self.y1],
'-ko', markersize=12)
if sl2d is not None:
if type(sl2d) not in dth.dt.ITERABLES:
sl2d = [sl2d]
for line in sl2d:
plt.plot([line.x0, line.x1], [line.y0, line.y1],
'--x', markersize=10)
if p2d is not None:
if type(p2d) not in dth.dt.ITERABLES:
p2d = [p2d]
for point in p2d:
plt.plot(point.x, point.y, 'b+', markersize=12)
plt.gca().set_aspect('equal')
[docs]
def distance_to_points(self, points=None, *, ref='all'):
"""
Calculate the Baudhāyana distance between self and list of points.
Parameters
----------
points : List of points
ref : Refers to point(s) location on the sline. Options include:
* 'all': uses location i, j and the mid on the line.
* 'i': starting point, (x0, y0)
* 'j': starting point, (x1, y1)
* 'mid': middle point.
Returns
-------
list or numpy.ndarray
Distances from the selected self reference location(s) to input
points. For `ref='all'`, returns three distance arrays/lists for
start, midpoint and end respectively.
Examples
--------
.. code-block:: python
import numpy as np
from upxo.geoEntities.point2d import Point2d as p2d
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(0, 0, 1, 0)
points = [p2d(xy[0], xy[1]) for xy in np.random.random((10, 2))]
line.distance_to_points(points, ref='all')
line.distance_to_points(points, ref='i')
line.distance_to_points(points, ref='mid')
line.distance_to_points(points, ref='j')
"""
if ref == 'all':
pnti, pntmid, pntj = self.points
distances = [pnti.distance(points),
pntmid.distance(points),
pntj.distance(points)]
elif ref in ('i', 'start'):
distances = self.points[0].distance(points)
elif ref in ('mid'):
distances = self.points[1].distance(points)
elif ref in ('j', 'end'):
distances = self.points[2].distance(points)
return distances
[docs]
def distance_to_lines(self, lines=None, method='ref',
refi='mid', refj='mid'):
"""
Calculate the Baudhāyana distance between self and list of edges.
Parameters
----------
lines : List of lines
method : Indicate the method of calculation. Can take following values:
* 'ref': Use the reference location specifiers to calculate
distance. That is, use refi and refj.
* 'min': Minimum Baudhāyana distance
* 'max': MAximum Baudhāyana distance
* 'mean': Mean Baudhāyana distance
refi: reference location for self i.e. i
refj: referecne location for the other edge i.e. j
Notes
-----
Current implementation computes pairwise Euclidean distances between
selected reference points only. The `method` argument is currently not
branched into `min`/`max`/`mean` logic inside this function.
Returns
-------
List of ndarray distances to all edges.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
rline = sl2d(0, 0, 1, 0)
lines = [sl2d(0, i, 1, i) for i in range(1, 10)]
rline.distance_to_lines(lines, refi='mid', refj='all')
"""
if type(lines) not in dth.dt.ITERABLES:
lines = [lines]
# ---------------------------------
if refi == 'all':
refpnts = np.array([self.coord_i, self.mid_coord, self.coord_j])
elif refi == 'i':
refpnts = np.array([self.coord_i])
elif refi == 'mid':
refpnts = np.array([self.mid_coord])
elif refi == 'j':
refpnts = np.array([self.coord_j])
elif refi == 'ij':
refpnts = np.array([self.coord_i, self.coord_j])
elif refi == 'imid':
refpnts = np.array([self.coord_i, self.mid_coord])
elif refi == 'midj':
refpnts = np.array([self.mid_coord, self.coord_j])
# ---------------------------------
if refj == 'all':
LinePoints = [np.array([line.coord_i, line.mid_coord, line.coord_j])
for line in lines]
elif refj == 'i':
LinePoints = [np.array([line.coord_i]) for line in lines]
elif refj == 'mid':
LinePoints = [np.array([line.mid_coord]) for line in lines]
elif refj == 'j':
LinePoints = [np.array([line.coord_j]) for line in lines]
elif refj == 'ij':
LinePoints = [np.array([line.coord_i, line.coord_j]) for line in lines]
elif refj == 'imid':
LinePoints = [np.array([line.coord_i, line.mid_coord]) for line in lines]
elif refj == 'midj':
LinePoints = [np.array([line.mid_coord, line.coord_j]) for line in lines]
# ---------------------------------
distances = []
for linepoints in LinePoints:
distances.append(np.linalg.norm(refpnts[:,None] - linepoints, axis=2))
# ---------------------------------
return distances
[docs]
def translate_by(self, *, vector=None, refloc = 'i', dist=None,
update=False, throw=True):
"""
Translate the Edge by a Euclidean distance.
Translate the Edge along the vector by a given distance. If
update is True, then coords of the self will be updated.
If throw is True and update is False, a new edge of the new
coordinates shall be returned. If throw is True and update is True, a
deepcopy of the self shall be returned.
Notes
-----
Phase 1: Without any validations (current implementation).
Parameters
----------
vector : Direction of translation. Two specifications allowed are:
Specification 1: [vector start point coords,
vector end point coords]
Specification 2: 'x+', 'z-'
dist : Euclidean distance. If None and not a Number, then the
translation distance will be the length of the vector. If a
number, then dist will be the translation distance, in which case
the vector will only be used to know the translation direction.
update : Update the current point if True, do not update if False.
throw : Return a edge if True, else return nothing if False.
Returns
-------
UPXO edge object: Conditional, depending on input throw (refer to
description).
Author
------
Dr. Sunil Anandatheertha
Examples
--------
Notes
-----
To be developed.
"""
raise NotImplementedError("translate_by is not yet implemented.")
[docs]
def intersection_lines(self, lines, dim=2):
"""
Notes
-----
Development milestones:
* All in lines are UPXO type 2D line objects.
* Consider 2D coordinate listings
* Consider UPXO type 3D line objects.
* Consider 3D coordinate listings
Parameters
----------
lines : Data representing 2D and/or 3D lines.
dim: dimensionality. Default to 2. Options are:
2: 2D.
3: 3D.
4: 2D/3D - indicates mixed collection in lines user input.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
lines = [sl2d.by_coord([-1, -1], [3, 3]),
sl2d.by_coord([-1, 1], [1, -1]),
sl2d.by_coord([0, 0], [1, 0]),
sl2d.by_coord([0, 0], [0, 1])]
sl2d.by_coord([0, 0], [0, 1]).intersection_lines(lines)
sl2d.by_coord([1, 0], [1, 1]).intersection_lines(lines)
"""
# Validations
# ------------------------------------
# When all are UPXO type 2D lines
gradients = [l.gradient for l in lines]
m = self.gradient
c = self.yint # b1
yintercepts = [l.yint for l in lines] # b2
x = [(yint-c)/(m-grad) for yint, grad in zip(yintercepts, gradients)]
y = [m*_x + c for _x in x]
return [[_x, _y] for _x, _y in zip(x, y)]
[docs]
def rectangle(self, width, vis=False):
"""
Return rectangle form of line.
Convert self line into rectangle of length equal to line.length and
width equal to the user specified width. Rectangle will completely
bound the line. Ednd points of line will be at midpoints of
corresponding opposite lines of the rectangle.
Parameters
----------
width: width of the rectangle.
vis: bool, optional
If True, plot a quick visualization of line and rectangle.
Returns
-------
coords: list of [p1, p2, p3, p4]. CCW from lower left point, p1.
rectangle : Shapely polygon object
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
# Horizontal lines
line = sl2d(0, 0, 1, 0)
_, r = line.rectangle(1, vis=True)
line = sl2d(1, 0, 0, 0)
_, r = line.rectangle(1, vis=True)
# Diagonal lines
line = sl2d(0, 0, 1, 1)
_, r = line.rectangle(1, vis=True)
line = sl2d(1, 1, 0, 0)
_, r = line.rectangle(1, vis=True)
line = sl2d(-1, -1, 1, 1)
_, r = line.rectangle(1, vis=True)
line = sl2d(1, -1, -1, 1)
_, r = line.rectangle(1, vis=True)
"""
from shapely.geometry import Polygon as ShPol
import matplotlib.pyplot as plt
# Validate user input
# ------------------------------------------
def splot(r):
"""Plot the bounding rectangle ``r`` for visualisation."""
x, y = r.boundary.xy
# ..........
# plt.plot(x[0], y[0], 'ro', markersize=6)
# plt.plot(x[1], y[1], 'ro', markersize=8)
# plt.plot(x[2], y[2], 'ro', markersize=10)
# plt.plot(x[3], y[3], 'ro', markersize=12)
# ..........
# plt.plot(self.x0, self.y0, 'gs', markersize=12)
# plt.plot(self.x1, self.y1, 'gs', markersize=16)
# ..........
plt.plot([self.x0, self.x1], [self.y0, self.y1], '-gs', markersize=8)
# ..........
plt.plot([x[0], x[1]], [y[0], y[1]], '--ko', markersize=6)
plt.plot([x[1], x[2]], [y[1], y[2]], '--ko', markersize=6)
plt.plot([x[2], x[3]], [y[2], y[3]], '--ko', markersize=6)
plt.plot([x[3], x[0]], [y[3], y[0]], '--ko', markersize=6)
# ------------------------------------------
hw, ang = width/2, self.ang
# Inlusivity drive: jya and upajya are for sin and cos in samskrita
hw_jya, hw_upajya = hw*math.sin(ang), hw*math.cos(ang)
# ----------------------------------------------------
if self.vert:
coords = [[self.x0-hw, self.y0], [self.x1+hw, self.y0],
[self.x1+hw, self.y1], [self.x0-hw, self.y1]]
elif self.horz:
coords = [[self.x0, self.y0-hw], [self.x1, self.y1-hw],
[self.x1, self.y1+hw], [self.x0, self.y0+hw]]
else:
coords = [[self.x0-hw_upajya, self.y0+hw_jya],
[self.x0+hw_upajya, self.y0-hw_jya],
[self.x1+hw_upajya, self.y1-hw_jya],
[self.x1-hw_upajya, self.y1+hw_jya]]
rectangle = ShPol(coords)
if vis:
splot(rectangle)
return coords, rectangle
[docs]
def identify_points_in_rectangle(self, points, width=None,
boundary_points=True, vis=False):
"""
Identify points which lie inside rectangle of the line.
For a given set of points list, this function returns the
indices of those points which are inside (and, on) the boundary of
rectangle made from the self line (using width, of course).
Parameters
----------
points: coordinates as [[x-coords],[y-coords]]
width: rectangle width
boundary_points : If True, points on the boundary of the rectangle will
be considered, else not.
vis : If True, a quick visualization will be provided. Green is for
self line, where smaller is starting point i and larger is ending
point j. Marker sizes indicating corners of rectangle follow
shapely polygon coordinate order. Black dots are points. Blue
crosses are points which satisfy the geometric condition.
NOTE: The above order may differ from UPXO line.rectangle.
Returns
-------
inside : Numpy arrau of bools. A True indicates position in input
points which satisfy the geometric criteria.
Notes
-----
The rectangle is created from `self.rectangle(width)`. If
`boundary_points=True`, points on polygon boundaries are included.
Examples
--------
**Example 1** — horizontal line:
.. code-block:: python
import numpy as np
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(0, 0, 1, 0)
_x, _y = np.arange(0, 1, 0.1), np.arange(0, 1, 0.1)
x, y = np.meshgrid(_x, _y)
inside = line.identify_points_in_rectangle(
[x.ravel(), y.ravel()], width=1, boundary_points=True, vis=True)
**Example 2** — reversed horizontal line:
.. code-block:: python
line = sl2d(-1, 0, 1, 0)
_x, _y = np.arange(0, 1, 0.1), np.arange(0, 1, 0.1)
x, y = np.meshgrid(_x, _y)
points = [x.ravel(), y.ravel()]
inside = line.identify_points_in_rectangle(points, 1, True, True)
**Example 3** — oblique line:
.. code-block:: python
line = sl2d(-1, 1, 1, 0)
x, y = np.meshgrid(np.arange(0, 1, 0.1), np.arange(0, 1, 0.1))
points = [x.ravel(), y.ravel()]
inside = line.identify_points_in_rectangle(points, 1, True, True)
**Example 4** — vertical line:
.. code-block:: python
line = sl2d(0, 0, 0, 1)
x, y = np.meshgrid(np.arange(0, 1, 0.1), np.arange(0, 1, 0.1))
points = [x.ravel(), y.ravel()]
inside = line.identify_points_in_rectangle(points, 1, True, True)
**Example 5** — diagonal line, wider grid:
.. code-block:: python
line = sl2d(1, -1, -1, 1)
x, y = np.meshgrid(np.arange(-2, 2, 0.1), np.arange(-2, 2, 0.1))
points = [x.ravel(), y.ravel()]
inside = line.identify_points_in_rectangle(points, 1, True, True)
"""
from shapely.geometry import Point as ShPnt
import matplotlib.pyplot as plt
# ------------------------------------------
# Validate: points, width=None, boundary_points=True, vis=False
# ------------------------------------------
def splot(r, x, y, inside):
"""Plot the bounding rectangle ``r`` and highlight inside/outside points."""
rx, ry = r.boundary.xy
# ..........
plt.plot([rx[0], rx[1]], [ry[0], ry[1]], '--ko', markersize=6)
plt.plot([rx[1], rx[2]], [ry[1], ry[2]], '--ko', markersize=6)
plt.plot([rx[2], rx[3]], [ry[2], ry[3]], '--ko', markersize=6)
plt.plot([rx[3], rx[0]], [ry[3], ry[0]], '--ko', markersize=6)
# ..........
plt.plot([self.x0, self.x1], [self.y0, self.y1], '-gs', markersize=8)
# ..........
plt.plot(x, y, 'k.')
plt.plot(x[inside], y[inside], 'bx')
# ------------------------------------------
_, r = self.rectangle(width)
x, y = points
shPoints = [ShPnt(_x, _y) for _x, _y in zip(x, y)]
inside = list(map(lambda shPoints: r.contains(shPoints), shPoints))
if boundary_points:
on_boundary = list(map(lambda shPoints: r.touches(shPoints),
shPoints))
inside = [_in or _onb for _in, _onb in zip(inside, on_boundary)]
if vis:
splot(r, x, y, inside)
return inside
[docs]
def translate_to(self, *, ref='i', point=None, update=False, throw=True):
"""
Translate self to the specified location.
New location is specified by point object. POint object could be
specified by an another UPXO point object or an Iterable of coords.
If throw is True and update is False, a new point of the new
coordinates shall be returned. If throw is True and update is True, a
deepcopy of the self shall be returned.
Parameters
----------
ref : Location on the edge which translates to new point. Values of ref
could be:
* 'i': Starting point of the edge
* 'j': Ending point of the edge
* 'mid': Middle point of the edge
* [x, y, (z)]: Coordinate value
point : New position. UPXO / direct point specification.
update : Update the current point if True, do not update if False.
throw : Return a point if True, else return nothing if False.
Returns
-------
UPXO point object: Conditional, depending on input throw (refer to
description).
Notes
-----
To be developed.
"""
raise NotImplementedError("translate_to is not yet implemented.")
[docs]
def rotate_about(self, *, axis=None, angle=0, degree=True,
update=False, throw=True):
"""
Rotate point about the specified axis by the specified angle.
New location is specified by point object. Point object could be
specified by an another UPXO point object or an Iterable of coords.
If throw is True and update is False, a new point of the new
coordinates shall be returned. If throw is True and update is True, a
deepcopy of the self shall be returned.
Parameters
----------
axis : Axis of rotation. Two specifications allowed are:
Specification 1: [axis start point coords,
axis end point coords]
Specification 2: 'x+', 'z-'
angle : Counter-Clockwise positive, angle of rotation
degree: angle considered in degrees if True, radians if False.
update : Update the current point if True, do not update if False.
throw : Return a point if True, else return nothing if False.
Returns
-------
UPXO point object: Conditional, depending on input throw (refer to
description).
Notes
-----
To be developed.
"""
raise NotImplementedError("rotate_about is not yet implemented.")
[docs]
def attach_mp(self, *, mp=None, name=None):
"""Attach a UPXO multi-point object and a name."""
self.mp[name] = mp
[docs]
def attach_xtal(self, *, xtals=None):
"""
Attach a list of UPXO xtal objects.
Notes
-----
To be developed.
"""
raise NotImplementedError("attach_xtal is not yet implemented.")
[docs]
def perp_distance(self, plist, ptype='coord_list'):
"""
Examples
--------
**Example 1** — coordinate pair:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(0, 0, 1, 1)
plist = (0.1, 0.1)
line.perp_distance(plist)
**Example 2** — numpy array of coordinates:
.. code-block:: python
import numpy as np
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(0, 0, 1, 0.0)
plist = np.random.random((4, 2)).T
line.perp_distance(plist)
**Example 3** — list of Point2d objects:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
from upxo.geoEntities.point2d import Point2d as p2d
line = sl2d(0, 0, 1, 0.0)
plist = [p2d(1, 1), p2d(1, 2), p2d(1, 3), p2d(1, 4), p2d(1, 5)]
line.perp_distance(plist, ptype='[point2d]')
"""
if ptype == 'coord_list':
x, y = plist
elif ptype in ('[point2d]', 'point2d', 'p2d'):
if type(plist) not in dth.dt.ITERABLES:
plist = [plist]
x, y = np.array([[p.x, p.y] for p in plist]).T
pd = abs((self.x1-self.x0)*(y-self.y0)-(x-self.x0)*(self.y1-self.y0))
pd = pd/self.length
return pd
[docs]
def find_neigh_point_by_perp_distance(self, points=None, r=0.25,
use_bounding_rec=False, epsfactor=1E2,
vis=False):
"""
Find the neighbouring point(s) in a list of points by perp distance.
Point subselection depends on whether a point is within OR on the
cut-off perpendicular distance, r.
Parameters
----------
points : Elements of points must contain the coordinates either in direct
Iterable form (such a list of [x, y] or a nparray np.array([x, y]))
OR a 2D/3D UPXO point object.
r : Cut-off perpendicular diatance.
If 0, the closest point will be looked out for.
If > 0, all points which fall in or on a circle of radius r will be
looked out for.
use_bounding_rec : bool, optional
If True, perform candidate filtering using rectangle containment
around the line with width `r` (plus optional epsilon padding).
epsfactor : float, optional
Multiplier for `Sline2d.ε` when expanding rectangle width in
bounding-rectangle mode.
vis : bool, optional
If True, plot the selected neighboring points.
Returns
-------
list
Indices in `points` satisfying selection criteria.
Empty list if no points are inside cut-off.
Examples
--------
**Example 1** — horizontal line, no bounding rectangle:
.. code-block:: python
import numpy as np
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(0, 0, 1, 0)
x, y = np.meshgrid(np.arange(0, 1, 0.1), np.arange(0, 1, 0.1))
points = (x.ravel(), y.ravel())
line.find_neigh_point_by_perp_distance(
points, 0.2, use_bounding_rec=False, epsfactor=1E6, vis=True)
**Example 2** — reversed horizontal line, with bounding rectangle:
.. code-block:: python
line = sl2d(0, 0, -1, 0)
x, y = np.meshgrid(np.arange(-0.2, 1, 0.1), np.arange(-0.2, 1, 0.1))
points = (x.ravel(), y.ravel())
line.find_neigh_point_by_perp_distance(
points, 0.2, use_bounding_rec=True, epsfactor=0, vis=True)
line.find_neigh_point_by_perp_distance(
points, 0.2, use_bounding_rec=True, epsfactor=1E7, vis=True)
**Example 3** — diagonal line:
.. code-block:: python
line = sl2d(0, 0, 1, 0.5)
x, y = np.meshgrid(np.arange(0, 1, 0.1), np.arange(0, 1, 0.1))
points = (x.ravel(), y.ravel())
line.find_neigh_point_by_perp_distance(
points, 0.2, use_bounding_rec=True, epsfactor=0, vis=True)
**Example 4** — oblique line with negative start:
.. code-block:: python
line = sl2d(-0.5, -1, 1, 0.5)
x, y = np.meshgrid(np.arange(-0.2, 1, 0.1), np.arange(-0.2, 1, 0.1))
points = (x.ravel(), y.ravel())
line.find_neigh_point_by_perp_distance(
points, 0.2, use_bounding_rec=True, epsfactor=0, vis=True)
line.find_neigh_point_by_perp_distance(
points, 0.2, use_bounding_rec=False, epsfactor=0, vis=True)
"""
import matplotlib.pyplot as plt
def splot(line, x, y, xnearest, ynearest):
"""Plot the line, all points, and nearest-neighbour highlights."""
plt.plot(line.x0, line.y0, 'gs', markersize=12)
plt.plot(line.x1, line.y1, 'gs', markersize=16)
# ..........
plt.plot(x, y, 'k.')
plt.plot(xnearest, ynearest, 'bx')
# -------------------------------------------
# Validate user inputs
# -------------------------------------------
nearest = self.perp_distance(points) <= r
# -------------------------------------------
if use_bounding_rec:
width = r + (epsfactor > 0)*epsfactor*Sline2d.ε
nearest = self.identify_points_in_rectangle(points,
width=width,
boundary_points=True,
vis=False)
# -------------------------------------------
if vis:
x, y = points
splot(self, x, y, x[nearest], y[nearest])
idx = np.argwhere(nearest).ravel()
return list(idx)
[docs]
def find_neigh_point_by_count(self, *, plist=None, n=None,
plane='xy'):
"""
Find n nearest neighbouring points in a specified list of points.
Parameters
----------
plist: Elements of plist must contain the coordinates either in direct
Iterable form (such a list of [x, y] or a nparray np.array([x, y]))
OR a 2D/3D UPXO point object.
n : Number of nearest neighbours to return. If not entered, a single
point shall be returned.
plane : Specify the plane of the self point. Only used if self is a 2D
point object. Defaults to 'xy'.
Returns
-------
Indices in plist.
Notes
-----
To be developed.
"""
raise NotImplementedError("find_neigh_point_by_count is not yet implemented.")
[docs]
def find_neigh_mulpoint_by_distance(self, *, mplist=None,
plane='xy', r=0, tolf=-1):
"""
Find the nearest UPXO multi-point in specified list of UPXO mulpoints.
If tolerance factor, tolf is provided to be -1, then, even if a single
point in a mp falls in or on r, the index of mp in mplist will be
added to the list to be returned. If 0 < tolf <= 1, then even if tolf
factor of total number of points in a mp falls inside r, then the index
of this mp in mplist will be added to the list to be returned.
Parameters
----------
mplist : Elements of plist must contain the coordinates either in direct
Iterable form (such a list of [x, y] or a nparray np.array([x, y]))
OR a 2D/3D UPXO point object.
plane : Specify the plane of the self point. Only used if self is a 2D
point object. Defaults to 'xy'.
r : Euclidean radius of search.
If 0, the closest point will be looked out for.
If > 0, all points which fall in or on a circle of radius r will be
looked out for.
Returns
-------
Indices in mplist.
Notes
-----
To be developed.
"""
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 the nearest UPXO edge in specified list of UPXO edges.
Parameters
----------
elist : Elements of elist must contain edges in either of the following
two formats:
1. 2D/3D UPXO edge objects.
2. Iterable object with elements starting point: [x0, y0, z0]
and ending point: [x1, y1, z1]
plane : Specify the plane of the self point. Only used if self is a 2D
point object. Defaults to 'xy'.
refloc : Specify the location on th edge which is to be used for
calculating the distance form the self point itself and the edge. It
can have the following options:
* 'starting'. Starting point of the edge. Alternative use: start
* 'ending'. Ending point of the edge. Alternative use: end
* 'middle'. Mid point of the edge. Alternative use: mid
* 'any'. Any point of the edge. No alternate.
* 'all'. Both start and end points of the edge. No alternate.
r : Euclidean radius of search.
If 0, the closest point will be looked out for.
If > 0, all points which fall in or on a circle of radius r will be
looked out for.
Returns
-------
Indices in mplist.
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 the nearest UPXO muledge in specified list of UPXO muledges.
Parameters
----------
melist : Elements of melist must contain medges in either of the
following single format:
1. 2D/3D UPXO medge objects.
plane : Specify the plane of the self point. Only used if self is a 2D
point object. Defaults to 'xy'.
refloc : Specify the location on th edge which is to be used for
calculating the distance form the self point itself and the edge. It
can have the following options:
* 'starting'. Starting point of the edge. Alternative use: start
* 'ending'. Ending point of the edge. Alternative use: end
* 'middle'. Mid point of the edge. Alternative use: mid
* 'any'. Any point of the edge. No alternate.
* 'all'. Both start and end points of the edge. No alternate.
r : Euclidean radius of search.
If 0, the closest point will be looked out for.
If > 0, all points which fall in or on a circle of radius r will be
looked out for.
Returns
-------
Indices in mplist.
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 the nearest UPXO xtal in specified list of UPXO xtals.
Parameters
----------
xlist : Elements of xlist must contain xtals in either of the
following three formats:
1. 2D/3D UPXO xtal objects.
2. Shapely polygon object.
3. GMSH closed region.
4. VTK polyhedra object.
plane : Specify the plane of the self point. Only used if self is a 2D
point object. Defaults to 'xy'.
refloc : Specify the location on th edge which is to be used for
calculating the distance form the self point itself and the edge. It
can have the following options:
* 'starting'. Starting point of the edge. Alternative use: start
* 'ending'. Ending point of the edge. Alternative use: end
* 'middle'. Mid point of the edge. Alternative use: mid
* 'any'. Any point of the edge. No alternate.
* 'all'. Both start and end points of the edge. No alternate.
r : Euclidean radius of search.
If 0, the closest point will be looked out for.
If > 0, all points which fall in or on a circle of radius r will be
looked out for.
Returns
-------
Indices in mplist.
Notes
-----
To be developed.
"""
raise NotImplementedError("find_neigh_xtal_by_distance is not yet implemented.")
[docs]
def find_colinear_lines(self, lines, line_repr='upxo'):
"""
Find lines collinear to self from a given collection.
Parameters
----------
lines : list
Collection of lines (UPXO objects or coordinate lists).
line_repr : str, optional
``'upxo'`` or ``'coord_list'``.
Returns
-------
list or None
Indices of collinear lines, or None if none found.
Examples
--------
**Example 1** — UPXO line objects:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(0, 0, -1, 0)
lines = [sl2d(0, 0, -1, 0), sl2d(0, 0, 1, 2),
sl2d(-1, 0, -2, 0), sl2d(4, 0, 3, 0),
sl2d(0, 0, 1, 0), sl2d(0, 1, 1, 1)]
line.find_colinear_lines(lines, line_repr='upxo')
**Example 2** — coordinate list:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(2, 7, 7, 3)
lines = [[[0, 9], [0, 1]], [[1, 8], [6, 2]],
[[3, 8], [8, 4]], [[3, 6], [8, 4]]]
line.find_colinear_lines(lines, line_repr='coord_list')
**Example 3** — coordinate list, horizontal:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(0, 0, -1, 0)
lines = [[[0, 0], [-1, 0]], [[0, 0], [1, 2]],
[[-1, 0], [-2, 0]], [[4, 0], [3, 0]],
[[0, 0], [1, 0]], [[0, 1], [1, 1]],
[[0, 0], [-1, 0]]]
line.find_colinear_lines(lines, line_repr='coord_list')
"""
parallel = self.find_parallel_lines(lines, line_repr=line_repr)
if parallel:
if line_repr == 'upxo':
midpoints = np.array([list(lines[prl].mid_coord)
for prl in parallel]).T
elif line_repr == 'coord_list':
lines = np.array(lines)
x1, y1 = lines[:, 0].T
x2, y2 = lines[:, 1].T
midpoints = np.array([[(xi+xj)/2, (yi+yj)/2] for xi, yi, xj, yj in zip(x1, y1, x2, y2)])
midpoints = midpoints[parallel].T
collinear_locs = self.perp_distance(midpoints) == 0
if any(collinear_locs):
return [parallel[i] for i in np.where(collinear_locs)[0]]
else:
return None
else:
return None
[docs]
def find_parallel_lines(self, lines, line_repr='upxo'):
"""
Find line amongst lines parallel to self.
Parameters
----------
lines: list of data representing lines
line_repr: specifies the type of line representation. Valid
options include (see examples below for specific information.):
* 'upxo'
* 'coord_list'
Returns
-------
Location indices of those lines in lines parallel to self.
Examples
--------
**Example 1** — UPXO line objects:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(0, 0, -1, 0)
lines = [sl2d(0, 0, -1, 0), sl2d(0, 0, 1, 2)]
line.find_parallel_lines(lines, line_repr='upxo')
**Example 2** — coordinate list:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(2, 7, 7, 3)
lines = [[[0, 9], [0, 1]], [[1, 8], [6, 2]],
[[2, 7], [7, 3]], [[3, 6], [8, 4]]]
line.find_parallel_lines(lines, line_repr='coord_list')
"""
# Validate user inputs
# -----------------------------------
if line_repr == 'upxo':
return [i for i, l in enumerate(lines) if self.gradient == l.gradient]
if line_repr == 'coord_list':
'''
Expected: [[X1, Y1], [X2, Y2]]
For example, lines = [[[0, 9], [0, 1]],
[[1, 8], [6, 2]],
[[2, 7], [7, 3]],
[[3, 6], [8, 4]]]
'''
lines = np.array(lines)
x1, y1 = lines[:, 0].T
x2, y2 = lines[:, 1].T
gradients = (y2-y1)/(x2-x1)
locations = np.where(gradients == self.gradient)
if locations[0].size:
return list(locations[0])
else:
return None
[docs]
def make_points(self, n, spacing='linear', threshold_factor=1.0, start='i',
store_as_feature=False, feature_replace=False, vis=False):
"""
Make n points on the line.
Parameters
----------
n : Number of points to make
spacing: mathematical spacing to apply
threshold_factor
start: spacifies the location about which point creation starts.
store_as_feature : If True, points will be stored in self.f dictionary.
Defaults to False.
feature_replace : If True, any existing feature points will be erased.
DEfaults to False.
vis : If True, result shall be visualized. Defaults to False.
Returns
-------
points:
increments:
Examples
--------
**Example 1** — linear spacing:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(-1, -1, 1, 1)
line.make_points(10, spacing='linear', threshold_factor=1.0, vis=True)
**Example 2** — quadratic spacing:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(-1, -1, 1, 1)
line.make_points(5, spacing='quadratic', threshold_factor=1.0, vis=True)
**Example 3** — reduced threshold:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(-1, -1, 1, 1)
line.make_points(5, spacing='linear', threshold_factor=0.5, vis=True)
**Example 4** — start from endpoint j:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
line = sl2d(-1, -1, 1, 1)
line.make_points(5, spacing='linear', threshold_factor=0.5,
start='j', vis=True)
"""
import matplotlib.pyplot as plt
# Validate user inputs
# ------------------------------------
def plot(points):
"""Plot the seed line endpoints and the generated seed points."""
plt.figure()
plt.plot(self.x0, self.y0, 'ks', markersize=6)
plt.plot(self.x1, self.y1, 'ks', markersize=8)
x, y = np.array([[p.x, p.y] for p in points]).T
plt.plot(x, y, 'bo', markersize=8, mfc='none')
# ------------------------------------
pi, _, pj = self.points
incr = threshold_factor/(n+1)
increments = np.arange(incr, threshold_factor-incr, incr)
# ------------------------------------
if spacing == 'linear':
pass
if spacing == 'quadratic':
increments = increments**2
# ------------------------------------
increments *= self.length
# ------------------------------------
points = []
for incr in increments:
if start == 'i':
points.append(pi.translate(vector=[pj.x, pj.y], dist=incr,
update=False, throw=True))
elif start == 'j':
points.append(pj.translate(vector=[pi.x, pi.y], dist=incr,
update=False, throw=True))
# ------------------------------------
if vis:
plot(points)
return points, increments
[docs]
def set_gmsh_props(self, prop_dict):
"""
Set dictionary of gmsh properties.
Notes
-----
To be developed.
"""
raise NotImplementedError("set_gmsh_props is not yet implemented.")
[docs]
def make_shapely(self):
"""
Return shapely point object. Only valid for 2D.
Notes
-----
To be developed.
"""
raise NotImplementedError("make_shapely is not yet implemented.")
[docs]
def make_vtk(self):
"""
Make VTK line object.
Notes
-----
To be developed.
"""
raise NotImplementedError("make_vtk is not yet implemented.")
[docs]
def generate_points(self, dxmean, pert_factor=0.0):
"""
dxmean : Mean spacing
pert_factor
Examples
--------
>>> from upxo.geoEntities.sline2d import Sline2d
>>> line = Sline2d(0, 0, 1, 1)
>>> line.generate_points(np.sqrt(2)/10, pert_factor=1)
"""
x0, y0, x1, y1 = self.coords
if dxmean <= 0:
raise ValueError("mean spacing 'dxmean' must be positive.")
dx, dy = x1-x0, y1-y0
n_points = max(1, int(np.floor(np.sqrt(dx**2+dy**2)/dxmean)))
t_values = np.linspace(0, 1, n_points+1)
pert = (np.random.random((n_points))-0.5)*dxmean*pert_factor
points = [(x0+t*dx*(1+p), y0+t*dy*(1+p))
for t, p in zip(t_values, pert)][1:]
return points
[docs]
def translate_along_normals(self, method='by_distances',
nlines=1, pert=0.0, spacing=1.0, dmax=None):
"""
Translate self along its normal direction to produce offset copies.
Parameters
----------
method : str
``'by_distances'`` or ``'by_count'``.
nlines : int, list, or nested list
Number of copies or explicit distances per side.
pert : float, optional
Random perturbation fraction ``[0, 0.25]``.
spacing : float or dict, optional
Uniform spacing or normal-distribution dict
``{'dist': 'normal', 'mean': ..., 'std_factor': ...}``.
dmax : list of float, optional
Maximum cumulative distance per side for ``'by_count'``.
Returns
-------
list of Sline2d
Offset line copies.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
line = Sline2d(-0.5, -1, 1, 0.5)
lines = line.translate_along_normals(method='by_count', nlines=[3, 2])
line.plot(sl2d=lines)
lines = line.translate_along_normals(method='by_distances',
nlines=[[1.0, 2.0], [1.5, 3.0]],
pert=0.1)
line.plot(sl2d=lines)
spacing = {'dist': 'normal', 'mean': 0.25, 'std_factor': 0.025}
lines = line.translate_along_normals(method='by_count', nlines=[5, 5],
spacing=spacing)
line.plot(sl2d=lines)
spacing = {'dist': 'normal', 'mean': 0.25, 'std_factor': 0.1015}
lines = line.translate_along_normals(method='by_count', nlines=[20, 20],
spacing=spacing, dmax=[12.5, 12.5])
line.plot(sl2d=lines)
"""
# Validations
if method not in ['by_distances', 'by_count']:
raise ValueError("Invalid method. Choose 'by_distances' or 'by_count'.")
if method == 'by_distances':
if not (isinstance(nlines, list) and len(nlines) == 2 and all(isinstance(lst, list) for lst in nlines)):
raise ValueError("For 'by_distances', nlines must be a list of two lists.")
if not (0.0 <= pert <= 0.25):
raise ValueError("Perturbation must be between 0 and 0.25.")
elif method == 'by_count':
if not (isinstance(nlines, list) and len(nlines) == 2 and all(isinstance(v, int) and v >= 0 for v in nlines)):
raise ValueError("For 'by_count', nlines must be a list of two non-negative integers.")
# ---------------------------------------------------------------------
dx, dy = self.dx, self.dy
norm = np.sqrt(dx**2+dy**2)
if norm == 0:
raise ValueError("Zero-length line.")
dx /= norm
dy /= norm
# ---------------------------------------------------------------------
lines = []
if method == 'by_distances':
for i, dist_list in enumerate(nlines):
sign = 1 if i == 0 else -1
for dist in dist_list:
if pert > 0:
perturb = dist*np.random.uniform(-pert, pert)
else:
perturb = 0
dd = sign*(dist+perturb)
nx, ny = dy*dd, -dx*dd
lines.append(Sline2d(self.x0+nx, self.y0+ny,
self.x1+nx, self.y1+ny))
if method == 'by_count':
def get_spacing(k):
"""Return the spacing distance for offset index ``k``."""
if isinstance(spacing, dict) and spacing.get("dist") == "normal":
mu = spacing.get("mean", 1.0)
std_factor = spacing.get("std_factor", 0.0)
sigma = abs(std_factor)*mu
return max(0.0, np.random.normal(mu, sigma))*k
elif isinstance(spacing, (int, float)):
return spacing * k
else:
raise ValueError("Invalid spacing. Must be float or dict with 'dist':'normal'.")
for i, count in enumerate(nlines):
sign = 1 if i == 0 else -1
max_dist = None
if isinstance(dmax, list) and len(dmax) == 2:
max_dist = dmax[i] - spacing.get("mean", 1.0) if isinstance(spacing, dict) else dmax[i] - spacing
max_dist = max(0.0, max_dist)
cum_dist, k = 0.0, 1
while k <= count:
sk = get_spacing(k)
if max_dist is not None and cum_dist+sk > max_dist:
break
cum_dist += sk
dd = sign*cum_dist
nx, ny = dy*dd, -dx*dd
lines.append(Sline2d(self.x0+nx, self.y0+ny,
self.x1+nx, self.y1+ny))
k += 1
return lines
[docs]
def translate_along_normals_2(self, method='by_distances', d=1, perturbation=0.0):
"""
Alternate normal-translation implementation (unit spacing for by_count).
Parameters
----------
method : str
``'by_distances'`` or ``'by_count'``.
d : list
Two-element list of distances or counts per side.
perturbation : float, optional
Perturbation fraction ``[0, 0.25]`` for ``'by_distances'``.
Returns
-------
list of Sline2d
Translated line copies.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
line = Sline2d(-0.5, -1, 1, 0.5)
translated_lines = line.translate_along_normals_2(
method='by_count', d=[3, 2])
line.plot(sl2d=translated_lines)
translated_lines = line.translate_along_normals_2(
method='by_distances', d=[[1.0, 2.0], [1.5, 3.0]],
perturbation=0.1)
line.plot(sl2d=translated_lines)
"""
import numpy as np
from copy import deepcopy
if method not in ['by_distances', 'by_count']:
raise ValueError("Invalid method. Choose from 'by_distances' or 'by_count'.")
if method == 'by_distances':
if not (isinstance(d, list) and len(d) == 2 and all(isinstance(sublist, list) for sublist in d)):
raise ValueError("For 'by_distances', d must be a list of two lists.")
if not (0.0 <= perturbation <= 0.25):
raise ValueError("Perturbation must be between 0 and 0.25.")
elif method == 'by_count':
if not (isinstance(d, list) and len(d) == 2 and all(isinstance(val, int) and val >= 0 for val in d)):
raise ValueError("For 'by_count', d must be a list of two non-negative integers.")
# Unit normal
dx, dy = self.dx, self.dy
norm = np.sqrt(dx ** 2 + dy ** 2)
if norm == 0:
raise ValueError("Zero-length line.")
dx /= norm
dy /= norm
translated_lines = []
if method == 'by_distances':
for i, dist_list in enumerate(d):
sign = 1 if i == 0 else -1
for dist in dist_list:
perturb = dist * np.random.uniform(-perturbation, perturbation) if perturbation > 0 else 0
dd = sign * (dist + perturb)
nx = dy * dd
ny = -dx * dd
x0_new, y0_new = self.x0 + nx, self.y0 + ny
x1_new, y1_new = self.x1 + nx, self.y1 + ny
translated_lines.append(Sline2d(x0_new, y0_new, x1_new, y1_new))
elif method == 'by_count':
spacing = 1.0 # unit distance
for i, count in enumerate(d):
sign = 1 if i == 0 else -1
for k in range(1, count + 1):
dd = sign * spacing * k
nx = dy * dd
ny = -dx * dd
x0_new, y0_new = self.x0 + nx, self.y0 + ny
x1_new, y1_new = self.x1 + nx, self.y1 + ny
translated_lines.append(Sline2d(x0_new, y0_new, x1_new, y1_new))
return translated_lines
[docs]
def translate_along_normals_1(self, d=1):
"""
Translate self by fixed distances on each normal side.
Parameters
----------
d : float or list of float
Distance(s) for each side. Scalar applies same distance both sides;
``[d0, d1]`` applies d0 on one side and d1 on the other.
Returns
-------
list of Sline2d
Two offset lines, one per normal side.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
line = Sline2d(-0.5, -1, 1, 0.5)
line.translate_along_normals_1(d=[1, 20])
line = Sline2d(-0.5, 0, 0.5, 0)
line.plot(sl2d=line.translate_along_normals_1(d=[1, 3]))
"""
# Validations
if type(d) in ITERABLES:
if not d:
raise ValueError('Invalid distance specification.')
if len(d) == 1:
if type(d[0]) in NUMBERS:
d = [d[0], d[0]]
else:
raise ValueError('Invalid distance specification.')
else:
if type(d) in NUMBERS:
d = [d, d]
else:
raise ValueError('Invalid distance specification.')
# -------------------------------------------
dx, dy = self.dx, self.dy
norm = np.sqrt(dx**2 + dy**2)
if norm > 0:
dx /= norm
dy /= norm
normal1_x = dy * d[0]
normal1_y = -dx * d[0]
normal2_x = -dy * d[1]
normal2_y = dx * d[1]
translated_lines = []
for normal_x, normal_y in [(normal1_x, normal1_y), (normal2_x, normal2_y)]:
x_new0 = self.x0 + normal_x
y_new0 = self.y0 + normal_y
x_new1 = self.x1 + normal_x
y_new1 = self.y1 + normal_y
translated_lines.append(Sline2d(x_new0, y_new0, x_new1, y_new1))
return translated_lines
[docs]
def array_translation(self,
ncopies=10,
vector=[0, 1],
spacing='constant',
trim_self=True
):
"""
Make an array of points by repeat-translating self.
Parameters
----------
ncopies:
vector:
spacing:
Returns
-------
list of point objects
Examples
--------
**Example 1** — translate along y-axis:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
line = Sline2d(-0.5, -1, 1, 0.5)
lines = line.array_translation(ncopies=2, vector=[0, 1],
spacing='constant')
line.plot(sl2d=lines)
**Example 2** — translate along normal direction:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
line = Sline2d(-0.5, -1, 1, 0.5)
nv = line.normal_vector(ratio=0.0, return_type='sl2d')
lines = line.array_translation(ncopies=1, vector=[nv.x0, nv.y0],
spacing='constant', trim_self=True)
line.plot(sl2d=lines)
**Example 3** — translate along both normal directions:
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d
line = Sline2d(-0.5, -1, 1, 0.5)
nv = line.normal_vector(ratio=0.0, return_type='sl2d')
lines_A = line.array_translation(ncopies=1, vector=[nv.x0, nv.y0],
spacing='constant', trim_self=True)
lines_B = line.array_translation(ncopies=1, vector=[-nv.x0, -nv.y0],
spacing='constant', trim_self=True)
line.plot(sl2d=lines_A + lines_B)
"""
if not ncopies or not isinstance(ncopies, int) or ncopies < 0:
raise ValueError('ncopies must be int > 0.')
# More validations
# ------------------------------------
center_x, center_y = self.mid_coord
dx, dy = vector
# ------------------------------------
if dx == 0:
DX = [0 for _ in range(ncopies)]
else:
DX = np.arange(0, center_x+ncopies*dx, dx)
# ------------------------------------
if dy == 0:
DY = [0 for _ in range(ncopies)]
else:
DY = np.arange(0, center_y+ncopies*dy, dy)
# ------------------------------------
lines = [Sline2d(self.x0+dx, self.y0+dy, self.x1+dx, self.y1+dy)
for dx, dy in zip(DX, DY)]
# ------------------------------------
if trim_self:
lines = lines[1:]
return lines
[docs]
def lies_on_which_edge(self, *, elist=None, consider_ends=True):
"""
Get indices from a list of edges, which contain the self point.
The point could lie on the end points of an edge or in-between the
two end points of an edge.
Parameters
----------
elist: list of edge objects
consider_ends : If True, index of the edge containing the selfpoint
coordinates at one of its end points will also be returned. If False,
the index will be included only if the point is not on the end
points but completely inside the edge's end points.
Returns
-------
List of indices of points which satisfy the condition.
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):
"""
Get indices from a list of xtals, which contain the self point.
The point could lie inside the xtal, on the boundaries of the xtal
or on one of the end points of the many edges of the xtal.
Parameters
----------
xlist: list of edge objects
consider_boundary : If True, search will be carried out to see if the
self point lies on one of the boundary edges of the xtal. How this
search behaves is decided by consider_boundary_ends.
consider_boundary_ends : If True, index of the xtal containing the
selfpoint coordinates at one of the end points of its the xtal's
many boundary edges will be returned. If False, the index will be
included only if the point is not on the end points but completely
inside the edge's end points, provided that the cosider_boundary is
True.
Returns
-------
List of indices of points which satisfy the condition.
Notes
-----
To be developed.
"""
raise NotImplementedError("lies_in_which_xtal is not yet implemented.")
[docs]
def split(self, method='byfactor', f=0.5, divider=None,
saa=False, throw=True, update='pntb',
perform_containment_check=True):
"""
Split the self.line at location(s) specified.
Parameters
----------
method: str
Split specification mode. Supported values in current
implementation are `factor`, `p2d` and `coord`.
f: specifies the locations. Can be a numeric value or an iterable of
numerical values. All values must be in the domain (0, 1).
divider:
Divider location used by `p2d` and `coord` modes.
saa: bool
If True, modifies current object and creates complementary segment.
throw: bool
If True, returns `(self, new_line)` after split.
update: str
Which endpoint of self is retained/updated (`pnta` or `pntb`).
perform_containment_check: bool
If True, validates divider is fully on and within self.
Returns
-------
tuple
`(self, new_line)` when `throw=True` and split succeeds.
Examples
--------
.. code-block:: python
from upxo.geoEntities.sline2d import Sline2d as sl2d
from upxo.geoEntities.point2d import Point2d
line = sl2d(0, 0, 1, 0)
line.split(method='factor', f=0.75, saa=True, throw=True, update='pnta')
line = sl2d(0, 0, 1, 0)
line.split(method='factor', f=0.75, saa=True, throw=True, update='pntb')
line = sl2d(0, 0, 1, 0)
line.split(method='p2d', divider=Point2d(0.05, 0),
saa=True, throw=True, update='pntb')
line = sl2d(0, 0, 1, 0)
line.split(method='p2d', divider=Point2d(0.05, 0),
saa=True, throw=True, update='pnta')
line = sl2d(0, 0, 1, 0)
line.split(method='p2d', divider=Point2d(0.00, 0),
saa=True, throw=True, update='pntb')
line = sl2d(0, 0, 1, 0)
line.split(method='coord', divider=(0.05, 0),
saa=True, throw=True, update='pnta')
line = sl2d(0, 0, 1, 0)
line.split(method='coord', divider=(0.0, 0),
saa=True, throw=True, update='pnta')
"""
if method=='factor':
if type(f) not in dth.dt.NUMBERS:
raise TypeError('Invalid type spec for factor f.')
if f > 0.0 and f < 1.0:
point = (self.x0+f*self.dx, self.y0+f*self.dy)
if saa:
if update == 'pnta':
startpoint = deepcopy(self.coord_list[0])
self.x0, self.y0 = point
self.pnta.x, self.pnta.y = point
new_line = Sline2d.by_coord(startpoint, point)
elif update == 'pntb':
endpoint = deepcopy(self.coord_list[1])
self.x1, self.y1 = point
self.pntb.x, self.pntb.y = point
new_line = Sline2d.by_coord(point, endpoint)
if throw:
return self, new_line
else:
print('Point not fully inside line.')
# -----------------------------------------------------
if method=='p2d':
# Validations
# -----------------------------
check_pass = True
# Calculate the relative position and assess whether to proceed.
if perform_containment_check:
check_pass = self.fully_contains_point(divider)
# -----------------------------
if check_pass:
dist_to_pnta = self.pnta.distance(plist=[divider])[0]
factor = dist_to_pnta / self.length
if factor > 0.0 and factor < 1.0:
if saa:
if update == 'pnta':
startpoint = deepcopy(self.coord_list[0])
self.x0, self.y0 = divider.x, divider.y
self.pnta = divider
new_line = Sline2d.by_p2d(Point2d(startpoint[0],
startpoint[1]
),
divider)
elif update == 'pntb':
endpoint = deepcopy(self.coord_list[1])
self.x1, self.y1 = divider.x, divider.y
self.pntb = divider
new_line = Sline2d.by_p2d(divider,
Point2d(endpoint[0],
endpoint[1]
))
if throw:
return self, new_line
else:
print('Point not fully inside line.')
else:
raise ValueError('divider point not fully on and inside line.')
# -----------------------------------------------------
if method=='coord':
# Validations
# -----------------------------
return self.split(method='p2d',
divider=Point2d(divider[0], divider[1],
plane='xy'),
saa=saa, throw=throw, update=update)