pfc-geometry 0.2.16__tar.gz → 0.2.19__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/PKG-INFO +1 -1
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/pyproject.toml +1 -1
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/__init__.py +1 -10
- pfc_geometry-0.2.19/src/geometry/angles.py +19 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/base.py +3 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/coordinate_frame.py +33 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/point.py +48 -11
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/quaternion.py +1 -1
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/transformation.py +12 -7
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/utils.py +5 -2
- pfc_geometry-0.2.19/tests/test_angles.py +18 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/.dockerignore +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/.github/workflows/publish_pypi.yml +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/.gitignore +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/LICENSE +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/README.md +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/checks.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/gps.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/mass.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/py.typed +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/src/geometry/time.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/__init__.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/test_base.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/test_coord.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/test_gps.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/test_mass.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/test_point.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/test_quaternion.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/test_remove_outliers.csv +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/test_time.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/test_transform.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/tests/test_utils.py +0 -0
- {pfc_geometry-0.2.16 → pfc_geometry-0.2.19}/uv.lock +0 -0
|
@@ -11,6 +11,7 @@ this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
11
11
|
"""
|
|
12
12
|
from .base import Base
|
|
13
13
|
from .time import Time
|
|
14
|
+
from . import angles as angles
|
|
14
15
|
from .point import *
|
|
15
16
|
from .quaternion import *
|
|
16
17
|
from .gps import GPS
|
|
@@ -27,13 +28,3 @@ def Euldeg(*args, **kwargs) -> Quaternion:
|
|
|
27
28
|
return Quaternion.from_euler(Point(*args, **kwargs).radians())
|
|
28
29
|
|
|
29
30
|
|
|
30
|
-
def angle_diff(a, b):
|
|
31
|
-
d1 = a - b
|
|
32
|
-
d2 = d1 - 2 * np.pi
|
|
33
|
-
bd=np.abs(d2) < np.abs(d1)
|
|
34
|
-
d1[bd] = d2[bd]
|
|
35
|
-
d3 = d1 + 2 * np.pi
|
|
36
|
-
bd=np.abs(d3) < np.abs(d1)
|
|
37
|
-
d1[bd] = d3[bd]
|
|
38
|
-
|
|
39
|
-
return d1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numpy.typing as npt
|
|
3
|
+
|
|
4
|
+
def unwind(angles: npt.NDArray, center: float = 0.0):
|
|
5
|
+
"""given an angle, unwind it to the range [-pi, pi] around a center point."""
|
|
6
|
+
turns = np.round((angles - center) / (2 * np.pi))
|
|
7
|
+
return angles - turns * 2 * np.pi
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def difference(a, b):
|
|
11
|
+
d1 = a - b
|
|
12
|
+
d2 = d1 - 2 * np.pi
|
|
13
|
+
bd=np.abs(d2) < np.abs(d1)
|
|
14
|
+
d1[bd] = d2[bd]
|
|
15
|
+
d3 = d1 + 2 * np.pi
|
|
16
|
+
bd=np.abs(d3) < np.abs(d1)
|
|
17
|
+
d1[bd] = d3[bd]
|
|
18
|
+
|
|
19
|
+
return d1
|
|
@@ -252,6 +252,9 @@ class Base:
|
|
|
252
252
|
def __neg__(self) -> Self:
|
|
253
253
|
return self.__class__(-self.data)
|
|
254
254
|
|
|
255
|
+
def __pow__(self, power: Number) -> Self:
|
|
256
|
+
return self.__class__(self.data ** power)
|
|
257
|
+
|
|
255
258
|
@dprep
|
|
256
259
|
def dot(self, other: Self) -> Self:
|
|
257
260
|
return np.einsum("ij,ij->i", self.data, other)
|
|
@@ -87,3 +87,36 @@ class Coord(Base):
|
|
|
87
87
|
|
|
88
88
|
def axes(self):
|
|
89
89
|
return Point.concatenate([self.x_axis, self.y_axis, self.z_axis])
|
|
90
|
+
|
|
91
|
+
def plot(self, fig=None, scale=1, label: str = None):
|
|
92
|
+
import plotly.graph_objects as go
|
|
93
|
+
if fig is None:
|
|
94
|
+
fig = go.Figure(layout=dict(scene=dict(aspectmode="data")))
|
|
95
|
+
|
|
96
|
+
if len(self) > 1:
|
|
97
|
+
for c in self:
|
|
98
|
+
fig = c.plot(fig)
|
|
99
|
+
return fig
|
|
100
|
+
fig.add_trace(
|
|
101
|
+
go.Scatter3d(
|
|
102
|
+
x=self.origin.x,
|
|
103
|
+
y=self.origin.y,
|
|
104
|
+
z=self.origin.z,
|
|
105
|
+
mode="markers",
|
|
106
|
+
name="Origin",
|
|
107
|
+
marker=dict(size=5, color="black"),
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
colors = ["red", "green", "blue"]
|
|
111
|
+
for i, axis in enumerate([self.x_axis, self.y_axis, self.z_axis]):
|
|
112
|
+
fig.add_trace(
|
|
113
|
+
go.Scatter3d(
|
|
114
|
+
x=[self.origin.x[0], (self.origin.x + axis.x * scale)[0]],
|
|
115
|
+
y=[self.origin.y[0], (self.origin.y + axis.y * scale)[0]],
|
|
116
|
+
z=[self.origin.z[0], (self.origin.z + axis.z * scale)[0]],
|
|
117
|
+
mode="lines",
|
|
118
|
+
name=f"{label or 'Axis'} {Point.cols[i]}",
|
|
119
|
+
line=dict(width=2, color=colors.pop(0))
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
return fig
|
|
@@ -12,6 +12,8 @@ this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
from typing import Literal
|
|
15
|
+
|
|
16
|
+
from sqlalchemy import literal
|
|
15
17
|
from .base import Base
|
|
16
18
|
import numpy as np
|
|
17
19
|
import pandas as pd
|
|
@@ -150,18 +152,45 @@ class Point(Base):
|
|
|
150
152
|
def zeros(count=1):
|
|
151
153
|
return Point(np.zeros((count, 3)))
|
|
152
154
|
|
|
155
|
+
@staticmethod
|
|
156
|
+
def circle_xy(radius: float, n: int) -> Point:
|
|
157
|
+
"""
|
|
158
|
+
Generate points on a circle in the specified plane.
|
|
159
|
+
|
|
160
|
+
:param radius: Radius of the circle.
|
|
161
|
+
:param n: Number of points to generate.
|
|
162
|
+
:return: Points on the circle as a Point object.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
|
|
166
|
+
return Point(radius * np.cos(angles), radius * np.sin(angles), np.zeros(n))
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def ellipse_xy(a: float, b: float, n: int) -> Point:
|
|
170
|
+
"""
|
|
171
|
+
Generate points on an ellipse in the specified plane.
|
|
172
|
+
|
|
173
|
+
:param a: Semi-major axis length.
|
|
174
|
+
:param b: Semi-minor axis length.
|
|
175
|
+
:param n: Number of points to generate.
|
|
176
|
+
:return: Points on the ellipse as a Point object.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
|
|
180
|
+
return Point(a * np.cos(angles), b * np.sin(angles), np.zeros(n))
|
|
181
|
+
|
|
153
182
|
def bearing(self):
|
|
154
183
|
return np.arctan2(self.y, self.x)
|
|
155
184
|
|
|
156
|
-
def plot3d(self, **kwargs):
|
|
185
|
+
def plot3d(self, fig=None, **kwargs):
|
|
157
186
|
import plotly.graph_objects as go
|
|
158
|
-
fig = go.Figure()
|
|
159
187
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
_fig = go.Figure() if fig is None else fig
|
|
189
|
+
|
|
190
|
+
_fig.add_trace(go.Scatter3d(x=self.x, y=self.y, z=self.z, **kwargs))
|
|
191
|
+
if fig is None:
|
|
192
|
+
_fig.update_layout(scene=dict(aspectmode="data"))
|
|
193
|
+
return _fig
|
|
165
194
|
|
|
166
195
|
def plotxy(self):
|
|
167
196
|
import plotly.express as px
|
|
@@ -183,6 +212,18 @@ class Point(Base):
|
|
|
183
212
|
return px.line(self.df, x="z", y="x").update_layout(
|
|
184
213
|
yaxis=dict(scaleanchor="x", scaleratio=1, title="x"), xaxis=dict(title="z")
|
|
185
214
|
)
|
|
215
|
+
def plotxz(self):
|
|
216
|
+
import plotly.express as px
|
|
217
|
+
|
|
218
|
+
return px.line(self.df, x="x", y="z").update_layout(
|
|
219
|
+
yaxis=dict(scaleanchor="x", scaleratio=1, title="x"), xaxis=dict(title="z")
|
|
220
|
+
)
|
|
221
|
+
def arbitrary_perpendicular(self) -> Point:
|
|
222
|
+
min_axes = np.argmin(np.abs(self.data), axis=1)
|
|
223
|
+
cvecs = Point.concatenate(
|
|
224
|
+
[Point(*[1 if axis == i else 0 for i in np.arange(3)]) for axis in min_axes]
|
|
225
|
+
)
|
|
226
|
+
return cross(self, cvecs)
|
|
186
227
|
|
|
187
228
|
|
|
188
229
|
def Points(*args, **kwargs):
|
|
@@ -262,10 +303,6 @@ def min_angle_between(p1: Point, p2: Point):
|
|
|
262
303
|
return np.minimum(angle, np.pi - angle)
|
|
263
304
|
|
|
264
305
|
|
|
265
|
-
def arbitrary_perpendicular(v: Point) -> Point:
|
|
266
|
-
return Point(-v.y, v.x, 0).unit()
|
|
267
|
-
|
|
268
|
-
|
|
269
306
|
def vector_norm(point: Point):
|
|
270
307
|
return abs(point)
|
|
271
308
|
|
|
@@ -12,7 +12,7 @@ this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
from .point import Point
|
|
14
14
|
from .base import Base
|
|
15
|
-
from geometry import PZ
|
|
15
|
+
from geometry.point import PZ
|
|
16
16
|
import numpy as np
|
|
17
17
|
import numpy.typing as npt
|
|
18
18
|
import pandas as pd
|
|
@@ -40,9 +40,6 @@ class Transformation(Base):
|
|
|
40
40
|
@property
|
|
41
41
|
def q(self):
|
|
42
42
|
return Quaternion(self.data[:,3:])
|
|
43
|
-
|
|
44
|
-
def offset(self, p: Point):
|
|
45
|
-
return Transformation(self.p + p, self.q)
|
|
46
43
|
|
|
47
44
|
def __getattr__(self, name):
|
|
48
45
|
if name in list("xyz"):
|
|
@@ -94,7 +91,6 @@ class Transformation(Base):
|
|
|
94
91
|
coord_b.origin - coord_a.origin,
|
|
95
92
|
-q1 * q2
|
|
96
93
|
)
|
|
97
|
-
|
|
98
94
|
|
|
99
95
|
def apply(self, oin: Point | Quaternion | Self | Coord):
|
|
100
96
|
if isinstance(oin, Point):
|
|
@@ -115,6 +111,14 @@ class Transformation(Base):
|
|
|
115
111
|
else:
|
|
116
112
|
raise TypeError(f"expected a Point or a Quaternion, got a {oin.__class__.__name__}")
|
|
117
113
|
|
|
114
|
+
def offset(self, p: Point | Self):
|
|
115
|
+
if isinstance(p, Point):
|
|
116
|
+
return Transformation(self.p + p, self.q)
|
|
117
|
+
elif isinstance(p, self.__class__):
|
|
118
|
+
return Transformation(self.p + p.p, self.q * p.q)
|
|
119
|
+
else:
|
|
120
|
+
raise TypeError(f"expected a Point or a Transformation, got a {p.__class__.__name__}")
|
|
121
|
+
|
|
118
122
|
def translate(self, point: Point):
|
|
119
123
|
return point + self.p
|
|
120
124
|
|
|
@@ -134,11 +138,12 @@ class Transformation(Base):
|
|
|
134
138
|
return outarr
|
|
135
139
|
|
|
136
140
|
|
|
137
|
-
def
|
|
141
|
+
def plot(self, fig=None, size: float=3, vis:Literal["coord", "plane"]="coord"):
|
|
138
142
|
import plotly.graph_objects as go
|
|
139
143
|
from plotting.traces import axestrace, meshes
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
if fig is None:
|
|
145
|
+
import plotting.templates
|
|
146
|
+
fig = go.Figure(layout=dict(template="generic3d+clean_paper"))
|
|
142
147
|
if vis=="coord":
|
|
143
148
|
fig.add_traces(axestrace(self, length=size))
|
|
144
149
|
elif vis=="plane":
|
|
@@ -36,12 +36,15 @@ def get_index(
|
|
|
36
36
|
value: Number,
|
|
37
37
|
missing: float | Literal["throw"] = "throw",
|
|
38
38
|
direction: Literal["forward", "backward"] = "forward",
|
|
39
|
+
increasing: bool = None
|
|
39
40
|
):
|
|
40
41
|
"""given a value, find the index of the first location in the aray,
|
|
41
42
|
if no exact match, linearly interpolate in the index
|
|
42
43
|
assumes arr is monotonic increasing
|
|
43
|
-
raise value error outside of bounds and missing == "throw", else return missing
|
|
44
|
-
increasing
|
|
44
|
+
raise value error outside of bounds and missing == "throw", else return missing
|
|
45
|
+
increasing, is the array going up or down, if not given it will be inferred from the data
|
|
46
|
+
"""
|
|
47
|
+
increasing = np.sign(np.diff(arr).mean()) if increasing is None else increasing
|
|
45
48
|
res = np.argwhere(arr == value)
|
|
46
49
|
if len(res):
|
|
47
50
|
return res[0 if direction == "forward" else -1, 0]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import geometry as g
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_unwind():
|
|
6
|
+
assert g.angles.unwind(0) == 0
|
|
7
|
+
assert g.angles.unwind(2*np.pi) == 0
|
|
8
|
+
assert g.angles.unwind(3*np.pi) == -np.pi
|
|
9
|
+
assert g.angles.unwind(-2*np.pi) == 0
|
|
10
|
+
assert g.angles.unwind(-3*np.pi) == np.pi
|
|
11
|
+
assert g.angles.unwind(3*np.pi, np.pi) == np.pi
|
|
12
|
+
assert g.angles.unwind(-np.pi, np.pi) == np.pi
|
|
13
|
+
|
|
14
|
+
def test_unwind_array():
|
|
15
|
+
np.testing.assert_array_almost_equal(
|
|
16
|
+
g.angles.unwind(np.zeros(10)),
|
|
17
|
+
np.zeros(10)
|
|
18
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|