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

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. 

5 

6The following predefined units are available: 

7 

8Dimensionless (those two names are equivalent): 

9 - dimensionless 

10 - one 

11 

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 

25 

26Special: 

27 - default_unit (used by some functions to deduce a unit) 

28 

29.. seealso:: 

30 :py:class:`scipp.Unit` to construct other units. 

31""" 

32 

33from collections.abc import Generator, Iterator 

34from contextlib import contextmanager 

35 

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 

56 

57 

58class UnitAliases: 

59 """Manager for unit aliases. 

60 

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. 

66 

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 """ 

72 

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] = {} 

77 

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 

87 

88 def __delitem__(self, alias: str) -> None: 

89 """Remove an existing alias.""" 

90 self._del_aliases(alias) 

91 

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 

99 

100 def clear(self) -> None: 

101 """Remove all aliases.""" 

102 self._aliases.clear() 

103 _clear_unit_aliases() 

104 

105 @contextmanager 

106 def scoped(self, **kwargs: str | Unit) -> Generator[None, None, None]: 

107 """Contextmanager to define temporary aliases. 

108 

109 Defines new aliases based on ``kwargs`` for the duration of the context. 

110 When exiting the context, all temporary aliases are removed. 

111 

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.) 

115 

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. 

121 

122 Parameters 

123 ---------- 

124 **kwargs 

125 Map from names to units for aliases to define. 

126 

127 Examples 

128 -------- 

129 Define temporary aliases: 

130 

131 >>> with sc.units.aliases.scoped(speed='m/s'): 

132 ... str(sc.Unit('m/s')) 

133 'speed' 

134 

135 Previously defined aliases still apply: 

136 

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' 

142 

143 Previous aliases can be overridden and are restored after the context: 

144 

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 

152 

153 Aliases defined within the context remain active 

154 unless they clash with previous aliases: 

155 

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 

175 

176 def __iter__(self) -> Iterator[str]: 

177 """Iterator over alias names.""" 

178 yield from self.keys() 

179 

180 def keys(self) -> Iterator[str]: 

181 """Iterator over alias names.""" 

182 yield from self._aliases.keys() 

183 

184 def values(self) -> Iterator[Unit]: 

185 """Iterator over aliased units.""" 

186 yield from self._aliases.values() 

187 

188 def items(self) -> Iterator[tuple[str, Unit]]: 

189 """Iterator over pairs of alias names and units.""" 

190 yield from self._aliases.items() 

191 

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') 

196 

197 def __deepcopy__(self) -> None: 

198 raise TypeError('UnitAliases is a singleton and must not be copied') 

199 

200 

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 

213 

214 

215aliases = UnitAliases() 

216"""Table of unit aliases.""" 

217 

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]