Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
7ff7e90
✨ feat(fmt): add the string-formatting engine (#683)
nstarman Aug 7, 2026
3a69cd1
♻️ refactor(quantity): route the IPython reprs through the fmt engine
nstarman Aug 7, 2026
6a36314
✨ feat(quantity): route `__format__` through the fmt engine (#683)
nstarman Aug 7, 2026
2746fb2
🐛 fix(unitsystems): render every unit system the same way
nstarman Aug 7, 2026
f29530b
💥 boom(unitsystems)!: make `repr` round-trippable, `str` readable
nstarman Aug 7, 2026
e7372b1
✅ test(fmt): cover the `custom=` chain and the array-summary branches
nstarman Aug 8, 2026
d23c711
♻️ refactor(fmt)!: make the engine private as `unxt._fmt`, and fix py…
nstarman Aug 8, 2026
da50140
🐛 fix(interop-gala): requote the unit-system doctests
nstarman Aug 8, 2026
176be63
🐛 fix(quantity): drop the branch in the `custom=` hook chain
nstarman Aug 8, 2026
0ec3633
♻️ refactor(fmt): invert the dependency -- consumers register into th…
nstarman Aug 8, 2026
d41c33c
♻️ refactor(fmt): render docs through wadler-lindig's public API
nstarman Aug 13, 2026
d8ceb41
🐛 fix(unitsystems): repr named realizations by name; use the list form
nstarman Aug 13, 2026
b13ccf9
🐛 fix(interop-gala): use the list form in the unit-system doctests
nstarman Aug 13, 2026
c45757a
📝 docs: fix and un-skip the dynamical unit-system example
nstarman Aug 13, 2026
e116319
🐛 fix(fmt): strip `$...$` only when the LaTeX fragment has them
nstarman Aug 13, 2026
affbc94
⚰️ dead(unitsystems): drop the full-precision unit spelling
nstarman Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,13 @@ Unit systems define consistent sets of base units for specific domains. `unxt` p

```{code-block} python
>>> u.unitsystem("si") # SI (International System of Units)
unitsystem(m, kg, s, mol, A, K, cd, rad)
unitsystem(['m', 'kg', 's', 'mol', 'A', 'K', 'cd', 'rad'])

>>> u.unitsystem("cgs") # CGS (centimeter-gram-second)
unitsystem(cm, g, s, dyn, erg, Ba, P, St, rad)
unitsystem(['cm', 'g', 's', 'dyn', 'erg', 'Ba', 'P', 'St', 'rad'])

>>> u.unitsystem("galactic") # galactic (astrophysics)
unitsystem(kpc, Myr, solMass, rad)
unitsystem(['kpc', 'Myr', 'solMass', 'rad'])
```

### Composing Units from a Unit System
Expand All @@ -167,7 +167,7 @@ Create custom unit systems by specifying base units:
```{code-block} python
>>> custom_usys = u.unitsystem("km", "h", "tonne", "degree")
>>> custom_usys
unitsystem(km, h, t, deg)
unitsystem(['km', 'h', 't', 'deg'])
```

Derived units are then available by dimension:
Expand All @@ -186,8 +186,7 @@ For domains like gravitational dynamics, use dynamical unit systems where $G = 1

>>> dyn_usys = u.unitsystem(DynamicalSimUSysFlag, "kpc", "Myr")
>>> dyn_usys
LengthMassTimeUnitSystem(length=Unit("kpc"),
mass=Unit("1.49828e+10 kpc3 s2 kg / (Myr2 m3)"), time=Unit("Myr"))
unitsystem(['kpc', '1.49828e+10 kpc3 s2 kg / (Myr2 m3)', 'Myr'])
```

The mass unit is the derived one — an exact composite expression, not a rounded label:
Expand Down
32 changes: 16 additions & 16 deletions docs/guides/units_and_systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,25 +103,25 @@ Unit("erg")
```{code-block} python
>>> from unxt.unitsystems import si
>>> si
unitsystem(m, kg, s, mol, A, K, cd, rad)
unitsystem(['m', 'kg', 's', 'mol', 'A', 'K', 'cd', 'rad'])
```

```{code-block} python
>>> from unxt.unitsystems import cgs
>>> cgs
unitsystem(cm, g, s, dyn, erg, Ba, P, St, rad)
unitsystem(['cm', 'g', 's', 'dyn', 'erg', 'Ba', 'P', 'St', 'rad'])
```

```{code-block} python
>>> from unxt.unitsystems import galactic
>>> galactic
unitsystem(kpc, Myr, solMass, rad)
unitsystem(['kpc', 'Myr', 'solMass', 'rad'])
```

```{code-block} python
>>> from unxt.unitsystems import solarsystem
>>> solarsystem
unitsystem(AU, yr, solMass, rad)
unitsystem(['AU', 'yr', 'solMass', 'rad'])
```

#### Natural unit systems
Expand All @@ -132,16 +132,16 @@ unitsystem(AU, yr, solMass, rad)
>>> from unxt.unitsystems import hep, geometrized, planck, atomic

>>> hep # high-energy physics: hbar = c = 1 (1 GeV scale)
LengthMassTimeUnitSystem(length=Unit("...e-16 m"), mass=Unit("...e-27 kg"), time=Unit("...e-25 s"))
unitsystem('hep')

>>> geometrized # general relativity: c = G = 1 (1 m scale)
LengthMassTimeUnitSystem(length=Unit("m"), mass=Unit("...e+27 kg"), time=Unit("...e-09 s"))
unitsystem('geometrized')

>>> planck # hbar = c = G = k_B = 1
LengthMassTimeTemperatureUnitSystem(length=Unit("...e-35 m"), mass=Unit("...e-08 kg"), time=Unit("...e-44 s"), temperature=Unit("...e+32 K"))
unitsystem('planck')

>>> atomic # Hartree: m_e = hbar = e = 4*pi*eps0 = 1
LengthMassTimeElectricalChargeUnitSystem(length=Unit("...e-11 m"), mass=Unit("...e-31 kg"), time=Unit("...e-17 s"), electrical_charge=Unit("...e-19 A s"))
unitsystem('atomic')
```

By construction the defining constants evaluate to 1 in each system, and the two systems with a remaining free scale (`HEPUSysFlag`, `GeometrizedUSysFlag`) accept it as a keyword. For worked examples on each system — setting those scales, recovering familiar values, and the semantics of natural-unit quantities — see the {doc}`natural-units` guide.
Expand All @@ -158,16 +158,16 @@ By construction the defining constants evaluate to 1 in each system, and the two

```{code-block} python
>>> unitsystem("si")
unitsystem(m, kg, s, mol, A, K, cd, rad)
unitsystem(['m', 'kg', 's', 'mol', 'A', 'K', 'cd', 'rad'])

>>> unitsystem("cgs")
unitsystem(cm, g, s, dyn, erg, Ba, P, St, rad)
unitsystem(['cm', 'g', 's', 'dyn', 'erg', 'Ba', 'P', 'St', 'rad'])

>>> unitsystem("galactic")
unitsystem(kpc, Myr, solMass, rad)
unitsystem(['kpc', 'Myr', 'solMass', 'rad'])

>>> unitsystem("solarsystem")
unitsystem(AU, yr, solMass, rad)
unitsystem(['AU', 'yr', 'solMass', 'rad'])

```

Expand All @@ -179,7 +179,7 @@ Unit systems are statically defined, the "galactic" and "solarsystem" units are

>>> usys = unitsystem("kpc", "Myr", "solMass", "degree")
>>> usys
unitsystem(kpc, Myr, solMass, deg)
unitsystem(['kpc', 'Myr', 'solMass', 'deg'])

>>> isinstance(usys, LTMAUnitSystem)
True
Expand All @@ -197,7 +197,7 @@ If the set of units does not correspond to any pre-defined unit system class, `u

>>> usys = unitsystem("kpc", "Myr", "solMass", "degree", "candela")
>>> usys
AngleLengthLuminousIntensityMassTimeUnitSystem(angle=Unit("deg"), length=Unit("kpc"), luminous_intensity=Unit("cd"), mass=Unit("solMass"), time=Unit("Myr"))
unitsystem(['deg', 'kpc', 'cd', 'solMass', 'Myr'])

>>> isinstance(usys, LTMAUnitSystem)
False
Expand All @@ -220,7 +220,7 @@ The dimensionless unit system is not the only special unit system. `unxt` also s
>>> from unxt.unitsystems import DynamicalSimUSysFlag

>>> unitsystem(DynamicalSimUSysFlag, "m", "kg")
LengthMassTimeUnitSystem(length=Unit("m"), mass=Unit("kg"), time=Unit("122404 s"))
unitsystem(['m', 'kg', '122404 s'])

```

Expand All @@ -236,7 +236,7 @@ Also, `unitsystem` can replace a unit in a unit system or extend a unit system.
True

>>> unitsystem(usys, "deg")
unitsystem(m, s, kg, deg)
unitsystem(['m', 's', 'kg', 'deg'])

```

Expand Down
14 changes: 7 additions & 7 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,19 +305,19 @@ Unit systems define consistent sets of base units for specific domains. `unxt` p

>>> si = u.unitsystem("si")
>>> si
unitsystem(m, kg, s, mol, A, K, cd, rad)
unitsystem(['m', 'kg', 's', 'mol', 'A', 'K', 'cd', 'rad'])

>>> cgs = u.unitsystem("cgs")
>>> cgs
unitsystem(cm, g, s, dyn, erg, Ba, P, St, rad)
unitsystem(['cm', 'g', 's', 'dyn', 'erg', 'Ba', 'P', 'St', 'rad'])

>>> galactic = u.unitsystem("galactic")
>>> galactic
unitsystem(kpc, Myr, solMass, rad)
unitsystem(['kpc', 'Myr', 'solMass', 'rad'])

>>> solarsystem = u.unitsystem("solarsystem")
>>> solarsystem
unitsystem(AU, yr, solMass, rad)
unitsystem(['AU', 'yr', 'solMass', 'rad'])

```

Expand Down Expand Up @@ -358,7 +358,7 @@ Create custom unit systems by specifying base units:

>>> custom_usys = u.unitsystem("km", "h", "tonne", "degree")
>>> custom_usys
unitsystem(km, h, t, deg)
unitsystem(['km', 'h', 't', 'deg'])

>>> custom_usys["velocity"]
Unit("km / h")
Expand All @@ -374,8 +374,8 @@ For domains like gravitational dynamics, use dynamical unit systems where $G = 1
>>> from unxt.unitsystems import DynamicalSimUSysFlag

>>> usys = u.unitsystem(DynamicalSimUSysFlag, "kpc", "Myr")
>>> usys # doctest: +SKIP
unitsystem(kpc, Myr, ...)
>>> usys
unitsystem(['kpc', '...kpc3 s2 kg / (Myr2 m3)', 'Myr'])

```

Expand Down
2 changes: 1 addition & 1 deletion docs/interop/dataclassish.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Let's explore unit systems with `dataclassish`.
>>> # Create a unit system
>>> si = u.unitsystem("si")
>>> print(f"Unit System: {si}")
Unit System: SIUnitSystem(length, mass, time, amount, electric_current, temperature, luminous_intensity, angle)
Unit System: unitsystem(m, kg, s, mol, A, K, cd, rad)
>>> print(f"Type: {type(si)}")
Type: <class 'unxt...unitsystems...SIUnitSystem'>

Expand Down
4 changes: 2 additions & 2 deletions packages/unxts.interop.gala/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Convert a `gala.units.UnitSystem` to a [`unxt.unitsystems.AbstractUnitSystem`][u
>>> from unxts.interop.gala import convert_gala_unitsystem_to_unxt_unitsystem

>>> convert_gala_unitsystem_to_unxt_unitsystem(gu.galactic)
unitsystem(kpc, Myr, solMass, rad)
unitsystem(['kpc', 'Myr', 'solMass', 'rad'])

```

Expand Down Expand Up @@ -47,7 +47,7 @@ The two calls above are equivalent to `plum.convert` with the target type, which
>>> from plum import convert

>>> convert(gu.galactic, unxt.AbstractUnitSystem)
unitsystem(kpc, Myr, solMass, rad)
unitsystem(['kpc', 'Myr', 'solMass', 'rad'])

>>> convert(usys, gu.UnitSystem)
<UnitSystem (kpc, Myr, solMass, rad)>
Expand Down
4 changes: 2 additions & 2 deletions packages/unxts.interop.gala/docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The most direct route is {func}`unxt.unitsystem`, which accepts a `gala.units.Un
<UnitSystem (kpc, Myr, solMass, rad)>

>>> unxt.unitsystem(gu.galactic)
unitsystem(kpc, Myr, solMass, rad)
unitsystem(['kpc', 'Myr', 'solMass', 'rad'])

```

Expand All @@ -35,7 +35,7 @@ Because the conversions are registered with `plum`, you can equivalently use `pl

>>> usys = convert(gu.galactic, unxt.AbstractUnitSystem)
>>> usys
unitsystem(kpc, Myr, solMass, rad)
unitsystem(['kpc', 'Myr', 'solMass', 'rad'])

```

Expand Down
2 changes: 1 addition & 1 deletion packages/unxts.interop.gala/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pip install unxts.interop.gala
<UnitSystem (kpc, Myr, solMass, rad)>

>>> unxt.unitsystem(gu.galactic) # as a unxt unit system
unitsystem(kpc, Myr, solMass, rad)
unitsystem(['kpc', 'Myr', 'solMass', 'rad'])

```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def unitsystem(value: gala.units.UnitSystem, /) -> AbstractUnitSystem:
>>> usys = gu.UnitSystem(apyu.km, apyu.s, apyu.Msun, apyu.radian)

>>> u.unitsystem(usys)
unitsystem(km, s, solMass, rad)
unitsystem(['km', 's', 'solMass', 'rad'])

"""
# Create a new unit system instance, and possibly class.
Expand Down Expand Up @@ -77,7 +77,7 @@ def convert_gala_unitsystem_to_unxt_unitsystem(
<UnitSystem (km, s, solMass, rad)>

>>> convert(usys, u.AbstractUnitSystem)
unitsystem(km, s, solMass, rad)
unitsystem(['km', 's', 'solMass', 'rad'])

"""
return unitsystem(usys)
Expand All @@ -103,7 +103,7 @@ def convert_unxt_unitsystem_to_gala_unitsystem(

>>> usys = u.unitsystem(apyu.km, apyu.s, apyu.Msun, apyu.radian)
>>> usys
unitsystem(km, s, solMass, rad)
unitsystem(['km', 's', 'solMass', 'rad'])

>>> convert(usys, gu.UnitSystem)
<UnitSystem (km, s, solMass, rad)>
Expand Down
22 changes: 22 additions & 0 deletions packages/unxts.linalg/src/unxts/linalg/_src/_units_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import plum

import unxt as u
import unxt._fmt as ufmt

T = TypeVar("T")

Expand Down Expand Up @@ -609,3 +610,24 @@ def unit_of(obj: UnitsMatrix, /) -> UnitsMatrix:

"""
return obj


@ufmt.pparts.dispatch # type: ignore[misc]
def pparts(obj: UnitsMatrix, /, *, markup: str = "text", **kw: Any) -> tuple[Any, ...]:
"""Decompose a `UnitsMatrix` for the `unxt._fmt` engine.

Without this registration a `QuantityMatrix` would fall through to
`unxt._fmt.pparts`'s `Any` method and render its unit structure via `str`.
It also has to ship with the engine's markup wiring: `_repr_latex_` used to
slice ``[1:-1]`` off whatever the unit's repr produced, assuming ``$...$``
wrapping, which silently ate real characters here.

Examples
--------
>>> from unxts.linalg import UnitsMatrix
>>> from unxt._fmt import pparts
>>> pparts(UnitsMatrix(("m", "s", "kg")))
(PPart(role='unit', text='(m, s, kg)', kind='content'),)

"""
return (ufmt.PPart("unit", obj.to_string()),)
27 changes: 18 additions & 9 deletions packages/unxts.linalg/tests/test_printing.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,21 @@ def test_use_short_name_true(self):
assert "unit='(m, s, kg)'" in result


def test_repr_latex_does_not_eat_characters():
r"""Regression: `_repr_latex_` used to slice `[1:-1]` off the unit repr.

That assumed astropy's `$...$` wrapping. `UnitsMatrix` has no
`_repr_latex_`, so the fallback `__repr__` was sliced and lost its first
and last characters, corrupting the output instead of raising.
"""
qm = QuantityMatrix(jnp.array([1.0, 2.0, 3.0]), unit=("m", "s", "kg"))
assert qm._repr_latex_() == r'$[1.,~2.,~3.] \; UnitsMatrix("(m, s, kg)")$'
class TestQuantityMatrixMarkup:
"""The IPython representations route through the ``unxt._fmt`` engine."""

def test_latex_does_not_eat_characters(self):
r"""Regression: the old ``[1:-1]`` slice assumed ``$...$`` wrapping.

``UnitsMatrix`` has no ``_repr_latex_``, so the fallback ``__repr__``
was sliced and lost its first and last characters, producing
``... \; nitsMatrix("(m, s, kg)"``.
"""
qm = QuantityMatrix(jnp.array([1.0, 2.0, 3.0]), unit=("m", "s", "kg"))
assert qm._repr_latex_() == r"$[1.,~2.,~3.] \; (m, s, kg)$"

def test_html_shows_the_unit_structure(self):
qm = QuantityMatrix(jnp.array([1.0, 2.0, 3.0]), unit=("m", "s", "kg"))
assert qm._repr_html_() == (
"<span>[1., 2., 3.]</span> * <span>(m, s, kg)</span>"
)
Loading
Loading