physpyx 2.2.0__tar.gz → 2.3.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: physpyx
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Provides a way to use python code in LaTeX
5
5
  Author: Jérôme Dufour
6
6
  Author-email: Jérôme Dufour <jerome.dufour@eduvaud.ch>
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "physpyx"
3
+ version = "2.3.0"
4
+ description = "Provides a way to use python code in LaTeX"
5
+ classifiers = [
6
+ "Programming Language :: Python :: 3",
7
+ "Operating System :: Unix",
8
+ "Topic :: Education",
9
+ "Topic :: Scientific/Engineering :: Physics",
10
+ ]
11
+ license = "GPL-3.0-or-later"
12
+ readme = "README.md"
13
+ requires-python = ">=3.13"
14
+ dependencies = [
15
+ "numpy>=2.3.4",
16
+ "pint>=0.25.1",
17
+ "scipy>=1.17.0",
18
+ ]
19
+
20
+ [[project.authors]]
21
+ name = "Jérôme Dufour"
22
+ email = "jerome.dufour@eduvaud.ch"
23
+
24
+ [project.urls]
25
+ Repository = "https://bitbucket.org/jdufour/physpyx/src/master/"
26
+ Changelog = "https://bitbucket.org/jdufour/physpyx/src/master/CHANGELOG.md"
27
+ Documentation = "https://physpyx.readthedocs.io/fr/latest/"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "mypy>=1.18.1",
32
+ "ipdb>=0.13.13",
33
+ "scipy-stubs>=1.17.1.5",
34
+ "ruff>=0.16.0",
35
+ ]
36
+ docs = [
37
+ "sphinx>=7.1.2",
38
+ "sphinx-rtd-theme>=3.0.2",
39
+ "sphinx-argparse>=0.5.2",
40
+ "myst-parser>=4.0.1",
41
+ "sphinx-autodoc2>=0.5.0",
42
+ "sphinx-autobuild>=2025.8.25",
43
+ "sphinx-design>=0.7.0",
44
+ ]
45
+
46
+ [build-system]
47
+ requires = ["uv_build>=0.11.0,<0.12.0"]
48
+ build-backend = "uv_build"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "physpyx"
3
- version = "2.2.0"
3
+ version = "2.3.0"
4
4
  authors = [
5
5
  { name="Jérôme Dufour", email="jerome.dufour@eduvaud.ch" },
6
6
  ]
@@ -25,6 +25,7 @@ dev = [
25
25
  "mypy>=1.18.1",
26
26
  "ipdb>=0.13.13",
27
27
  "scipy-stubs>=1.17.1.5",
28
+ "ruff>=0.16.0",
28
29
  ]
29
30
  docs = [
30
31
  "sphinx>=7.1.2",
@@ -1,6 +1,7 @@
1
1
  from .astronomy import Orbit
2
+ from .context import NuclearContext
2
3
  from .nuclide import NuclideTable
3
4
  from .physpyx import PhysPyX
4
5
  from .qty import Qty
5
6
 
6
- __all__ = ["NuclideTable", "PhysPyX", "Qty", "Orbit"]
7
+ __all__ = ["NuclearContext", "NuclideTable", "Orbit", "PhysPyX", "Qty"]
@@ -1,8 +1,9 @@
1
+ from collections.abc import Iterator
2
+
1
3
  import numpy as np
2
4
  from numpy.typing import NDArray
3
- from scipy.special import ellipk, ellipkinc
4
5
  from scipy.optimize import newton
5
- from typing import Iterator
6
+ from scipy.special import ellipk, ellipkinc
6
7
 
7
8
 
8
9
  class Orbit:
@@ -77,7 +78,7 @@ class Orbit:
77
78
  raise ValueError(f"{every} is not a divisor {steps} steps")
78
79
 
79
80
  dt = self.T / steps
80
- for i in range(0, steps + 1):
81
+ for i in range(steps + 1):
81
82
  if i % every == 0:
82
83
  yield self.x
83
84
  self.step(dt)
@@ -0,0 +1,75 @@
1
+ from json import loads
2
+ from pathlib import Path
3
+ from typing import ClassVar
4
+
5
+ from physpyx.nuclide import NuclideTable
6
+
7
+
8
+ class Constants:
9
+ _CONSTANTS: ClassVar[dict[str, tuple[float | int, str, str]]] = {
10
+ "cesium_standard": (9192631770, "hertz", "d"), # SI defining constant
11
+ "c": (299792458, "meter per second", "d"), # SI defining constant
12
+ "h": (6.62607015e-34, "joule second", ".8e"), # SI defining constant
13
+ "qe": (1.602176634e-19, "coulomb", ".3e"), # SI defining constant
14
+ "kB": (1.380649e-23, "joule per kelvin", "e"), # SI defining constant
15
+ "Na": (6.02214076e23, "", ".8e"), # SI defining constant
16
+ "R": (8.31446261815324, "joule per kelvin per mole", "f"),
17
+ "k": (8.987551e9, "newton meter squared per coulomb squared", ".0e"),
18
+ "epsilon0": (
19
+ 8.8541878188e-12,
20
+ "coulomb squared per newton meter squared",
21
+ ".3e",
22
+ ),
23
+ "mu0": (1.2566370612696005e-06, "tesla meter per ampere", ".3e"),
24
+ "mn": (1.67492750056e-27, "kilogram", ".3e"),
25
+ "mp": (1.67262192595e-27, "kilogram", ".3e"),
26
+ "me": (9.1093837139e-31, "kilogram", ".3e"),
27
+ "a0": (5.29177210903e-11, "meter", "e"),
28
+ "G": (6.6743e-11, "newton meter squared per kilogram squared", ".2e"),
29
+ "g": (9.81, "meter per second squared", ".2f"),
30
+ }
31
+
32
+ def __init__(self, fname: Path | None):
33
+ self._fname = fname
34
+ self._nuclide_table = NuclideTable()
35
+ self._constants: dict[str, tuple[float, str, str]] = {}
36
+
37
+ def _get_default_constant(
38
+ self, name: str, fmt: str
39
+ ) -> tuple[float, str, str] | None:
40
+ if data := self._CONSTANTS.get(name, None):
41
+ value, unit, default_fmt = data
42
+ return value, unit, default_fmt if fmt == "g" else fmt
43
+
44
+ def _get_custom_constant(
45
+ self, name: str, fmt: str
46
+ ) -> tuple[float, str, str] | None:
47
+ if not self._constants and self._fname and self._fname.exists():
48
+ self._constants = loads(self._fname.read_bytes())
49
+
50
+ if data := self._constants.get(name, None):
51
+ value, unit, default_fmt = data
52
+ return value, unit, default_fmt if fmt == "g" else fmt
53
+
54
+ def _get_nuclide_data(self, name: str, fmt: str) -> tuple[float, str, str] | None:
55
+ try:
56
+ match name[0]:
57
+ case "m":
58
+ n = self._nuclide_table.get(symbol=name[1:])
59
+ return n.atomic_mass, "amu", "f" if fmt == "g" else fmt
60
+ case "t":
61
+ n = self._nuclide_table.get(symbol=name[1:])
62
+ if n.radioactivity.stable:
63
+ raise ValueError(f"No half-life for {n.symbol}")
64
+ return n.radioactivity.half_life, "second", "g"
65
+ except KeyError:
66
+ pass
67
+
68
+ def __call__(self, name: str, fmt: str) -> tuple[float, str, str]:
69
+ if data := (
70
+ self._get_default_constant(name, fmt)
71
+ or self._get_custom_constant(name, fmt)
72
+ or self._get_nuclide_data(name, fmt)
73
+ ):
74
+ return data
75
+ raise ValueError(f"Constant '{name}' not found")
@@ -7,7 +7,7 @@ class SiunitxContext(Context):
7
7
 
8
8
 
9
9
  class NuclearContext(SiunitxContext):
10
- def __init__(self, mass_is_amu: bool = True):
10
+ def __init__(self, preferred_units: bool = True):
11
11
  super().__init__("nuclear")
12
12
  self.add_transformation(
13
13
  "[mass]",
@@ -20,5 +20,16 @@ class NuclearContext(SiunitxContext):
20
20
  lambda ureg, x: x / ureg.speed_of_light / ureg.speed_of_light,
21
21
  )
22
22
  self.preferred_units = (
23
- {UnitsContainer({"[mass]": 1}): "amu"} if mass_is_amu else {}
23
+ {
24
+ UnitsContainer({"[mass]": 1}): "amu",
25
+ UnitsContainer(
26
+ {
27
+ "[mass]": 1,
28
+ "[length]": 2,
29
+ "[time]": -2,
30
+ }
31
+ ): "megaelectronvolt",
32
+ }
33
+ if preferred_units
34
+ else {}
24
35
  )
@@ -29,7 +29,7 @@ class PTManager(ABC):
29
29
  except Exception as e:
30
30
  if tb_type := e.__traceback__:
31
31
  self._write_errorfile(e, tb_type)
32
- raise e
32
+ raise
33
33
 
34
34
  @abstractmethod
35
35
  def _write_successfile(self) -> None: ...
@@ -1,10 +1,11 @@
1
+ from collections.abc import Generator, Iterable, Iterator
1
2
  from enum import Enum
2
3
  from logging import getLogger
3
4
  from math import sqrt
4
5
  from pathlib import Path
5
6
  from pickle import dump, loads
6
7
  from tempfile import gettempdir
7
- from typing import Generator, Iterable, Iterator, TypedDict
8
+ from typing import ClassVar, TypedDict
8
9
 
9
10
  logger = getLogger(__name__)
10
11
 
@@ -59,7 +60,7 @@ class PossibleDecays(Enum):
59
60
 
60
61
 
61
62
  class Channel:
62
- _DECAY_MAP = {d: md for md in PossibleDecays for d in md.channels.keys()}
63
+ _DECAY_MAP: ClassVar = {d: md for md in PossibleDecays for d in md.channels}
63
64
 
64
65
  def __init__(self, channel: str, prob: float):
65
66
  self.decay = self._DECAY_MAP[channel]
@@ -105,16 +106,15 @@ class DecayChannels:
105
106
  self.channels.append(B)
106
107
  look_for_C = True
107
108
 
108
- if look_for_C:
109
- if C := self._parse_decays(data[24], data[25]):
110
- if C.prob < 1:
111
- logger.warning(f"{symbol} skip 3rd (<1% | none)")
112
- elif A.prob + B.prob + C.prob > 100:
113
- logger.warning(
114
- f"{symbol} skip 3rd ({A.prob + B.prob + C.prob:.0f}%>100%)"
115
- )
116
- else:
117
- self.channels.append(C)
109
+ if look_for_C and (C := self._parse_decays(data[24], data[25])):
110
+ if C.prob < 1:
111
+ logger.warning(f"{symbol} skip 3rd (<1% | none)")
112
+ elif A.prob + B.prob + C.prob > 100:
113
+ logger.warning(
114
+ f"{symbol} skip 3rd ({A.prob + B.prob + C.prob:.0f}%>100%)"
115
+ )
116
+ else:
117
+ self.channels.append(C)
118
118
 
119
119
  if len(self.channels) == 1 and self.channels[0].prob == 0:
120
120
  self.channels[0].prob = 100
@@ -214,8 +214,7 @@ class Nuclide:
214
214
  with_mass: bool = False,
215
215
  with_decay: bool = False,
216
216
  ) -> Generator[str]:
217
- for tex in self.radioactivity.to_tex(self.N, self.Z):
218
- yield tex
217
+ yield from self.radioactivity.to_tex(self.N, self.Z)
219
218
 
220
219
  if with_symbol:
221
220
  yield rf"\node[anchor=north] at ({self.N + 0.5}, {self.Z + 1}) {{\ce{{^{self.A}{self.symbol}}}}};"
@@ -231,14 +230,13 @@ class Nuclide:
231
230
 
232
231
  class NuclideTable:
233
232
  def __init__(self):
234
- self._by_numbers = dict()
235
- self._by_symbols = dict()
233
+ self._by_numbers = {}
234
+ self._by_symbols = {}
236
235
 
237
236
  def __iter__(self) -> Iterator[Nuclide]:
238
237
  if not self._by_numbers:
239
238
  self._load()
240
- for n in self._by_numbers.values():
241
- yield n
239
+ yield from self._by_numbers.values()
242
240
 
243
241
  def _load(self) -> None:
244
242
  pk_file = Path(gettempdir()) / "physpyx-nuclides.pkl"
@@ -354,14 +352,10 @@ class NuclideTable:
354
352
  Nmin, Nmax = 1000, 0
355
353
  Zmin, Zmax = 1000, 0
356
354
  for n in nuclides:
357
- if n.N < Nmin:
358
- Nmin = n.N
359
- if n.Z < Zmin:
360
- Zmin = n.Z
361
- if n.N > Nmax:
362
- Nmax = n.N
363
- if n.Z > Zmax:
364
- Zmax = n.Z
355
+ Nmin = min(Nmin, n.N)
356
+ Zmin = min(Zmin, n.Z)
357
+ Nmax = max(Nmax, n.N)
358
+ Zmax = max(Zmax, n.Z)
365
359
 
366
360
  yield rf"\draw[<->] ({Nmin - 0.1}, {Zmax + 1}) node[left]{{$Z$}} |- ({Nmax + 1}, {Zmin - 0.1}) node[below]{{$N$}};"
367
361
  for i in range(Nmin, Nmax + 1):
@@ -1,4 +1,4 @@
1
- from typing import Generator
1
+ from collections.abc import Generator
2
2
  from contextlib import contextmanager
3
3
 
4
4
  from physpyx.manager import PTManager
@@ -20,7 +20,7 @@ class PhysPyX(PTManager):
20
20
  indent = "\t" * self._stack
21
21
  self._out += indent + f"\n{indent}".join(data) + "\n"
22
22
  else:
23
- raise ValueError
23
+ raise TypeError
24
24
 
25
25
  @contextmanager
26
26
  def begin(
@@ -1,16 +1,14 @@
1
1
  import re
2
2
  import sys
3
-
4
3
  from logging import getLogger
5
- from typing import TYPE_CHECKING
6
4
 
5
+ from pint._typing import Scalar
7
6
  from pint.facets.context import Context
8
7
 
8
+ from physpyx.constants import Constants
9
9
  from physpyx.manager import PTManager
10
- from physpyx.quantity import AltDict, Constants, SiunitxQuantity, SiunitxUnitRegistry
11
-
12
- if TYPE_CHECKING:
13
- from pint._typing import Scalar
10
+ from physpyx.quantity import AltDict, SiunitxQuantity
11
+ from physpyx.registry import SiunitxUnitRegistry
14
12
 
15
13
  logger = getLogger(__name__)
16
14
 
@@ -42,9 +40,9 @@ class Qty(PTManager):
42
40
  outfile.write(
43
41
  "\\ExplSyntaxOn\n\\prop_gset_from_keyval:Nn \\g_physpyx_pty_prop {\n"
44
42
  )
45
- for name, qty in sys._getframe(2).f_locals.items():
43
+ for variable, qty in sys._getframe(2).f_locals.items():
46
44
  if isinstance(qty, SiunitxQuantity):
47
- for name, new in qty.export(name):
45
+ for name, new in qty.export(variable):
48
46
  outfile.write(f"\t{{{name}}} = {{{new}}},\n")
49
47
  if old := current.pop(name, None):
50
48
  if old != new:
@@ -1,16 +1,10 @@
1
- from json import loads
1
+ from collections.abc import Generator, Iterator
2
2
  from logging import getLogger
3
- from numpy import cross, ndarray, linalg
4
- from pathlib import Path
5
- from pint.registry import GenericUnitRegistry, Quantity, Unit
6
- from pint.facets.context import Context
7
- from typing import TYPE_CHECKING, Any, Generator, TypedDict, cast
3
+ from typing import ClassVar, Self, TypedDict, cast
8
4
 
9
- from physpyx.context import SiunitxContext
10
- from physpyx.nuclide import NuclideTable
11
-
12
- if TYPE_CHECKING:
13
- from pint._typing import Magnitude, UnitLike
5
+ from numpy import cross, linalg, ndarray
6
+ from pint._typing import Magnitude, UnitLike
7
+ from pint.registry import Quantity
14
8
 
15
9
  logger = getLogger(__name__)
16
10
 
@@ -31,7 +25,7 @@ class SiunitxQuantity(Quantity):
31
25
  _preferred: bool
32
26
  _alt: AltDict
33
27
 
34
- _sinunitx_map = {
28
+ _sinunitx_map: ClassVar[dict[str, str]] = {
35
29
  "astronomical_unit": "astronomicalunit",
36
30
  "degree_Celsius": "celsius",
37
31
  "delta_degree_Celsius": "celsius",
@@ -54,7 +48,7 @@ class SiunitxQuantity(Quantity):
54
48
  compact: bool = False,
55
49
  preferred: bool = True,
56
50
  alt: AltDict | bool = False,
57
- ) -> "SiunitxQuantity":
51
+ ) -> Self:
58
52
  """
59
53
  Return an instance of SiunitxQuantity
60
54
 
@@ -71,7 +65,7 @@ class SiunitxQuantity(Quantity):
71
65
  :param alt: If provided, writes an additional alternative output with
72
66
  different formatting. The string '_alt' is added to the base name.
73
67
  """
74
- quantity = cast(SiunitxQuantity, super().__new__(cls, value, units))
68
+ quantity = cast(Self, super().__new__(cls, value, units))
75
69
  quantity._fmt = fmt
76
70
  quantity._base = base
77
71
  quantity._compact = compact
@@ -86,6 +80,17 @@ class SiunitxQuantity(Quantity):
86
80
  else:
87
81
  return alt
88
82
 
83
+ def __iter__(self) -> Iterator["SiunitxQuantity"]:
84
+ # NOTE: this is used to set formatting options in tuple expansions:
85
+ # rx, ry = qty([3, 2], "kilometer", fmt=".1e")
86
+ for val in super().__iter__():
87
+ val._fmt = self._fmt
88
+ val._base = self._base
89
+ val._compact = self._compact
90
+ val._preferred = self._preferred
91
+ val._alt = self._alt
92
+ yield val
93
+
89
94
  def __str__(self) -> str:
90
95
  quantity = self
91
96
  if self._base:
@@ -146,6 +151,11 @@ class SiunitxQuantity(Quantity):
146
151
  # "PlainQuantity[Unknown]"'
147
152
  return cast(SiunitxQuantity, super().__neg__())
148
153
 
154
+ def __add__(self, other: object) -> "SiunitxQuantity":
155
+ # NOTE: to fix typing warning 'Cannot access attribute "???" for class
156
+ # "PlainQuantity[Unknown]"'
157
+ return cast(SiunitxQuantity, super().__add__(other))
158
+
149
159
  def _format_unit(self) -> str:
150
160
  num = ""
151
161
  den = ""
@@ -176,7 +186,7 @@ class SiunitxQuantity(Quantity):
176
186
  den += r"\per" + tmp
177
187
  return num + den
178
188
 
179
- def export(self, name: str) -> Generator[tuple[str, str], None, None]:
189
+ def export(self, name: str) -> Generator[tuple[str, str]]:
180
190
  try:
181
191
  yield (
182
192
  name,
@@ -189,7 +199,7 @@ class SiunitxQuantity(Quantity):
189
199
  # the one raised in PTManager.__exit__
190
200
  logger.error(f"exporting {name} failed, {self._fmt}")
191
201
  e.add_note(f"exporting {name} failed")
192
- raise e
202
+ raise
193
203
 
194
204
  if self._alt:
195
205
  alt = self.fmtex(**self._alt)
@@ -257,130 +267,3 @@ class SiunitxQuantity(Quantity):
257
267
  if not isinstance(other, SiunitxQuantity):
258
268
  raise TypeError(f"Can't perform a cross product with a {type(other)}")
259
269
  return cast(SiunitxQuantity, cross(self, other))
260
-
261
-
262
- class SiunitxUnitRegistry(GenericUnitRegistry):
263
- Quantity: type = SiunitxQuantity
264
- Unit: type = Unit
265
-
266
- def __init__(self):
267
- super().__init__()
268
- self.define("heure = hour")
269
- self.define("heures = hours")
270
- self.define("jour = day")
271
- self.define("jours = days")
272
- self.define("semaine = week")
273
- self.define("semaines = weeks")
274
- self.define("mois = month")
275
- self.define("an = year")
276
- self.define("ans = years")
277
- self.define("celerity = speed_of_light")
278
- self.define("chf = [currency] = franc")
279
- self.define("ct = 0.01 * chf = centime")
280
- self._preferred_units = {
281
- Unit(name).dimensionality: name
282
- for name in [
283
- "meter",
284
- "second",
285
- "kilogram",
286
- "ampere",
287
- "kelvin",
288
- "mole",
289
- "meter per second",
290
- "meter per second squared",
291
- "newton",
292
- "joule",
293
- "watt",
294
- "pascal",
295
- "volt",
296
- "ohm",
297
- "tesla",
298
- "henry",
299
- "farad",
300
- "coulomb",
301
- ]
302
- }
303
-
304
- def enable_contexts(self, *names_or_contexts: str | Context, **kwargs: Any) -> None:
305
- for context in names_or_contexts:
306
- if isinstance(context, SiunitxContext):
307
- self._preferred_units |= context.preferred_units
308
- return super().enable_contexts(*names_or_contexts, **kwargs)
309
-
310
- def to_preferred_units(self, qty: SiunitxQuantity) -> SiunitxQuantity:
311
- return cast(
312
- SiunitxQuantity,
313
- qty.to(to)
314
- if (to := self._preferred_units.get(qty.dimensionality, None))
315
- else qty.to_reduced_units(),
316
- )
317
-
318
-
319
- class Constants:
320
- _CONSTANTS = {
321
- "cesium_standard": (9192631770, "hertz", "d"), # SI defining constant
322
- "c": (299792458, "meter per second", "d"), # SI defining constant
323
- "h": (6.62607015e-34, "joule second", ".8e"), # SI defining constant
324
- "qe": (1.602176634e-19, "coulomb", ".3e"), # SI defining constant
325
- "kB": (1.380649e-23, "joule per kelvin", "e"), # SI defining constant
326
- "Na": (6.02214076e23, "", ".8e"), # SI defining constant
327
- "R": (8.31446261815324, "joule per kelvin per mole", "f"),
328
- "k": (8.987551e9, "newton meter squared per coulomb squared", ".0e"),
329
- "epsilon0": (
330
- 8.8541878188e-12,
331
- "coulomb squared per newton meter squared",
332
- ".3e",
333
- ),
334
- "mu0": (1.2566370612696005e-06, "tesla meter per ampere", ".3e"),
335
- "mn": (1.67492750056e-27, "kilogram", ".3e"),
336
- "mp": (1.67262192595e-27, "kilogram", ".3e"),
337
- "me": (9.1093837139e-31, "kilogram", ".3e"),
338
- "a0": (5.29177210903e-11, "meter", "e"),
339
- "G": (6.6743e-11, "newton meter squared per kilogram squared", ".2e"),
340
- "g": (9.81, "meter per second squared", ".2f"),
341
- }
342
-
343
- def __init__(self, fname: Path | None):
344
- self._fname = fname
345
- self._nuclide_table = NuclideTable()
346
- self._constants: dict[str, tuple[float, str, str]] = {}
347
-
348
- def _get_default_constant(
349
- self, name: str, fmt: str
350
- ) -> tuple[float, str, str] | None:
351
- if data := self._CONSTANTS.get(name, None):
352
- value, unit, default_fmt = data
353
- return value, unit, default_fmt if fmt == "g" else fmt
354
-
355
- def _get_custom_constant(
356
- self, name: str, fmt: str
357
- ) -> tuple[float, str, str] | None:
358
- if not self._constants and self._fname and self._fname.exists():
359
- self._constants = loads(self._fname.read_bytes())
360
-
361
- if data := self._constants.get(name, None):
362
- value, unit, default_fmt = data
363
- return value, unit, default_fmt if fmt == "g" else fmt
364
-
365
- def _get_nuclide_data(self, name: str, fmt: str) -> tuple[float, str, str] | None:
366
- try:
367
- match name[0]:
368
- case "m":
369
- n = self._nuclide_table.get(symbol=name[1:])
370
- return n.atomic_mass, "amu", "f" if fmt == "g" else fmt
371
- case "t":
372
- n = self._nuclide_table.get(symbol=name[1:])
373
- if n.radioactivity.stable:
374
- raise ValueError(f"No half-life for {n.symbol}")
375
- return n.radioactivity.half_life, "second", "g"
376
- except KeyError:
377
- pass
378
-
379
- def __call__(self, name: str, fmt: str) -> tuple[float, str, str]:
380
- if data := (
381
- self._get_default_constant(name, fmt)
382
- or self._get_custom_constant(name, fmt)
383
- or self._get_nuclide_data(name, fmt)
384
- ):
385
- return data
386
- raise ValueError(f"Constant '{name}' not found")
@@ -0,0 +1,64 @@
1
+ from typing import Any, cast
2
+
3
+ from pint.facets.context import Context
4
+ from pint.registry import GenericUnitRegistry, Unit
5
+
6
+ from physpyx.context import SiunitxContext
7
+ from physpyx.quantity import SiunitxQuantity
8
+
9
+
10
+ class SiunitxUnitRegistry(GenericUnitRegistry):
11
+ Quantity: type = SiunitxQuantity
12
+ Unit: type = Unit
13
+
14
+ def __init__(self):
15
+ super().__init__()
16
+ self.define("heure = hour")
17
+ self.define("heures = hours")
18
+ self.define("jour = day")
19
+ self.define("jours = days")
20
+ self.define("semaine = week")
21
+ self.define("semaines = weeks")
22
+ self.define("mois = month")
23
+ self.define("an = year")
24
+ self.define("ans = years")
25
+ self.define("celerity = speed_of_light")
26
+ self.define("chf = [currency] = franc")
27
+ self.define("ct = 0.01 * chf = centime")
28
+ self._preferred_units = {
29
+ Unit(name).dimensionality: name
30
+ for name in [
31
+ "meter",
32
+ "second",
33
+ "kilogram",
34
+ "ampere",
35
+ "kelvin",
36
+ "mole",
37
+ "meter per second",
38
+ "meter per second squared",
39
+ "newton",
40
+ "joule",
41
+ "watt",
42
+ "pascal",
43
+ "volt",
44
+ "ohm",
45
+ "tesla",
46
+ "henry",
47
+ "farad",
48
+ "coulomb",
49
+ ]
50
+ }
51
+
52
+ def enable_contexts(self, *names_or_contexts: str | Context, **kwargs: Any) -> None:
53
+ for context in names_or_contexts:
54
+ if isinstance(context, SiunitxContext):
55
+ self._preferred_units |= context.preferred_units
56
+ return super().enable_contexts(*names_or_contexts, **kwargs)
57
+
58
+ def to_preferred_units(self, qty: SiunitxQuantity) -> SiunitxQuantity:
59
+ return cast(
60
+ SiunitxQuantity,
61
+ qty.to(to)
62
+ if (to := self._preferred_units.get(qty.dimensionality, None))
63
+ else qty.to_reduced_units(),
64
+ )
File without changes
File without changes