pfc-geometry 0.2.17__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.
Files changed (33) hide show
  1. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/PKG-INFO +1 -1
  2. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/pyproject.toml +1 -1
  3. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/__init__.py +1 -10
  4. pfc_geometry-0.2.19/src/geometry/angles.py +19 -0
  5. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/base.py +3 -0
  6. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/coordinate_frame.py +33 -0
  7. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/point.py +48 -11
  8. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/quaternion.py +1 -1
  9. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/transformation.py +12 -7
  10. pfc_geometry-0.2.19/tests/test_angles.py +18 -0
  11. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/.dockerignore +0 -0
  12. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/.github/workflows/publish_pypi.yml +0 -0
  13. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/.gitignore +0 -0
  14. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/LICENSE +0 -0
  15. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/README.md +0 -0
  16. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/checks.py +0 -0
  17. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/gps.py +0 -0
  18. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/mass.py +0 -0
  19. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/py.typed +0 -0
  20. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/time.py +0 -0
  21. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/src/geometry/utils.py +0 -0
  22. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/__init__.py +0 -0
  23. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/test_base.py +0 -0
  24. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/test_coord.py +0 -0
  25. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/test_gps.py +0 -0
  26. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/test_mass.py +0 -0
  27. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/test_point.py +0 -0
  28. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/test_quaternion.py +0 -0
  29. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/test_remove_outliers.csv +0 -0
  30. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/test_time.py +0 -0
  31. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/test_transform.py +0 -0
  32. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/tests/test_utils.py +0 -0
  33. {pfc_geometry-0.2.17 → pfc_geometry-0.2.19}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pfc-geometry
3
- Version: 0.2.17
3
+ Version: 0.2.19
4
4
  Summary: A library for working with 3D geometry.
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pfc-geometry"
3
- version="0.2.17"
3
+ version="0.2.19"
4
4
  description = "A library for working with 3D geometry."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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
- fig.add_trace(go.Scatter3d(x=self.x, y=self.y, z=self.z, **kwargs))
161
- fig.update_layout(
162
- scene=dict(aspectmode="data"),
163
- )
164
- return fig
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 plot_3d(self, size: float=3, vis:Literal["coord", "plane"]="coord"):
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
- import plotting.templates
141
- fig = go.Figure(layout=dict(template="generic3d+clean_paper"))
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":
@@ -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