pfc-geometry 0.2.17__tar.gz → 0.2.20__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.20}/PKG-INFO +1 -1
  2. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/pyproject.toml +1 -1
  3. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/__init__.py +1 -10
  4. pfc_geometry-0.2.20/src/geometry/angles.py +19 -0
  5. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/base.py +3 -0
  6. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/coordinate_frame.py +33 -0
  7. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/point.py +46 -11
  8. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/quaternion.py +1 -1
  9. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/transformation.py +12 -7
  10. pfc_geometry-0.2.20/tests/test_angles.py +18 -0
  11. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/.dockerignore +0 -0
  12. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/.github/workflows/publish_pypi.yml +0 -0
  13. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/.gitignore +0 -0
  14. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/LICENSE +0 -0
  15. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/README.md +0 -0
  16. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/checks.py +0 -0
  17. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/gps.py +0 -0
  18. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/mass.py +0 -0
  19. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/py.typed +0 -0
  20. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/time.py +0 -0
  21. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/src/geometry/utils.py +0 -0
  22. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/__init__.py +0 -0
  23. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/test_base.py +0 -0
  24. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/test_coord.py +0 -0
  25. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/test_gps.py +0 -0
  26. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/test_mass.py +0 -0
  27. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/test_point.py +0 -0
  28. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/test_quaternion.py +0 -0
  29. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/test_remove_outliers.csv +0 -0
  30. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/test_time.py +0 -0
  31. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/test_transform.py +0 -0
  32. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/tests/test_utils.py +0 -0
  33. {pfc_geometry-0.2.17 → pfc_geometry-0.2.20}/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.20
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.20"
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
@@ -150,18 +150,45 @@ class Point(Base):
150
150
  def zeros(count=1):
151
151
  return Point(np.zeros((count, 3)))
152
152
 
153
+ @staticmethod
154
+ def circle_xy(radius: float, n: int) -> Point:
155
+ """
156
+ Generate points on a circle in the specified plane.
157
+
158
+ :param radius: Radius of the circle.
159
+ :param n: Number of points to generate.
160
+ :return: Points on the circle as a Point object.
161
+ """
162
+
163
+ angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
164
+ return Point(radius * np.cos(angles), radius * np.sin(angles), np.zeros(n))
165
+
166
+ @staticmethod
167
+ def ellipse_xy(a: float, b: float, n: int) -> Point:
168
+ """
169
+ Generate points on an ellipse in the specified plane.
170
+
171
+ :param a: Semi-major axis length.
172
+ :param b: Semi-minor axis length.
173
+ :param n: Number of points to generate.
174
+ :return: Points on the ellipse as a Point object.
175
+ """
176
+
177
+ angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
178
+ return Point(a * np.cos(angles), b * np.sin(angles), np.zeros(n))
179
+
153
180
  def bearing(self):
154
181
  return np.arctan2(self.y, self.x)
155
182
 
156
- def plot3d(self, **kwargs):
183
+ def plot3d(self, fig=None, **kwargs):
157
184
  import plotly.graph_objects as go
158
- fig = go.Figure()
159
185
 
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
186
+ _fig = go.Figure() if fig is None else fig
187
+
188
+ _fig.add_trace(go.Scatter3d(x=self.x, y=self.y, z=self.z, **kwargs))
189
+ if fig is None:
190
+ _fig.update_layout(scene=dict(aspectmode="data"))
191
+ return _fig
165
192
 
166
193
  def plotxy(self):
167
194
  import plotly.express as px
@@ -183,6 +210,18 @@ class Point(Base):
183
210
  return px.line(self.df, x="z", y="x").update_layout(
184
211
  yaxis=dict(scaleanchor="x", scaleratio=1, title="x"), xaxis=dict(title="z")
185
212
  )
213
+ def plotxz(self):
214
+ import plotly.express as px
215
+
216
+ return px.line(self.df, x="x", y="z").update_layout(
217
+ yaxis=dict(scaleanchor="x", scaleratio=1, title="x"), xaxis=dict(title="z")
218
+ )
219
+ def arbitrary_perpendicular(self) -> Point:
220
+ min_axes = np.argmin(np.abs(self.data), axis=1)
221
+ cvecs = Point.concatenate(
222
+ [Point(*[1 if axis == i else 0 for i in np.arange(3)]) for axis in min_axes]
223
+ )
224
+ return cross(self, cvecs)
186
225
 
187
226
 
188
227
  def Points(*args, **kwargs):
@@ -262,10 +301,6 @@ def min_angle_between(p1: Point, p2: Point):
262
301
  return np.minimum(angle, np.pi - angle)
263
302
 
264
303
 
265
- def arbitrary_perpendicular(v: Point) -> Point:
266
- return Point(-v.y, v.x, 0).unit()
267
-
268
-
269
304
  def vector_norm(point: Point):
270
305
  return abs(point)
271
306
 
@@ -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