pfc-geometry 0.2.9__tar.gz → 0.2.11__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 (35) hide show
  1. {pfc_geometry-0.2.9/pfc_geometry.egg-info → pfc_geometry-0.2.11}/PKG-INFO +1 -1
  2. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/geometry/base.py +108 -81
  3. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/geometry/coordinate_frame.py +9 -9
  4. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/geometry/point.py +10 -5
  5. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/geometry/quaternion.py +4 -1
  6. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11/pfc_geometry.egg-info}/PKG-INFO +1 -1
  7. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/.github/workflows/publish_pypi.yml +0 -0
  8. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/.gitignore +0 -0
  9. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/.vscode/settings.json +0 -0
  10. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/LICENSE +0 -0
  11. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/README.md +0 -0
  12. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/geometry/__init__.py +0 -0
  13. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/geometry/gps.py +0 -0
  14. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/geometry/mass.py +0 -0
  15. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/geometry/testing.py +0 -0
  16. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/geometry/time.py +0 -0
  17. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/geometry/transformation.py +0 -0
  18. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/pfc_geometry.egg-info/SOURCES.txt +0 -0
  19. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/pfc_geometry.egg-info/dependency_links.txt +0 -0
  20. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/pfc_geometry.egg-info/requires.txt +0 -0
  21. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/pfc_geometry.egg-info/top_level.txt +0 -0
  22. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/pyproject.toml +0 -0
  23. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/requirements-dev.txt +0 -0
  24. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/requirements.txt +0 -0
  25. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/setup.cfg +0 -0
  26. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/tests/__init__.py +0 -0
  27. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/tests/quat_body_diff_test.csv +0 -0
  28. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/tests/test_base.py +0 -0
  29. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/tests/test_coord.py +0 -0
  30. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/tests/test_gps.py +0 -0
  31. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/tests/test_mass.py +0 -0
  32. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/tests/test_point.py +0 -0
  33. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/tests/test_quaternion.py +0 -0
  34. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/tests/test_remove_outliers.csv +0 -0
  35. {pfc_geometry-0.2.9 → pfc_geometry-0.2.11}/tests/test_transform.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pfc-geometry
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Summary: A package for handling 3D geometry with a nice interface
5
5
  Author-email: Thomas David <thomasdavid0@gmail.com>
6
6
  License: GNU GPL v3
@@ -18,26 +18,31 @@ from numbers import Number
18
18
 
19
19
 
20
20
  def dprep(func):
21
- """this decorates a method that works on numpy arrays of shape equal to self.data.
22
- you can pass a nupy array or an instance of self.__class__. As long as the length
23
- is the same as self, 1, or len(self) == 1 it should construct the arguments for the decorated function.
21
+ """this decorates a method that works on numpy arrays of shape equal to self.data.
22
+ you can pass a nupy array or an instance of self.__class__. As long as the length
23
+ is the same as self, 1, or len(self) == 1 it should construct the arguments for the decorated function.
24
24
  """
25
+
25
26
  def wrapper(self, b):
26
27
  bdat = self._dprep(b)
27
-
28
+
28
29
  if len(bdat) > 1 and len(self) == 1:
29
30
  a = self.tile(len(bdat))
30
31
  else:
31
32
  a = self
32
33
  return func(a, bdat)
33
-
34
+
34
35
  return wrapper
35
36
 
37
+
36
38
  class Base:
37
- __array_priority__ = 15.0 # this is a quirk of numpy so the __r*__ methods here take priority
38
- cols=[]
39
+ __array_priority__ = (
40
+ 15.0 # this is a quirk of numpy so the __r*__ methods here take priority
41
+ )
42
+ cols = []
39
43
  from_np_base = []
40
44
  from_np = []
45
+
41
46
  def __init__(self, *args, **kwargs):
42
47
  if len(kwargs) > 0:
43
48
  if len(args) > 0:
@@ -47,23 +52,31 @@ class Base:
47
52
  elif "data" in kwargs:
48
53
  args = [kwargs["data"]]
49
54
  else:
50
- raise TypeError(f"unknown kwargs passed to {self.__class__.__name__}: {args}")
51
-
52
- if len(args)==1:
53
- if isinstance(args[0], np.ndarray) or isinstance(args[0], list): #data was passed directly
55
+ raise TypeError(
56
+ f"unknown kwargs passed to {self.__class__.__name__}: {args}"
57
+ )
58
+
59
+ if len(args) == 1:
60
+ if isinstance(args[0], np.ndarray) or isinstance(
61
+ args[0], list
62
+ ): # data was passed directly
54
63
  self.data = self.__class__._clean_data(np.array(args[0]))
55
64
 
56
65
  elif all([isinstance(a, self.__class__) for a in args[0]]):
57
- #a list of self.__class__ is passed, concatenate into one
58
- self.data = self.__class__._clean_data(np.concatenate([a.data for a in args[0]]))
59
-
66
+ # a list of self.__class__ is passed, concatenate into one
67
+ self.data = self.__class__._clean_data(
68
+ np.concatenate([a.data for a in args[0]])
69
+ )
70
+
60
71
  elif isinstance(args[0], pd.DataFrame):
61
72
  self.data = self.__class__._clean_data(np.array(args[0]))
62
73
  else:
63
- raise TypeError(f"unknown args passed to {self.__class__.__name__}: {args[0]}")
64
-
74
+ raise TypeError(
75
+ f"unknown args passed to {self.__class__.__name__}: {args[0]}"
76
+ )
77
+
65
78
  elif len(args) == len(self.__class__.cols):
66
- #three args passed, each represents a col
79
+ # three args passed, each represents a col
67
80
  if all(isinstance(arg, Number) for arg in args):
68
81
  self.data = self.__class__._clean_data(np.array(args))
69
82
  elif all(isinstance(arg, np.ndarray) for arg in args):
@@ -71,7 +84,9 @@ class Base:
71
84
  elif all(isinstance(arg, list) for arg in args):
72
85
  self.data = self.__class__._clean_data(np.array(args).T)
73
86
  elif all(isinstance(arg, pd.Series) for arg in args):
74
- self.data = self.__class__._clean_data(np.array(pd.concat(args, axis=1)))
87
+ self.data = self.__class__._clean_data(
88
+ np.array(pd.concat(args, axis=1))
89
+ )
75
90
  else:
76
91
  raise TypeError
77
92
  else:
@@ -80,16 +95,18 @@ class Base:
80
95
  @classmethod
81
96
  def _clean_data(cls, data) -> npt.NDArray[np.float64]:
82
97
  assert isinstance(data, np.ndarray)
83
- if data.dtype == 'O':
84
- raise TypeError(f'data must have homogeneous shape for {cls.__name__}, given {data.shape}')
98
+ if data.dtype == "O":
99
+ raise TypeError(
100
+ f"data must have homogeneous shape for {cls.__name__}, given {data.shape}"
101
+ )
85
102
  if len(data.shape) == 1:
86
103
  data = data.reshape(1, len(data))
87
-
104
+
88
105
  assert data.shape[1] == len(cls.cols)
89
106
  return data
90
107
 
91
108
  @classmethod
92
- def type_check(cls,a):
109
+ def type_check(cls, a):
93
110
  return a if isinstance(a, cls) else cls(a)
94
111
 
95
112
  @classmethod
@@ -99,7 +116,9 @@ class Base:
99
116
  elif len(b) == 1 and len(a) > 1:
100
117
  b = b.tile(len(a))
101
118
  elif len(a) > 1 and len(b) > 1 and not len(a) == len(b):
102
- raise TypeError(f"lengths of passed arguments must be equal or 1, got {len(a)}, {len(b)}")
119
+ raise TypeError(
120
+ f"lengths of passed arguments must be equal or 1, got {len(a)}, {len(b)}"
121
+ )
103
122
  return a, b
104
123
 
105
124
  @classmethod
@@ -108,16 +127,16 @@ class Base:
108
127
 
109
128
  def __getattr__(self, name) -> npt.NDArray[np.float64]:
110
129
  if name in self.__class__.cols:
111
- return self.data[:,self.__class__.cols.index(name)]
112
- #return res[0] if len(res) == 1 else res
130
+ return self.data[:, self.__class__.cols.index(name)]
131
+ # return res[0] if len(res) == 1 else res
113
132
  elif name in self.__class__.from_np + self.__class__.from_np_base:
114
133
  return self.__class__(getattr(np, name)(self.data))
115
134
  else:
116
135
  for col in self.__class__.cols:
117
136
  if len(name) > len(col):
118
- if name[:len(col)] == col:
137
+ if name[: len(col)] == col:
119
138
  try:
120
- id = int(name[len(col):])
139
+ id = int(name[len(col) :])
121
140
  except ValueError:
122
141
  break
123
142
  return getattr(self, col)[id]
@@ -128,31 +147,31 @@ class Base:
128
147
  return self.__class__.cols
129
148
 
130
149
  def __getitem__(self, sli) -> Self:
131
- return self.__class__(self.data[sli,:])
150
+ return self.__class__(self.data[sli, :])
132
151
 
133
- def _dprep(self, other):
134
- l , w = len(self), len(self.cols)
152
+ def _dprep(self, other):
153
+ l, w = len(self), len(self.cols)
135
154
 
136
155
  if isinstance(other, np.ndarray):
137
- if other.shape == (l,w):
156
+ if other.shape == (l, w):
138
157
  return other
139
158
  elif other.shape == (l, 1) or other.shape == (l,):
140
- return np.tile(other, (w,1)).T
159
+ return np.tile(other, (w, 1)).T
141
160
  elif other.shape == (1,):
142
- return np.full((l,w), other[0])
143
- elif l==1:
161
+ return np.full((l, w), other[0])
162
+ elif l == 1:
144
163
  if len(other.shape) == 1:
145
- return np.tile(other, (w,1)).T
164
+ return np.tile(other, (w, 1)).T
146
165
  elif other.shape[1] == w:
147
166
  return other
148
167
  else:
149
- raise ValueError(f"array shape {other.shape} not handled")
168
+ raise ValueError(f"array shape {other.shape} not handled")
150
169
  else:
151
170
  raise ValueError(f"array shape {other.shape} not handled")
152
171
  elif isinstance(other, float) or isinstance(other, int):
153
- return np.full((l,w), other)
172
+ return np.full((l, w), other)
154
173
  elif isinstance(other, Base):
155
- a,b = self.__class__.length_check(self, other)
174
+ a, b = self.__class__.length_check(self, other)
156
175
  return self._dprep(b.data)
157
176
  else:
158
177
  raise ValueError(f"unhandled datatype ({other.__class__.name})")
@@ -163,7 +182,7 @@ class Base:
163
182
  def degrees(self) -> Self:
164
183
  return self.__class__(np.degrees(self.data))
165
184
 
166
- def count(self) -> int:
185
+ def count(self) -> int:
167
186
  return len(self)
168
187
 
169
188
  def __len__(self) -> int:
@@ -171,7 +190,7 @@ class Base:
171
190
 
172
191
  @property
173
192
  def ends(self) -> Self:
174
- return self.__class__(self.data[[0,-1], :])
193
+ return self.__class__(self.data[[0, -1], :])
175
194
 
176
195
  @dprep
177
196
  def __eq__(self, other):
@@ -180,7 +199,7 @@ class Base:
180
199
  @dprep
181
200
  def __add__(self, other) -> Self:
182
201
  return self.__class__(self.data + other)
183
-
202
+
184
203
  @dprep
185
204
  def __radd__(self, other) -> Self:
186
205
  return self.__class__(other + self.data)
@@ -188,7 +207,7 @@ class Base:
188
207
  @dprep
189
208
  def __sub__(self, other) -> Self:
190
209
  return self.__class__(self.data - other)
191
-
210
+
192
211
  @dprep
193
212
  def __rsub__(self, other) -> Self:
194
213
  return self.__class__(other - self.data)
@@ -210,12 +229,14 @@ class Base:
210
229
  return self.__class__(self.data / other)
211
230
 
212
231
  def __str__(self):
213
- means = ' '.join(f'{c}_={v}' for c, v in zip(self.cols, np.mean(self.data, axis=0).round(2)))
214
- return f'{self.__class__.__name__}({means}, len={len(self)})'
232
+ means = " ".join(
233
+ f"{c}_={v}" for c, v in zip(self.cols, np.mean(self.data, axis=0).round(2))
234
+ )
235
+ return f"{self.__class__.__name__}({means}, len={len(self)})"
215
236
 
216
237
  def __abs__(self):
217
238
  return np.linalg.norm(self.data, axis=1)
218
-
239
+
219
240
  def abs(self) -> Self:
220
241
  return self.__class__(np.abs(self.data))
221
242
 
@@ -224,27 +245,23 @@ class Base:
224
245
 
225
246
  @dprep
226
247
  def dot(self, other: Self) -> Self:
227
- return np.einsum('ij,ij->i', self.data, other)
248
+ return np.einsum("ij,ij->i", self.data, other)
228
249
 
229
- def diff(self, dt:np.array) -> Self:
250
+ def diff(self, dt: np.array) -> Self:
230
251
  if not pd.api.types.is_list_like(dt):
231
252
  dt = np.full(len(self), dt)
232
253
  assert len(dt) == len(self)
233
254
  return self.__class__(
234
- np.gradient(self.data,axis=0) \
235
- / \
236
- np.tile(dt, (len(self.__class__.cols),1)).T)
255
+ np.gradient(self.data, axis=0)
256
+ / np.tile(dt, (len(self.__class__.cols), 1)).T
257
+ )
237
258
 
238
- def to_pandas(self, prefix='', suffix='', columns=None, index=None):
259
+ def to_pandas(self, prefix="", suffix="", columns=None, index=None):
239
260
  if columns is not None:
240
261
  cols = columns
241
262
  else:
242
263
  cols = [prefix + col + suffix for col in self.__class__.cols]
243
- return pd.DataFrame(
244
- self.data,
245
- columns=cols,
246
- index=index
247
- )
264
+ return pd.DataFrame(self.data, columns=cols, index=index)
248
265
 
249
266
  def tile(self, count) -> Self:
250
267
  return self.__class__(np.tile(self.data, (count, 1)))
@@ -254,14 +271,14 @@ class Base:
254
271
  return {key: getattr(self, key)[0] for key in self.cols}
255
272
  else:
256
273
  return {key: getattr(self, key) for key in self.cols}
257
-
274
+
258
275
  @classmethod
259
276
  def from_dict(Cls, data):
260
277
  return Cls(**data)
261
278
 
262
279
  def to_dicts(self):
263
- return self.to_pandas().to_dict('records')
264
-
280
+ return self.to_pandas().to_dict("records")
281
+
265
282
  @classmethod
266
283
  def from_dicts(Cls, data: dict):
267
284
  return Cls(pd.DataFrame.from_dict(data))
@@ -282,53 +299,63 @@ class Base:
282
299
  def maxloc(self):
283
300
  return self.__class__(self.data.argmax(axis=0))
284
301
 
285
-
286
302
  def cumsum(self):
287
- return self.__class__(np.cumsum(self.data,axis=0))
303
+ return self.__class__(np.cumsum(self.data, axis=0))
288
304
 
289
305
  def round(self, decimals=0):
290
306
  return self.__class__(self.data.round(decimals))
291
-
307
+
292
308
  def __repr__(self):
293
309
  return str(self)
294
-
310
+
295
311
  def copy(self):
296
312
  return self.__class__(self.data.copy())
297
-
298
313
 
299
314
  def unwrap(self, discont=np.pi):
300
315
  return self.__class__(np.unwrap(self.data, discont=discont, axis=0))
301
-
302
- def filter(self, order, cutoff, ts: np.ndarray=None):
316
+
317
+ def filter(self, order, cutoff, ts: np.ndarray = None):
303
318
  from scipy.signal import butter, freqz, filtfilt
319
+
304
320
  if ts is None:
305
321
  ts = np.array(range(len(self)))
306
322
  N = len(self)
307
323
  T = (ts[-1] - ts[0]) / N
308
324
 
309
- fs = 1/T
310
- b, a = butter(
311
- order,
312
- cutoff,
313
- fs=fs,
314
- btype='low', analog=False
315
- )
325
+ fs = 1 / T
326
+ b, a = butter(order, cutoff, fs=fs, btype="low", analog=False)
316
327
 
317
328
  return self.__class__(filtfilt(b, a, self.data, axis=0))
318
-
319
- def fft(self, ts: np.ndarray=None):
329
+
330
+ def fft(self, ts: np.ndarray = None):
320
331
  from scipy.fft import fft, fftfreq
332
+
321
333
  if ts is None:
322
334
  ts = np.array(range(len(self)))
323
- N = len(self)*2
335
+ N = len(self) * 2
324
336
  T = (ts[-1] - ts[0]) / len(self)
325
337
 
326
338
  yf = fft(self.data, axis=0, n=N)
327
- xf = fftfreq(N, T)[:N//2]
339
+ xf = fftfreq(N, T)[: N // 2]
328
340
 
329
-
330
- y=2.0/N * np.abs(yf[0:N//2, :])
341
+ y = 2.0 / N * np.abs(yf[0 : N // 2, :])
342
+
343
+ return pd.DataFrame(
344
+ np.column_stack([xf, y]), columns=["freq"] + self.cols
345
+ ).set_index("freq")
346
+
347
+ def fill_zeros(self):
348
+ """fills zero length rows with the previous or next non-zero value"""
349
+ return self.__class__(
350
+ pd.DataFrame(np.where(
351
+ np.tile(abs(self) == 0, (3, 1)).T,
352
+ np.full(self.data.shape, np.nan),
353
+ self.data,
354
+ )).ffill().bfill().to_numpy()
355
+ )
331
356
 
332
- return pd.DataFrame(np.column_stack([xf, y]), columns=['freq'] + self.cols).set_index('freq')
357
+ def ffill(self):
358
+ return self.__class__(pd.DataFrame(self.data).ffill().to_numpy())
333
359
 
334
-
360
+ def bfill(self):
361
+ return self.__class__(pd.DataFrame(self.data).bfill().to_numpy())
@@ -9,7 +9,7 @@ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
9
9
  You should have received a copy of the GNU General Public License along with
10
10
  this program. If not, see <http://www.gnu.org/licenses/>.
11
11
  """
12
-
12
+ from __future__ import annotations
13
13
  from geometry import Point, Quaternion, PX, PY, PZ, P0
14
14
  from typing import List
15
15
  import numpy as np
@@ -33,7 +33,7 @@ class Coord(Base):
33
33
  self.z_axis=Point(self.data[:,9:12])
34
34
 
35
35
  @staticmethod
36
- def from_axes(o:Point, x:Point, y:Point, z:Point):
36
+ def from_axes(o:Point, x:Point, y:Point, z:Point) -> Coord:
37
37
  assert len(o) == len(x) == len(y) == len(z)
38
38
  return Coord(np.concatenate([
39
39
  o.data,
@@ -43,25 +43,25 @@ class Coord(Base):
43
43
  ],axis=1))
44
44
 
45
45
  @staticmethod
46
- def zero(count=1):
46
+ def zero(count=1) -> Coord:
47
47
  return Coord.from_nothing(count)
48
48
 
49
49
  @staticmethod
50
- def from_nothing(count=1):
50
+ def from_nothing(count=1) -> Coord:
51
51
  return Coord.from_axes(P0(count), PX(1,count), PY(1,count), PZ(1,count))
52
52
 
53
53
  @staticmethod
54
- def from_xy(origin: Point, x_axis: Point, y_axis: Point):
54
+ def from_xy(origin: Point, x_axis: Point, y_axis: Point) -> Coord:
55
55
  z_axis = x_axis.cross(y_axis)
56
56
  return Coord.from_axes(origin, x_axis, z_axis.cross(x_axis), z_axis)
57
57
 
58
58
  @staticmethod
59
- def from_yz(origin: Point, y_axis: Point, z_axis: Point):
59
+ def from_yz(origin: Point, y_axis: Point, z_axis: Point) -> Coord:
60
60
  x_axis = y_axis.cross(z_axis)
61
61
  return Coord.from_axes(origin, x_axis, y_axis, x_axis.cross(y_axis))
62
62
 
63
63
  @staticmethod
64
- def from_zx(origin: Point, z_axis: Point, x_axis: Point):
64
+ def from_zx(origin: Point, z_axis: Point, x_axis: Point) -> Coord:
65
65
  y_axis = z_axis.cross(x_axis)
66
66
  return Coord.from_axes(origin, y_axis.cross(z_axis), y_axis, z_axis)
67
67
 
@@ -71,7 +71,7 @@ class Coord(Base):
71
71
  def inverse_rotation_matrix(self):
72
72
  return Quaternion.from_rotation_matrix(self.rotation_matrix()).inverse().to_rotation_matrix()
73
73
 
74
- def rotate(self, rotation=Quaternion):
74
+ def rotate(self, rotation=Quaternion) -> Coord:
75
75
  return Coord.from_axes(
76
76
  self.origin,
77
77
  rotation.transform_point(self.x_axis),
@@ -82,7 +82,7 @@ class Coord(Base):
82
82
  def __eq__(self, other):
83
83
  return self.data == other.data
84
84
 
85
- def translate(self, point):
85
+ def translate(self, point) -> Coord:
86
86
  return Coord.from_axes(self.origin + point, self.x_axis, self.y_axis, self.z_axis)
87
87
 
88
88
  def axes(self):
@@ -14,6 +14,8 @@ from .base import Base
14
14
  import numpy as np
15
15
  import pandas as pd
16
16
  from warnings import warn
17
+ from numbers import Number
18
+ import numpy.typing as npt
17
19
 
18
20
 
19
21
  class Point(Base):
@@ -62,16 +64,16 @@ class Point(Base):
62
64
  return abs(Point.angles(self, p2))
63
65
 
64
66
  @staticmethod
65
- def X(value=1, count=1):
66
- return Point(np.tile([value,0,0], (count, 1)))
67
+ def X(value: Number | npt.NDArray=1, count=1):
68
+ return np.tile(value, count) * Point(1,0,0)
67
69
 
68
70
  @staticmethod
69
71
  def Y(value=1, count=1):
70
- return Point(np.tile([0,value,0], (count, 1)))
72
+ return np.tile(value, count) * Point(0,1,0)
71
73
 
72
74
  @staticmethod
73
75
  def Z(value=1, count=1):
74
- return Point(np.tile([0,0,value], (count, 1)))
76
+ return np.tile(value, count) * Point(0,0,1)
75
77
 
76
78
  def rotate(self, rmat=np.ndarray):
77
79
  if len(rmat.shape) == 3:
@@ -108,6 +110,9 @@ class Point(Base):
108
110
  def zeros(count=1):
109
111
  return Point(np.zeros((count,3)))
110
112
 
113
+ def bearing(self):
114
+ return np.arctan2(self.y, self.x)
115
+
111
116
  def Points(*args, **kwargs):
112
117
  warn("Points is deprecated, you can now just use Point", DeprecationWarning)
113
118
  return Point(*args, **kwargs)
@@ -136,7 +141,7 @@ def ppmeth(func):
136
141
 
137
142
 
138
143
  @ppmeth
139
- def cross(a, b) -> Point:
144
+ def cross(a: Point, b: Point) -> Point:
140
145
  return Point(np.cross(a.data, b.data))
141
146
 
142
147
 
@@ -253,7 +253,10 @@ class Quaternion(Base):
253
253
  # does the rotation reverse the Z axis?
254
254
  return np.sign(self.transform_point(PZ()).z) > 0
255
255
 
256
-
256
+ def bearing(self, p: Point=None):
257
+ if p is None:
258
+ p = Point.X()
259
+ return self.transform_point(p).bearing()
257
260
 
258
261
 
259
262
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pfc-geometry
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Summary: A package for handling 3D geometry with a nice interface
5
5
  Author-email: Thomas David <thomasdavid0@gmail.com>
6
6
  License: GNU GPL v3
File without changes
File without changes
File without changes
File without changes