physpyx 2.2.1__tar.gz → 3.0.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,21 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: physpyx
3
- Version: 2.2.1
3
+ Version: 3.0.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>
7
7
  License-Expression: GPL-3.0-or-later
8
- Classifier: Programming Language :: Python :: 3
9
8
  Classifier: Operating System :: Unix
9
+ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Topic :: Education
11
11
  Classifier: Topic :: Scientific/Engineering :: Physics
12
12
  Requires-Dist: numpy>=2.3.4
13
13
  Requires-Dist: pint>=0.25.1
14
14
  Requires-Dist: scipy>=1.17.0
15
15
  Requires-Python: >=3.13
16
- Project-URL: Repository, https://bitbucket.org/jdufour/physpyx/src/master/
17
16
  Project-URL: Changelog, https://bitbucket.org/jdufour/physpyx/src/master/CHANGELOG.md
18
17
  Project-URL: Documentation, https://physpyx.readthedocs.io/fr/latest/
18
+ Project-URL: Repository, https://bitbucket.org/jdufour/physpyx/src/master/
19
19
  Description-Content-Type: text/markdown
20
20
 
21
21
  # PhysPyX
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "physpyx"
3
+ version = "3.0.0"
4
+ description = "Provides a way to use python code in LaTeX"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ license = "GPL-3.0-or-later"
8
+ classifiers = [
9
+ "Operating System :: Unix",
10
+ "Programming Language :: Python :: 3",
11
+ "Topic :: Education",
12
+ "Topic :: Scientific/Engineering :: Physics",
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
+ Changelog = "https://bitbucket.org/jdufour/physpyx/src/master/CHANGELOG.md"
26
+ Documentation = "https://physpyx.readthedocs.io/fr/latest/"
27
+ Repository = "https://bitbucket.org/jdufour/physpyx/src/master/"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "ipdb>=0.13.13",
32
+ "mypy>=2.3.0",
33
+ "ruff>=0.16.0",
34
+ "scipy-stubs>=1.17.1.5",
35
+ ]
36
+ docs = [
37
+ "myst-parser>=4.0.1",
38
+ "sphinx>=7.1.2",
39
+ "sphinx-argparse>=0.5.2",
40
+ "sphinx-autobuild>=2025.8.25",
41
+ "sphinx-autodoc2>=0.5.0",
42
+ "sphinx-design>=0.7.0",
43
+ "sphinx-rtd-theme>=3.0.2",
44
+ ]
45
+
46
+ [build-system]
47
+ requires = ["uv_build>=0.12.0,<0.13.0"]
48
+ build-backend = "uv_build"
@@ -1,46 +1,47 @@
1
1
  [project]
2
2
  name = "physpyx"
3
- version = "2.2.1"
3
+ version = "3.0.0"
4
+ description = "Provides a way to use python code in LaTeX"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ license = "GPL-3.0-or-later"
4
8
  authors = [
5
- { name="Jérôme Dufour", email="jerome.dufour@eduvaud.ch" },
9
+ { name = "Jérôme Dufour", email = "jerome.dufour@eduvaud.ch" },
6
10
  ]
7
- description = "Provides a way to use python code in LaTeX"
8
11
  classifiers = [
9
- "Programming Language :: Python :: 3",
10
- "Operating System :: Unix",
11
- "Topic :: Education",
12
- "Topic :: Scientific/Engineering :: Physics"
12
+ "Operating System :: Unix",
13
+ "Programming Language :: Python :: 3",
14
+ "Topic :: Education",
15
+ "Topic :: Scientific/Engineering :: Physics",
13
16
  ]
14
- license = "GPL-3.0-or-later"
15
- readme = "README.md"
16
- requires-python = ">=3.13"
17
17
  dependencies = [
18
18
  "numpy>=2.3.4",
19
19
  "pint>=0.25.1",
20
20
  "scipy>=1.17.0",
21
21
  ]
22
22
 
23
+ [project.urls]
24
+ Changelog = "https://bitbucket.org/jdufour/physpyx/src/master/CHANGELOG.md"
25
+ Documentation = "https://physpyx.readthedocs.io/fr/latest/"
26
+ Repository = "https://bitbucket.org/jdufour/physpyx/src/master/"
27
+
23
28
  [dependency-groups]
24
29
  dev = [
25
- "mypy>=1.18.1",
26
30
  "ipdb>=0.13.13",
31
+ "mypy>=2.3.0",
32
+ "ruff>=0.16.0",
27
33
  "scipy-stubs>=1.17.1.5",
28
34
  ]
29
35
  docs = [
30
- "sphinx>=7.1.2",
31
- "sphinx-rtd-theme>=3.0.2",
32
- "sphinx-argparse>=0.5.2",
33
- "myst-parser>=4.0.1",
34
- "sphinx-autodoc2>=0.5.0",
35
- "sphinx-autobuild>=2025.8.25",
36
- "sphinx-design>=0.7.0",
36
+ "myst-parser>=4.0.1",
37
+ "sphinx>=7.1.2",
38
+ "sphinx-argparse>=0.5.2",
39
+ "sphinx-autobuild>=2025.8.25",
40
+ "sphinx-autodoc2>=0.5.0",
41
+ "sphinx-design>=0.7.0",
42
+ "sphinx-rtd-theme>=3.0.2",
37
43
  ]
38
44
 
39
45
  [build-system]
40
- requires = ["uv_build>=0.11.0,<0.12.0"]
46
+ requires = ["uv_build>=0.12.0,<0.13.0"]
41
47
  build-backend = "uv_build"
42
-
43
- [project.urls]
44
- Repository = "https://bitbucket.org/jdufour/physpyx/src/master/"
45
- Changelog = "https://bitbucket.org/jdufour/physpyx/src/master/CHANGELOG.md"
46
- Documentation = "https://physpyx.readthedocs.io/fr/latest/"
@@ -4,4 +4,4 @@ from .nuclide import NuclideTable
4
4
  from .physpyx import PhysPyX
5
5
  from .qty import Qty
6
6
 
7
- __all__ = ["NuclideTable", "NuclearContext", "Orbit", "PhysPyX", "Qty"]
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")
@@ -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():
46
- if isinstance(qty, SiunitxQuantity):
47
- for name, new in qty.export(name):
43
+ for variable, qty in sys._getframe(2).f_locals.items():
44
+ if variable != "_" and isinstance(qty, SiunitxQuantity):
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,17 +25,18 @@ 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",
38
32
  "electron_volt": "electronvolt",
33
+ "light_year": "lightyear",
34
+ "meter_Hg": "mHg",
39
35
  "metric_ton": "tonne",
40
36
  "revolutions_per_minute": "rpm",
41
37
  "speed_of_light": "celerity",
42
38
  "standard_atmosphere": "atm",
43
39
  "unified_atomic_mass_unit": "amu",
44
- "meter_Hg": "mHg",
45
40
  "watt_hour": "watthour",
46
41
  }
47
42
 
@@ -54,7 +49,7 @@ class SiunitxQuantity(Quantity):
54
49
  compact: bool = False,
55
50
  preferred: bool = True,
56
51
  alt: AltDict | bool = False,
57
- ) -> "SiunitxQuantity":
52
+ ) -> Self:
58
53
  """
59
54
  Return an instance of SiunitxQuantity
60
55
 
@@ -71,7 +66,7 @@ class SiunitxQuantity(Quantity):
71
66
  :param alt: If provided, writes an additional alternative output with
72
67
  different formatting. The string '_alt' is added to the base name.
73
68
  """
74
- quantity = cast(SiunitxQuantity, super().__new__(cls, value, units))
69
+ quantity = cast(Self, super().__new__(cls, value, units))
75
70
  quantity._fmt = fmt
76
71
  quantity._base = base
77
72
  quantity._compact = compact
@@ -86,6 +81,17 @@ class SiunitxQuantity(Quantity):
86
81
  else:
87
82
  return alt
88
83
 
84
+ def __iter__(self) -> Iterator["SiunitxQuantity"]:
85
+ # NOTE: this is used to set formatting options in tuple expansions:
86
+ # rx, ry = qty([3, 2], "kilometer", fmt=".1e")
87
+ for val in super().__iter__():
88
+ val._fmt = self._fmt
89
+ val._base = self._base
90
+ val._compact = self._compact
91
+ val._preferred = self._preferred
92
+ val._alt = self._alt
93
+ yield val
94
+
89
95
  def __str__(self) -> str:
90
96
  quantity = self
91
97
  if self._base:
@@ -181,7 +187,7 @@ class SiunitxQuantity(Quantity):
181
187
  den += r"\per" + tmp
182
188
  return num + den
183
189
 
184
- def export(self, name: str) -> Generator[tuple[str, str], None, None]:
190
+ def export(self, name: str) -> Generator[tuple[str, str]]:
185
191
  try:
186
192
  yield (
187
193
  name,
@@ -194,7 +200,7 @@ class SiunitxQuantity(Quantity):
194
200
  # the one raised in PTManager.__exit__
195
201
  logger.error(f"exporting {name} failed, {self._fmt}")
196
202
  e.add_note(f"exporting {name} failed")
197
- raise e
203
+ raise
198
204
 
199
205
  if self._alt:
200
206
  alt = self.fmtex(**self._alt)
@@ -262,130 +268,3 @@ class SiunitxQuantity(Quantity):
262
268
  if not isinstance(other, SiunitxQuantity):
263
269
  raise TypeError(f"Can't perform a cross product with a {type(other)}")
264
270
  return cast(SiunitxQuantity, cross(self, other))
265
-
266
-
267
- class SiunitxUnitRegistry(GenericUnitRegistry):
268
- Quantity: type = SiunitxQuantity
269
- Unit: type = Unit
270
-
271
- def __init__(self):
272
- super().__init__()
273
- self.define("heure = hour")
274
- self.define("heures = hours")
275
- self.define("jour = day")
276
- self.define("jours = days")
277
- self.define("semaine = week")
278
- self.define("semaines = weeks")
279
- self.define("mois = month")
280
- self.define("an = year")
281
- self.define("ans = years")
282
- self.define("celerity = speed_of_light")
283
- self.define("chf = [currency] = franc")
284
- self.define("ct = 0.01 * chf = centime")
285
- self._preferred_units = {
286
- Unit(name).dimensionality: name
287
- for name in [
288
- "meter",
289
- "second",
290
- "kilogram",
291
- "ampere",
292
- "kelvin",
293
- "mole",
294
- "meter per second",
295
- "meter per second squared",
296
- "newton",
297
- "joule",
298
- "watt",
299
- "pascal",
300
- "volt",
301
- "ohm",
302
- "tesla",
303
- "henry",
304
- "farad",
305
- "coulomb",
306
- ]
307
- }
308
-
309
- def enable_contexts(self, *names_or_contexts: str | Context, **kwargs: Any) -> None:
310
- for context in names_or_contexts:
311
- if isinstance(context, SiunitxContext):
312
- self._preferred_units |= context.preferred_units
313
- return super().enable_contexts(*names_or_contexts, **kwargs)
314
-
315
- def to_preferred_units(self, qty: SiunitxQuantity) -> SiunitxQuantity:
316
- return cast(
317
- SiunitxQuantity,
318
- qty.to(to)
319
- if (to := self._preferred_units.get(qty.dimensionality, None))
320
- else qty.to_reduced_units(),
321
- )
322
-
323
-
324
- class Constants:
325
- _CONSTANTS = {
326
- "cesium_standard": (9192631770, "hertz", "d"), # SI defining constant
327
- "c": (299792458, "meter per second", "d"), # SI defining constant
328
- "h": (6.62607015e-34, "joule second", ".8e"), # SI defining constant
329
- "qe": (1.602176634e-19, "coulomb", ".3e"), # SI defining constant
330
- "kB": (1.380649e-23, "joule per kelvin", "e"), # SI defining constant
331
- "Na": (6.02214076e23, "", ".8e"), # SI defining constant
332
- "R": (8.31446261815324, "joule per kelvin per mole", "f"),
333
- "k": (8.987551e9, "newton meter squared per coulomb squared", ".0e"),
334
- "epsilon0": (
335
- 8.8541878188e-12,
336
- "coulomb squared per newton meter squared",
337
- ".3e",
338
- ),
339
- "mu0": (1.2566370612696005e-06, "tesla meter per ampere", ".3e"),
340
- "mn": (1.67492750056e-27, "kilogram", ".3e"),
341
- "mp": (1.67262192595e-27, "kilogram", ".3e"),
342
- "me": (9.1093837139e-31, "kilogram", ".3e"),
343
- "a0": (5.29177210903e-11, "meter", "e"),
344
- "G": (6.6743e-11, "newton meter squared per kilogram squared", ".2e"),
345
- "g": (9.81, "meter per second squared", ".2f"),
346
- }
347
-
348
- def __init__(self, fname: Path | None):
349
- self._fname = fname
350
- self._nuclide_table = NuclideTable()
351
- self._constants: dict[str, tuple[float, str, str]] = {}
352
-
353
- def _get_default_constant(
354
- self, name: str, fmt: str
355
- ) -> tuple[float, str, str] | None:
356
- if data := self._CONSTANTS.get(name, None):
357
- value, unit, default_fmt = data
358
- return value, unit, default_fmt if fmt == "g" else fmt
359
-
360
- def _get_custom_constant(
361
- self, name: str, fmt: str
362
- ) -> tuple[float, str, str] | None:
363
- if not self._constants and self._fname and self._fname.exists():
364
- self._constants = loads(self._fname.read_bytes())
365
-
366
- if data := self._constants.get(name, None):
367
- value, unit, default_fmt = data
368
- return value, unit, default_fmt if fmt == "g" else fmt
369
-
370
- def _get_nuclide_data(self, name: str, fmt: str) -> tuple[float, str, str] | None:
371
- try:
372
- match name[0]:
373
- case "m":
374
- n = self._nuclide_table.get(symbol=name[1:])
375
- return n.atomic_mass, "amu", "f" if fmt == "g" else fmt
376
- case "t":
377
- n = self._nuclide_table.get(symbol=name[1:])
378
- if n.radioactivity.stable:
379
- raise ValueError(f"No half-life for {n.symbol}")
380
- return n.radioactivity.half_life, "second", "g"
381
- except KeyError:
382
- pass
383
-
384
- def __call__(self, name: str, fmt: str) -> tuple[float, str, str]:
385
- if data := (
386
- self._get_default_constant(name, fmt)
387
- or self._get_custom_constant(name, fmt)
388
- or self._get_nuclide_data(name, fmt)
389
- ):
390
- return data
391
- raise ValueError(f"Constant '{name}' not found")
@@ -0,0 +1,66 @@
1
+ from typing import Any, cast
2
+
3
+ from pint.facets.context import Context
4
+ from pint.registry import GenericUnitRegistry, Unit
5
+ from pint.util import UnitsContainer
6
+
7
+ from physpyx.context import SiunitxContext
8
+ from physpyx.quantity import SiunitxQuantity
9
+
10
+
11
+ class SiunitxUnitRegistry(GenericUnitRegistry):
12
+ Quantity: type = SiunitxQuantity
13
+ Unit: type = Unit
14
+
15
+ def __init__(self):
16
+ super().__init__()
17
+ self.define("heure = hour")
18
+ self.define("heures = hours")
19
+ self.define("jour = day")
20
+ self.define("jours = days")
21
+ self.define("semaine = week")
22
+ self.define("semaines = weeks")
23
+ self.define("mois = month")
24
+ self.define("an = year")
25
+ self.define("ans = years")
26
+ self.define("celerity = speed_of_light")
27
+ self.define("swissfranc = [currency]")
28
+ self.define("swisscent = swissfranc * 0.01")
29
+ self._preferred_units = {
30
+ Unit(name).dimensionality: name
31
+ for name in [
32
+ "ampere",
33
+ "coulomb",
34
+ "farad",
35
+ "henry",
36
+ "joule",
37
+ "kelvin",
38
+ "kilogram",
39
+ "meter per second squared",
40
+ "meter per second",
41
+ "meter",
42
+ "mole",
43
+ "newton",
44
+ "ohm",
45
+ "pascal",
46
+ "second",
47
+ "tesla",
48
+ "volt",
49
+ "watt",
50
+ ]
51
+ }
52
+ self._preferred_units[UnitsContainer({"[currency]": 1})] = "swissfranc"
53
+
54
+ def enable_contexts(self, *names_or_contexts: str | Context, **kwargs: Any) -> None:
55
+ for context in names_or_contexts:
56
+ if isinstance(context, SiunitxContext):
57
+ self._preferred_units |= context.preferred_units
58
+ return super().enable_contexts(*names_or_contexts, **kwargs)
59
+
60
+ def to_preferred_units(self, qty: SiunitxQuantity) -> SiunitxQuantity:
61
+ return cast(
62
+ SiunitxQuantity,
63
+ qty.to(to)
64
+ if (to := self._preferred_units.get(qty.dimensionality, None))
65
+ else qty.to_reduced_units(),
66
+ )
File without changes
File without changes
File without changes