spectrochempy-hypercomplex 0.1.1__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.
- spectrochempy_hypercomplex-0.1.1/PKG-INFO +8 -0
- spectrochempy_hypercomplex-0.1.1/pyproject.toml +23 -0
- spectrochempy_hypercomplex-0.1.1/setup.cfg +4 -0
- spectrochempy_hypercomplex-0.1.1/src/spectrochempy_hypercomplex/__init__.py +338 -0
- spectrochempy_hypercomplex-0.1.1/src/spectrochempy_hypercomplex/_quaternion.py +185 -0
- spectrochempy_hypercomplex-0.1.1/src/spectrochempy_hypercomplex.egg-info/PKG-INFO +8 -0
- spectrochempy_hypercomplex-0.1.1/src/spectrochempy_hypercomplex.egg-info/SOURCES.txt +10 -0
- spectrochempy_hypercomplex-0.1.1/src/spectrochempy_hypercomplex.egg-info/dependency_links.txt +1 -0
- spectrochempy_hypercomplex-0.1.1/src/spectrochempy_hypercomplex.egg-info/entry_points.txt +2 -0
- spectrochempy_hypercomplex-0.1.1/src/spectrochempy_hypercomplex.egg-info/requires.txt +3 -0
- spectrochempy_hypercomplex-0.1.1/src/spectrochempy_hypercomplex.egg-info/top_level.txt +1 -0
- spectrochempy_hypercomplex-0.1.1/tests/test_hypercomplex.py +49 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spectrochempy-hypercomplex
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Hypercomplex / quaternion support plugin for SpectroChemPy
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: spectrochempy<0.10,>=0.9.0.dev0
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: numpy-quaternion>=2024.0.7
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spectrochempy-hypercomplex"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Hypercomplex / quaternion support plugin for SpectroChemPy"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"spectrochempy>=0.9.0.dev0,<0.10",
|
|
12
|
+
"numpy",
|
|
13
|
+
"numpy-quaternion>=2024.0.7",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.entry-points."spectrochempy.plugins"]
|
|
17
|
+
hypercomplex = "spectrochempy_hypercomplex:HyperComplexPlugin"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["src"]
|
|
21
|
+
|
|
22
|
+
[tool.pytest.ini_options]
|
|
23
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# ruff: noqa: PLC0415 — defer imports in plugin methods to avoid startup cost
|
|
2
|
+
"""Hypercomplex / quaternion support plugin for SpectroChemPy."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from spectrochempy.api.plugins import CORE_PLUGIN_API_VERSION
|
|
11
|
+
from spectrochempy.api.plugins import PluginCapability
|
|
12
|
+
from spectrochempy.api.plugins import SpectroChemPyPlugin
|
|
13
|
+
|
|
14
|
+
# Public quaternion utilities — safe for other plugins to import
|
|
15
|
+
from ._quaternion import _HAS_QUATERNION as is_available # noqa: F401, N811
|
|
16
|
+
from ._quaternion import as_float_array # noqa: F401
|
|
17
|
+
from ._quaternion import as_quat_array # noqa: F401
|
|
18
|
+
from ._quaternion import as_quaternion # noqa: F401
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HyperAccessor:
|
|
22
|
+
"""
|
|
23
|
+
Dataset accessor for hypercomplex / quaternion operations.
|
|
24
|
+
|
|
25
|
+
Accessed via ``dataset.hyper``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, dataset) -> None:
|
|
29
|
+
self._dataset = dataset
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
# Properties
|
|
33
|
+
# ------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_quaternion(self) -> bool:
|
|
37
|
+
"""True if the data array is hypercomplex (quaternion)."""
|
|
38
|
+
from ._quaternion import typequaternion # noqa: PLC0415
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
typequaternion is not None and self._dataset._data.dtype == typequaternion
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def RR(self):
|
|
46
|
+
"""Real-real component of a 2D hypercomplex array."""
|
|
47
|
+
if not self.is_quaternion:
|
|
48
|
+
raise TypeError("Not a hypercomplex array")
|
|
49
|
+
return self.component("RR")
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def RI(self):
|
|
53
|
+
"""Real-imaginary component of a 2D hypercomplex array."""
|
|
54
|
+
if not self.is_quaternion:
|
|
55
|
+
raise TypeError("Not a hypercomplex array")
|
|
56
|
+
return self.component("RI")
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def IR(self):
|
|
60
|
+
"""Imaginary-real component of a 2D hypercomplex array."""
|
|
61
|
+
if not self.is_quaternion:
|
|
62
|
+
raise TypeError("Not a hypercomplex array")
|
|
63
|
+
return self.component("IR")
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def II(self):
|
|
67
|
+
"""Imaginary-imaginary component of a 2D hypercomplex array."""
|
|
68
|
+
if not self.is_quaternion:
|
|
69
|
+
raise TypeError("Not a hypercomplex array")
|
|
70
|
+
return self.component("II")
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
# Methods
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def set_quaternion(self, inplace=False):
|
|
77
|
+
"""Convert the dataset data to quaternion (hypercomplex) type."""
|
|
78
|
+
from ._quaternion import typequaternion # noqa: PLC0415
|
|
79
|
+
|
|
80
|
+
if typequaternion is None:
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"numpy-quaternion is required for hypercomplex support. "
|
|
83
|
+
"Install it with: pip install numpy-quaternion"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
new = self._dataset if inplace else self._dataset.copy()
|
|
87
|
+
dtype = new._data.dtype if hasattr(new._data, "dtype") else new.dtype
|
|
88
|
+
if dtype != typequaternion:
|
|
89
|
+
new._data = _make_quaternion(new.data)
|
|
90
|
+
if not inplace:
|
|
91
|
+
return new
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
set_hypercomplex = set_quaternion
|
|
95
|
+
set_hypercomplex.__doc__ = "Alias of set_quaternion."
|
|
96
|
+
|
|
97
|
+
def component(self, select="REAL"):
|
|
98
|
+
"""
|
|
99
|
+
Extract a named component from a hypercomplex array.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
select : str
|
|
104
|
+
Component selector: ``"RR"``, ``"RI"``, ``"IR"``, ``"II"``,
|
|
105
|
+
or ``"REAL"`` / ``"R"`` / ``"I"`` for 1D complex data.
|
|
106
|
+
"""
|
|
107
|
+
from ._quaternion import get_component # noqa: PLC0415
|
|
108
|
+
|
|
109
|
+
return get_component(self._dataset.data, select)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _make_quaternion(data):
|
|
113
|
+
"""Convert real/interleaved data to quaternion dtype."""
|
|
114
|
+
from ._quaternion import typequaternion # noqa: PLC0415
|
|
115
|
+
|
|
116
|
+
if data.ndim == 0:
|
|
117
|
+
return data
|
|
118
|
+
if typequaternion is not None and data.dtype == typequaternion:
|
|
119
|
+
return data
|
|
120
|
+
if data.ndim == 1:
|
|
121
|
+
r, i = data[::2], data[1::2]
|
|
122
|
+
return as_quaternion(r, i)
|
|
123
|
+
if data.ndim == 2:
|
|
124
|
+
return as_quaternion(data[:, ::2], data[:, 1::2])
|
|
125
|
+
msg = (
|
|
126
|
+
"An array of data to be transformed to quaternion must be 2D or 1D, "
|
|
127
|
+
f"got {data.ndim}D."
|
|
128
|
+
)
|
|
129
|
+
raise ValueError(msg)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
# NDMath handler implementations
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Ufunc names that numpy-quaternion handles natively (no decomposition needed).
|
|
138
|
+
# Everything else must be decomposed into complex arrays, executed,
|
|
139
|
+
# and recomposed.
|
|
140
|
+
_NATIVE_QUATERNION_UFUNCS = frozenset(
|
|
141
|
+
[
|
|
142
|
+
"add",
|
|
143
|
+
"iadd",
|
|
144
|
+
"sub",
|
|
145
|
+
"isub",
|
|
146
|
+
"subtract",
|
|
147
|
+
"mul",
|
|
148
|
+
"imul",
|
|
149
|
+
"multiply",
|
|
150
|
+
"div",
|
|
151
|
+
"idiv",
|
|
152
|
+
"divide",
|
|
153
|
+
"true_divide",
|
|
154
|
+
"truediv",
|
|
155
|
+
"power",
|
|
156
|
+
"negative",
|
|
157
|
+
"positive",
|
|
158
|
+
"sign",
|
|
159
|
+
"equal",
|
|
160
|
+
"not_equal",
|
|
161
|
+
"less",
|
|
162
|
+
"less_equal",
|
|
163
|
+
"greater",
|
|
164
|
+
"greater_equal",
|
|
165
|
+
"isnan",
|
|
166
|
+
"isinf",
|
|
167
|
+
"isfinite",
|
|
168
|
+
"copysign",
|
|
169
|
+
"nextafter",
|
|
170
|
+
"spacing",
|
|
171
|
+
"maximum",
|
|
172
|
+
"minimum",
|
|
173
|
+
"fmax",
|
|
174
|
+
"fmin",
|
|
175
|
+
"absolute",
|
|
176
|
+
"fabs",
|
|
177
|
+
"conj",
|
|
178
|
+
"conjugate",
|
|
179
|
+
]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _hyper_ndmath_branch(fname: str, data: np.ndarray, args: list) -> str | None:
|
|
184
|
+
"""Return ``"quaternion"`` when the operands require quaternion execution."""
|
|
185
|
+
from ._quaternion import typequaternion # noqa: PLC0415
|
|
186
|
+
|
|
187
|
+
if typequaternion is None:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
is_quaternion = data.dtype == typequaternion
|
|
191
|
+
for arg in args:
|
|
192
|
+
if hasattr(arg, "dtype") and arg.dtype == typequaternion:
|
|
193
|
+
is_quaternion = True
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
if not is_quaternion:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# numpy-quaternion handles these ufuncs natively — no need to decompose.
|
|
200
|
+
if fname in _NATIVE_QUATERNION_UFUNCS:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
return "quaternion"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _hyper_ndmath_execute(branch: str, f, d, args):
|
|
207
|
+
"""Execute *f* on quaternion data *d* by decomposing into complex arrays."""
|
|
208
|
+
if branch != "quaternion":
|
|
209
|
+
return None
|
|
210
|
+
from ._quaternion import quat_as_complex_array # noqa: PLC0415
|
|
211
|
+
|
|
212
|
+
dr, di = quat_as_complex_array(d)
|
|
213
|
+
datar = f(dr, *args)
|
|
214
|
+
datai = f(di, *args)
|
|
215
|
+
return as_quaternion(datar, datai)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
# Numpy-method overrides for quaternion data
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _is_quaternion_dataset(dataset) -> bool:
|
|
224
|
+
"""Return True if *dataset* carries quaternion data."""
|
|
225
|
+
from ._quaternion import typequaternion # noqa: PLC0415
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
typequaternion is not None
|
|
229
|
+
and hasattr(dataset, "_data")
|
|
230
|
+
and hasattr(dataset._data, "dtype")
|
|
231
|
+
and dataset._data.dtype == typequaternion
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _hyper_numpy_abs(dataset, *args, **kwargs):
|
|
236
|
+
"""Quaternion-aware absolute value."""
|
|
237
|
+
if not _is_quaternion_dataset(dataset):
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
w, x, y, z = as_float_array(dataset.data).T
|
|
241
|
+
data = np.ma.sqrt(w**2 + x**2 + y**2 + z**2)
|
|
242
|
+
dataset._data = data.data
|
|
243
|
+
dataset._mask = getattr(data, "mask", False)
|
|
244
|
+
return dataset
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _hyper_numpy_conjugate(dataset, *args, **kwargs):
|
|
248
|
+
"""Quaternion-aware conjugate."""
|
|
249
|
+
if not _is_quaternion_dataset(dataset):
|
|
250
|
+
return None
|
|
251
|
+
dim = kwargs.get("dim", "x")
|
|
252
|
+
axis, _ = dataset.get_axis(dim, allows_none=True)
|
|
253
|
+
dataset = dataset.swapdims(axis, -1)
|
|
254
|
+
dataset[..., 1::2] = -dataset[..., 1::2]
|
|
255
|
+
return dataset.swapdims(axis, -1)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _hyper_numpy_max(dataset, *args, **kwargs):
|
|
259
|
+
"""Quaternion-aware max — operates on the real (w) component."""
|
|
260
|
+
if not _is_quaternion_dataset(dataset):
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
data = dataset.data
|
|
264
|
+
w = as_float_array(data)[..., 0]
|
|
265
|
+
axis = kwargs.get("axis")
|
|
266
|
+
keepdims = kwargs.get("keepdims", False)
|
|
267
|
+
m = np.ma.max(w, axis=axis, keepdims=keepdims)
|
|
268
|
+
if np.isscalar(m) or (m.size == 1 and not keepdims):
|
|
269
|
+
if not np.isscalar(m):
|
|
270
|
+
m = m[()]
|
|
271
|
+
if dataset.units is not None:
|
|
272
|
+
from spectrochempy.core.units import Quantity # noqa: PLC0415
|
|
273
|
+
|
|
274
|
+
return Quantity(m, dataset.units)
|
|
275
|
+
return m
|
|
276
|
+
dataset._data = m.data
|
|
277
|
+
dataset._mask = getattr(m, "mask", False)
|
|
278
|
+
return dataset
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _hyper_numpy_min(dataset, *args, **kwargs):
|
|
282
|
+
"""Quaternion-aware min — operates on the real (w) component."""
|
|
283
|
+
if not _is_quaternion_dataset(dataset):
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
data = dataset.data
|
|
287
|
+
w = as_float_array(data)[..., 0]
|
|
288
|
+
axis = kwargs.get("axis")
|
|
289
|
+
keepdims = kwargs.get("keepdims", False)
|
|
290
|
+
m = np.ma.min(w, axis=axis, keepdims=keepdims)
|
|
291
|
+
if np.isscalar(m) or (m.size == 1 and not keepdims):
|
|
292
|
+
if not np.isscalar(m):
|
|
293
|
+
m = m[()]
|
|
294
|
+
if dataset.units is not None:
|
|
295
|
+
from spectrochempy.core.units import Quantity # noqa: PLC0415
|
|
296
|
+
|
|
297
|
+
return Quantity(m, dataset.units)
|
|
298
|
+
return m
|
|
299
|
+
dataset._data = m.data
|
|
300
|
+
dataset._mask = getattr(m, "mask", False)
|
|
301
|
+
return dataset
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
# Plugin class
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class HyperComplexPlugin(SpectroChemPyPlugin):
|
|
310
|
+
"""Hypercomplex / quaternion support for SpectroChemPy."""
|
|
311
|
+
|
|
312
|
+
name = "hypercomplex"
|
|
313
|
+
version = "0.1.0"
|
|
314
|
+
description = "Hypercomplex / quaternion data support"
|
|
315
|
+
spectrochempy_min_version = "0.9.0.dev0"
|
|
316
|
+
PLUGIN_API_VERSION = CORE_PLUGIN_API_VERSION
|
|
317
|
+
capabilities = [PluginCapability.ACCESSOR]
|
|
318
|
+
|
|
319
|
+
def register_accessors(self) -> list[dict]:
|
|
320
|
+
"""Register the ``hyper`` dataset accessor."""
|
|
321
|
+
return [
|
|
322
|
+
{
|
|
323
|
+
"name": "hyper",
|
|
324
|
+
"func": HyperAccessor,
|
|
325
|
+
"description": "Hypercomplex / quaternion dataset operations",
|
|
326
|
+
},
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
def register_handlers(self) -> dict[str, Callable]:
|
|
330
|
+
"""Register NDMath execution handlers for quaternion dispatch."""
|
|
331
|
+
return {
|
|
332
|
+
"ndmath.execution_branch": _hyper_ndmath_branch,
|
|
333
|
+
"ndmath.execute": _hyper_ndmath_execute,
|
|
334
|
+
"ndmath.numpy_method.absolute": _hyper_numpy_abs,
|
|
335
|
+
"ndmath.numpy_method.conjugate": _hyper_numpy_conjugate,
|
|
336
|
+
"ndmath.numpy_method.amax": _hyper_numpy_max,
|
|
337
|
+
"ndmath.numpy_method.amin": _hyper_numpy_min,
|
|
338
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# ======================================================================================
|
|
2
|
+
# Copyright (©) 2014-2026 Laboratoire Catalyse et Spectrochimie (LCS), Caen, France.
|
|
3
|
+
# CeCILL-B FREE SOFTWARE LICENSE AGREEMENT
|
|
4
|
+
# See full LICENSE agreement in the root directory.
|
|
5
|
+
# ======================================================================================
|
|
6
|
+
"""
|
|
7
|
+
Optional quaternion support — internal to the hypercomplex plugin.
|
|
8
|
+
|
|
9
|
+
The public API is exposed through :class:`HyperAccessor`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from spectrochempy.utils.constants import TYPE_COMPLEX
|
|
17
|
+
|
|
18
|
+
# Try to import the quaternion library (optional dependency)
|
|
19
|
+
_HAS_QUATERNION = False
|
|
20
|
+
typequaternion = None
|
|
21
|
+
as_float_array = None
|
|
22
|
+
as_quat_array = None
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from quaternion import as_float_array # noqa: F811
|
|
26
|
+
from quaternion import as_quat_array # noqa: F811
|
|
27
|
+
|
|
28
|
+
_HAS_QUATERNION = True
|
|
29
|
+
typequaternion = np.dtype(np.quaternion)
|
|
30
|
+
except ImportError:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _check():
|
|
35
|
+
if not _HAS_QUATERNION:
|
|
36
|
+
raise ImportError(
|
|
37
|
+
"Missing optional dependency 'numpy-quaternion'. "
|
|
38
|
+
"Use pip or conda to install numpy-quaternion."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ======================================================================================
|
|
43
|
+
# Public methods
|
|
44
|
+
# ======================================================================================
|
|
45
|
+
def as_quaternion(*args):
|
|
46
|
+
"""
|
|
47
|
+
Recombine the arguments to produce a numpy array with quaternion dtype.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
*args : `ndarray` with dtype: `float` or `complex`
|
|
52
|
+
The quaternion array components: If there is 4 components, then we assume it is
|
|
53
|
+
the four components of the
|
|
54
|
+
quaternion array: w, x, y, z. If there is only two, they are casted to complex
|
|
55
|
+
and correspond respectively
|
|
56
|
+
to w + i.x and y + j.z.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
_check()
|
|
60
|
+
if len(args) == 4:
|
|
61
|
+
w, x, y, z = args
|
|
62
|
+
elif len(args) == 2:
|
|
63
|
+
r, i = args
|
|
64
|
+
w, x, y, z = r.real, r.imag, i.real, i.imag
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError("as_quaternion requires 2 or 4 arguments")
|
|
67
|
+
|
|
68
|
+
data = as_quat_array(
|
|
69
|
+
list(zip(w.flatten(), x.flatten(), y.flatten(), z.flatten(), strict=False)),
|
|
70
|
+
)
|
|
71
|
+
return data.reshape(w.shape)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def quat_as_complex_array(arr):
|
|
75
|
+
"""
|
|
76
|
+
Recombine the component of a quaternion array into a tuple of two complex array.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
arr : quaternion ndarray
|
|
81
|
+
The arr will be separated into (w + i.x) and (y + i.z).
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
tuple
|
|
86
|
+
Tuple of two complex array.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
_check()
|
|
90
|
+
if typequaternion is None or arr.dtype != typequaternion:
|
|
91
|
+
return arr
|
|
92
|
+
|
|
93
|
+
wt, xt, yt, zt = as_float_array(arr).T
|
|
94
|
+
w, x, y, z = wt.T, xt.T, yt.T, zt.T
|
|
95
|
+
|
|
96
|
+
return (w + 1j * x), (y + 1j * z)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_component(data, select="REAL"):
|
|
100
|
+
"""
|
|
101
|
+
Take selected components of an hypercomplex array (RRR, RIR, ...).
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
data : `ndarray`
|
|
106
|
+
Input data array.
|
|
107
|
+
select : `str`, optional, default: 'REAL'
|
|
108
|
+
If 'REAL', only real component in all dimensions will be selected.
|
|
109
|
+
Else a string must specify which real (R) or imaginary (I) component
|
|
110
|
+
has to be selected along a specific dimension. For instance,
|
|
111
|
+
a string such as 'RRI' for a 2D hypercomplex array indicated
|
|
112
|
+
that we take the real component in each dimension except the last
|
|
113
|
+
one, for which imaginary component is preferred.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
component
|
|
118
|
+
A component of the complex or hypercomplex array.
|
|
119
|
+
|
|
120
|
+
.. warning::
|
|
121
|
+
The definition is somewhat different from Bruker, as we order the component in
|
|
122
|
+
the order of the dimensions in
|
|
123
|
+
dataset:
|
|
124
|
+
e.g., for dims = ['y','x'], 'IR' means that the `y` component is imaginary
|
|
125
|
+
while the `x` is real.
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
if not select:
|
|
129
|
+
return data
|
|
130
|
+
|
|
131
|
+
new = data.copy()
|
|
132
|
+
|
|
133
|
+
if select == "REAL":
|
|
134
|
+
select = "R" * new.ndim
|
|
135
|
+
|
|
136
|
+
w = x = y = z = None
|
|
137
|
+
|
|
138
|
+
if typequaternion is not None and new.dtype == typequaternion:
|
|
139
|
+
w, x, y, z = as_float_array(new).T
|
|
140
|
+
w, x, y, z = w.T, x.T, y.T, z.T
|
|
141
|
+
if select == "R":
|
|
142
|
+
new = w + x * 1j
|
|
143
|
+
elif select == "I":
|
|
144
|
+
new = y + z * 1j
|
|
145
|
+
elif select == "RR":
|
|
146
|
+
new = w
|
|
147
|
+
elif select == "RI":
|
|
148
|
+
new = x
|
|
149
|
+
elif select == "IR":
|
|
150
|
+
new = y
|
|
151
|
+
elif select == "II":
|
|
152
|
+
new = z
|
|
153
|
+
else:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"something wrong: cannot interpret `{select}` for hypercomplex (quaternion) data!",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
elif new.dtype in TYPE_COMPLEX:
|
|
159
|
+
w, x = new.real, new.imag
|
|
160
|
+
if (select == "R") or (select == "RR"):
|
|
161
|
+
new = w
|
|
162
|
+
elif (select == "I") or (select == "RI"):
|
|
163
|
+
new = x
|
|
164
|
+
else:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"something wrong: cannot interpret `{select}` for complex data!",
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
warnings.warn(
|
|
170
|
+
f"No selection was performed because datasets with complex data have no "
|
|
171
|
+
f"`{select}` component. ",
|
|
172
|
+
stacklevel=2,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return new
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def interleaved2complex(data):
|
|
179
|
+
"""Make a complex array from interleaved data."""
|
|
180
|
+
return data[..., ::2] + 1j * data[..., 1::2]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def interleaved2quaternion(data):
|
|
184
|
+
"""Make a complex array from interleaved data."""
|
|
185
|
+
return data[..., ::2] + 1j * data[..., 1::2]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spectrochempy-hypercomplex
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Hypercomplex / quaternion support plugin for SpectroChemPy
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: spectrochempy<0.10,>=0.9.0.dev0
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: numpy-quaternion>=2024.0.7
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/spectrochempy_hypercomplex/__init__.py
|
|
3
|
+
src/spectrochempy_hypercomplex/_quaternion.py
|
|
4
|
+
src/spectrochempy_hypercomplex.egg-info/PKG-INFO
|
|
5
|
+
src/spectrochempy_hypercomplex.egg-info/SOURCES.txt
|
|
6
|
+
src/spectrochempy_hypercomplex.egg-info/dependency_links.txt
|
|
7
|
+
src/spectrochempy_hypercomplex.egg-info/entry_points.txt
|
|
8
|
+
src/spectrochempy_hypercomplex.egg-info/requires.txt
|
|
9
|
+
src/spectrochempy_hypercomplex.egg-info/top_level.txt
|
|
10
|
+
tests/test_hypercomplex.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spectrochempy_hypercomplex
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ruff: noqa: S101
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestHyperAccessor:
|
|
6
|
+
def test_accessor_registered(self):
|
|
7
|
+
import spectrochempy as scp
|
|
8
|
+
|
|
9
|
+
ds = scp.NDDataset([1.0, 2.0, 3.0])
|
|
10
|
+
assert hasattr(ds, "hyper")
|
|
11
|
+
|
|
12
|
+
def test_is_quaternion_false_for_real(self):
|
|
13
|
+
import spectrochempy as scp
|
|
14
|
+
|
|
15
|
+
ds = scp.NDDataset([1.0, 2.0, 3.0])
|
|
16
|
+
assert ds.hyper.is_quaternion is False
|
|
17
|
+
|
|
18
|
+
def test_set_quaternion_creates_quaternion_dtype(self):
|
|
19
|
+
import spectrochempy as scp
|
|
20
|
+
|
|
21
|
+
ds = scp.NDDataset([1.0, 2.0, 3.0, 4.0])
|
|
22
|
+
result = ds.hyper.set_quaternion()
|
|
23
|
+
# After conversion, the data should have a quaternion dtype
|
|
24
|
+
assert result._data.dtype.name == "quaternion"
|
|
25
|
+
|
|
26
|
+
def test_component_real_data_noop(self):
|
|
27
|
+
import spectrochempy as scp
|
|
28
|
+
|
|
29
|
+
ds = scp.NDDataset([1.0, 2.0, 3.0])
|
|
30
|
+
# For real data, get_component returns the array unchanged
|
|
31
|
+
result = ds.hyper.component("RR")
|
|
32
|
+
assert result is not None
|
|
33
|
+
|
|
34
|
+
def test_RR_RI_IR_II_raise_for_real(self):
|
|
35
|
+
import spectrochempy as scp
|
|
36
|
+
|
|
37
|
+
ds = scp.NDDataset([1.0, 2.0, 3.0])
|
|
38
|
+
for prop in ["RR", "RI", "IR", "II"]:
|
|
39
|
+
with pytest.raises(TypeError):
|
|
40
|
+
getattr(ds.hyper, prop)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestNDMathHandlers:
|
|
44
|
+
def test_handler_registration(self):
|
|
45
|
+
from spectrochempy.plugins import manager
|
|
46
|
+
|
|
47
|
+
registry = manager.plugin_manager.registry
|
|
48
|
+
assert registry.get_handler("ndmath.execution_branch") is not None
|
|
49
|
+
assert registry.get_handler("ndmath.execute") is not None
|