Coverage for install/scipp/units/__init__.py: 58%
65 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-17 01:51 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-17 01:51 +0000
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3# @author Jan-Lukas Wynen
4"""Unit helpers and predefined units.
6The following predefined units are available:
8Dimensionless (those two names are equivalent):
9 - dimensionless
10 - one
12Common units:
13 - angstrom
14 - counts
15 - deg
16 - kg
17 - K
18 - meV
19 - m
20 - rad
21 - s
22 - us
23 - ns
24 - mm
26Special:
27 - default_unit (used by some functions to deduce a unit)
29.. seealso::
30 :py:class:`scipp.Unit` to construct other units.
31"""
33from collections.abc import Generator, Iterator
34from contextlib import contextmanager
36from .._scipp.core import add_unit_alias as _add_unit_alias
37from .._scipp.core import clear_unit_aliases as _clear_unit_aliases
38from .._scipp.core.units import (
39 K,
40 angstrom,
41 counts,
42 default_unit,
43 deg,
44 dimensionless,
45 kg,
46 m,
47 meV,
48 mm,
49 ns,
50 one,
51 rad,
52 s,
53 us,
54)
55from ..core.cpp_classes import DefaultUnit, Unit, UnitError, Variable, VariancesError
58class UnitAliases:
59 """Manager for unit aliases.
61 Aliases override how units are converted to and from strings.
62 The table is similar to a :class:`dict` and maps alias names to units.
63 But unlike a dict, no guarantees are made about the order of aliases or their
64 priority in string formatting.
65 And there may be only one alias for each unit at a time.
67 Attention
68 ---------
69 This class is a singleton and should never be instantiated by user code.
70 Instead, use it through :attr:`scipp.units.aliases`.
71 """
73 def __init__(self) -> None:
74 if any(isinstance(x, UnitAliases) for x in globals().values()):
75 raise RuntimeError('There can be only one instance of _Aliases')
76 self._aliases: dict[str, Unit] = {}
78 def __setitem__(self, alias: str, unit: str | Unit | Variable) -> None:
79 """Define a new unit alias."""
80 unit = _build_unit(unit)
81 if self._aliases.get(alias) == unit:
82 return
83 if unit in self.values():
84 raise ValueError(f"There already is an alias for unit '{unit!r}'")
85 _add_unit_alias(name=alias, unit=unit)
86 self._aliases[alias] = unit
88 def __delitem__(self, alias: str) -> None:
89 """Remove an existing alias."""
90 self._del_aliases(alias)
92 def _del_aliases(self, *names: str) -> None:
93 old_aliases = dict(self._aliases)
94 for name in names:
95 del old_aliases[name]
96 self.clear()
97 for name, unit in old_aliases.items():
98 self[name] = unit
100 def clear(self) -> None:
101 """Remove all aliases."""
102 self._aliases.clear()
103 _clear_unit_aliases()
105 @contextmanager
106 def scoped(self, **kwargs: str | Unit) -> Generator[None, None, None]:
107 """Contextmanager to define temporary aliases.
109 Defines new aliases based on ``kwargs`` for the duration of the context.
110 When exiting the context, all temporary aliases are removed.
112 It is possible to define additional aliases in the context.
113 They are not removed when the context manager exits unless they override
114 scoped aliases. (See examples.)
116 Warning
117 -------
118 This context manager is not thread-safe.
119 Aliases defined here affect all threads and other threads can define different
120 aliases which affect the managed context.
122 Parameters
123 ----------
124 **kwargs
125 Map from names to units for aliases to define.
127 Examples
128 --------
129 Define temporary aliases:
131 >>> with sc.units.aliases.scoped(speed='m/s'):
132 ... str(sc.Unit('m/s'))
133 'speed'
135 Previously defined aliases still apply:
137 >>> sc.units.aliases.clear()
138 >>> sc.units.aliases['dogyear'] = '4492800s'
139 >>> with sc.units.aliases.scoped(speed='m/s'):
140 ... str(sc.Unit('4492800s'))
141 'dogyear'
143 Previous aliases can be overridden and are restored after the context:
145 >>> sc.units.aliases.clear()
146 >>> sc.units.aliases['speed'] = 'km/s'
147 >>> with sc.units.aliases.scoped(speed='m/s'):
148 ... sc.Unit('speed') == 'm/s'
149 True
150 >>> sc.Unit('speed') == 'km/s'
151 True
153 Aliases defined within the context remain active
154 unless they clash with previous aliases:
156 >>> sc.units.aliases.clear()
157 >>> sc.units.aliases['speed'] = 'km/s'
158 >>> with sc.units.aliases.scoped(speed='m/s'):
159 ... sc.units.aliases['speed'] = 'mm/s'
160 ... sc.units.aliases['dogyear'] = '4492800s'
161 >>> str(sc.Unit('4492800s'))
162 'dogyear'
163 >>> sc.Unit('speed') == 'km/s'
164 True
165 """
166 overridden = {
167 name: unit for name, unit in self._aliases.items() if name in kwargs
168 }
169 for name, unit in kwargs.items():
170 self[name] = unit
171 yield
172 self._del_aliases(*kwargs)
173 for name, unit in overridden.items():
174 self[name] = unit
176 def __iter__(self) -> Iterator[str]:
177 """Iterator over alias names."""
178 yield from self.keys()
180 def keys(self) -> Iterator[str]:
181 """Iterator over alias names."""
182 yield from self._aliases.keys()
184 def values(self) -> Iterator[Unit]:
185 """Iterator over aliased units."""
186 yield from self._aliases.values()
188 def items(self) -> Iterator[tuple[str, Unit]]:
189 """Iterator over pairs of alias names and units."""
190 yield from self._aliases.items()
192 # Making copies would allow _Alias's internal map and
193 # LLNL/Unit's global map to get out of sync.
194 def __copy__(self) -> None:
195 raise TypeError('UnitAliases is a singleton and must not be copied')
197 def __deepcopy__(self) -> None:
198 raise TypeError('UnitAliases is a singleton and must not be copied')
201def _build_unit(x: str | Unit | Variable) -> Unit:
202 if isinstance(x, Unit):
203 return x
204 if isinstance(x, str):
205 return Unit(x)
206 if x.variance is not None:
207 raise VariancesError('Cannot define a unit with a variance')
208 if x.unit is None:
209 raise UnitError('Cannot define a unit based on a variable without units')
210 # Convert to float first to make sure the variable only contains a
211 # multiplier and not a string that would be multiplied to the unit.
212 return Unit(str(float(x.value))) * x.unit
215aliases = UnitAliases()
216"""Table of unit aliases."""
218__all__ = [
219 'DefaultUnit',
220 'angstrom',
221 'counts',
222 'default_unit',
223 'deg',
224 'dimensionless',
225 'kg',
226 'K',
227 'meV',
228 'm',
229 'one',
230 'rad',
231 's',
232 'us',
233 'ns',
234 'mm',
235]