Coverage for /usr/lib/python3/dist-packages/sympy/physics/units/dimensions.py: 58%

288 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1""" 

2Definition of physical dimensions. 

3 

4Unit systems will be constructed on top of these dimensions. 

5 

6Most of the examples in the doc use MKS system and are presented from the 

7computer point of view: from a human point, adding length to time is not legal 

8in MKS but it is in natural system; for a computer in natural system there is 

9no time dimension (but a velocity dimension instead) - in the basis - so the 

10question of adding time to length has no meaning. 

11""" 

12 

13from __future__ import annotations 

14 

15import collections 

16from functools import reduce 

17 

18from sympy.core.basic import Basic 

19from sympy.core.containers import (Dict, Tuple) 

20from sympy.core.singleton import S 

21from sympy.core.sorting import default_sort_key 

22from sympy.core.symbol import Symbol 

23from sympy.core.sympify import sympify 

24from sympy.matrices.dense import Matrix 

25from sympy.functions.elementary.trigonometric import TrigonometricFunction 

26from sympy.core.expr import Expr 

27from sympy.core.power import Pow 

28 

29 

30class _QuantityMapper: 

31 

32 _quantity_scale_factors_global: dict[Expr, Expr] = {} 

33 _quantity_dimensional_equivalence_map_global: dict[Expr, Expr] = {} 

34 _quantity_dimension_global: dict[Expr, Expr] = {} 

35 

36 def __init__(self, *args, **kwargs): 

37 self._quantity_dimension_map = {} 

38 self._quantity_scale_factors = {} 

39 

40 def set_quantity_dimension(self, quantity, dimension): 

41 """ 

42 Set the dimension for the quantity in a unit system. 

43 

44 If this relation is valid in every unit system, use 

45 ``quantity.set_global_dimension(dimension)`` instead. 

46 """ 

47 from sympy.physics.units import Quantity 

48 dimension = sympify(dimension) 

49 if not isinstance(dimension, Dimension): 

50 if dimension == 1: 

51 dimension = Dimension(1) 

52 else: 

53 raise ValueError("expected dimension or 1") 

54 elif isinstance(dimension, Quantity): 

55 dimension = self.get_quantity_dimension(dimension) 

56 self._quantity_dimension_map[quantity] = dimension 

57 

58 def set_quantity_scale_factor(self, quantity, scale_factor): 

59 """ 

60 Set the scale factor of a quantity relative to another quantity. 

61 

62 It should be used only once per quantity to just one other quantity, 

63 the algorithm will then be able to compute the scale factors to all 

64 other quantities. 

65 

66 In case the scale factor is valid in every unit system, please use 

67 ``quantity.set_global_relative_scale_factor(scale_factor)`` instead. 

68 """ 

69 from sympy.physics.units import Quantity 

70 from sympy.physics.units.prefixes import Prefix 

71 scale_factor = sympify(scale_factor) 

72 # replace all prefixes by their ratio to canonical units: 

73 scale_factor = scale_factor.replace( 

74 lambda x: isinstance(x, Prefix), 

75 lambda x: x.scale_factor 

76 ) 

77 # replace all quantities by their ratio to canonical units: 

78 scale_factor = scale_factor.replace( 

79 lambda x: isinstance(x, Quantity), 

80 lambda x: self.get_quantity_scale_factor(x) 

81 ) 

82 self._quantity_scale_factors[quantity] = scale_factor 

83 

84 def get_quantity_dimension(self, unit): 

85 from sympy.physics.units import Quantity 

86 # First look-up the local dimension map, then the global one: 

87 if unit in self._quantity_dimension_map: 

88 return self._quantity_dimension_map[unit] 

89 if unit in self._quantity_dimension_global: 

90 return self._quantity_dimension_global[unit] 

91 if unit in self._quantity_dimensional_equivalence_map_global: 

92 dep_unit = self._quantity_dimensional_equivalence_map_global[unit] 

93 if isinstance(dep_unit, Quantity): 

94 return self.get_quantity_dimension(dep_unit) 

95 else: 

96 return Dimension(self.get_dimensional_expr(dep_unit)) 

97 if isinstance(unit, Quantity): 

98 return Dimension(unit.name) 

99 else: 

100 return Dimension(1) 

101 

102 def get_quantity_scale_factor(self, unit): 

103 if unit in self._quantity_scale_factors: 

104 return self._quantity_scale_factors[unit] 

105 if unit in self._quantity_scale_factors_global: 

106 mul_factor, other_unit = self._quantity_scale_factors_global[unit] 

107 return mul_factor*self.get_quantity_scale_factor(other_unit) 

108 return S.One 

109 

110 

111class Dimension(Expr): 

112 """ 

113 This class represent the dimension of a physical quantities. 

114 

115 The ``Dimension`` constructor takes as parameters a name and an optional 

116 symbol. 

117 

118 For example, in classical mechanics we know that time is different from 

119 temperature and dimensions make this difference (but they do not provide 

120 any measure of these quantites. 

121 

122 >>> from sympy.physics.units import Dimension 

123 >>> length = Dimension('length') 

124 >>> length 

125 Dimension(length) 

126 >>> time = Dimension('time') 

127 >>> time 

128 Dimension(time) 

129 

130 Dimensions can be composed using multiplication, division and 

131 exponentiation (by a number) to give new dimensions. Addition and 

132 subtraction is defined only when the two objects are the same dimension. 

133 

134 >>> velocity = length / time 

135 >>> velocity 

136 Dimension(length/time) 

137 

138 It is possible to use a dimension system object to get the dimensionsal 

139 dependencies of a dimension, for example the dimension system used by the 

140 SI units convention can be used: 

141 

142 >>> from sympy.physics.units.systems.si import dimsys_SI 

143 >>> dimsys_SI.get_dimensional_dependencies(velocity) 

144 {Dimension(length, L): 1, Dimension(time, T): -1} 

145 >>> length + length 

146 Dimension(length) 

147 >>> l2 = length**2 

148 >>> l2 

149 Dimension(length**2) 

150 >>> dimsys_SI.get_dimensional_dependencies(l2) 

151 {Dimension(length, L): 2} 

152 

153 """ 

154 

155 _op_priority = 13.0 

156 

157 # XXX: This doesn't seem to be used anywhere... 

158 _dimensional_dependencies = {} # type: ignore 

159 

160 is_commutative = True 

161 is_number = False 

162 # make sqrt(M**2) --> M 

163 is_positive = True 

164 is_real = True 

165 

166 def __new__(cls, name, symbol=None): 

167 

168 if isinstance(name, str): 

169 name = Symbol(name) 

170 else: 

171 name = sympify(name) 

172 

173 if not isinstance(name, Expr): 

174 raise TypeError("Dimension name needs to be a valid math expression") 

175 

176 if isinstance(symbol, str): 

177 symbol = Symbol(symbol) 

178 elif symbol is not None: 

179 assert isinstance(symbol, Symbol) 

180 

181 obj = Expr.__new__(cls, name) 

182 

183 obj._name = name 

184 obj._symbol = symbol 

185 return obj 

186 

187 @property 

188 def name(self): 

189 return self._name 

190 

191 @property 

192 def symbol(self): 

193 return self._symbol 

194 

195 def __str__(self): 

196 """ 

197 Display the string representation of the dimension. 

198 """ 

199 if self.symbol is None: 

200 return "Dimension(%s)" % (self.name) 

201 else: 

202 return "Dimension(%s, %s)" % (self.name, self.symbol) 

203 

204 def __repr__(self): 

205 return self.__str__() 

206 

207 def __neg__(self): 

208 return self 

209 

210 def __add__(self, other): 

211 from sympy.physics.units.quantities import Quantity 

212 other = sympify(other) 

213 if isinstance(other, Basic): 

214 if other.has(Quantity): 

215 raise TypeError("cannot sum dimension and quantity") 

216 if isinstance(other, Dimension) and self == other: 

217 return self 

218 return super().__add__(other) 

219 return self 

220 

221 def __radd__(self, other): 

222 return self.__add__(other) 

223 

224 def __sub__(self, other): 

225 # there is no notion of ordering (or magnitude) among dimension, 

226 # subtraction is equivalent to addition when the operation is legal 

227 return self + other 

228 

229 def __rsub__(self, other): 

230 # there is no notion of ordering (or magnitude) among dimension, 

231 # subtraction is equivalent to addition when the operation is legal 

232 return self + other 

233 

234 def __pow__(self, other): 

235 return self._eval_power(other) 

236 

237 def _eval_power(self, other): 

238 other = sympify(other) 

239 return Dimension(self.name**other) 

240 

241 def __mul__(self, other): 

242 from sympy.physics.units.quantities import Quantity 

243 if isinstance(other, Basic): 

244 if other.has(Quantity): 

245 raise TypeError("cannot sum dimension and quantity") 

246 if isinstance(other, Dimension): 

247 return Dimension(self.name*other.name) 

248 if not other.free_symbols: # other.is_number cannot be used 

249 return self 

250 return super().__mul__(other) 

251 return self 

252 

253 def __rmul__(self, other): 

254 return self.__mul__(other) 

255 

256 def __truediv__(self, other): 

257 return self*Pow(other, -1) 

258 

259 def __rtruediv__(self, other): 

260 return other * pow(self, -1) 

261 

262 @classmethod 

263 def _from_dimensional_dependencies(cls, dependencies): 

264 return reduce(lambda x, y: x * y, ( 

265 d**e for d, e in dependencies.items() 

266 ), 1) 

267 

268 def has_integer_powers(self, dim_sys): 

269 """ 

270 Check if the dimension object has only integer powers. 

271 

272 All the dimension powers should be integers, but rational powers may 

273 appear in intermediate steps. This method may be used to check that the 

274 final result is well-defined. 

275 """ 

276 

277 return all(dpow.is_Integer for dpow in dim_sys.get_dimensional_dependencies(self).values()) 

278 

279 

280# Create dimensions according to the base units in MKSA. 

281# For other unit systems, they can be derived by transforming the base 

282# dimensional dependency dictionary. 

283 

284 

285class DimensionSystem(Basic, _QuantityMapper): 

286 r""" 

287 DimensionSystem represents a coherent set of dimensions. 

288 

289 The constructor takes three parameters: 

290 

291 - base dimensions; 

292 - derived dimensions: these are defined in terms of the base dimensions 

293 (for example velocity is defined from the division of length by time); 

294 - dependency of dimensions: how the derived dimensions depend 

295 on the base dimensions. 

296 

297 Optionally either the ``derived_dims`` or the ``dimensional_dependencies`` 

298 may be omitted. 

299 """ 

300 

301 def __new__(cls, base_dims, derived_dims=(), dimensional_dependencies={}): 

302 dimensional_dependencies = dict(dimensional_dependencies) 

303 

304 def parse_dim(dim): 

305 if isinstance(dim, str): 

306 dim = Dimension(Symbol(dim)) 

307 elif isinstance(dim, Dimension): 

308 pass 

309 elif isinstance(dim, Symbol): 

310 dim = Dimension(dim) 

311 else: 

312 raise TypeError("%s wrong type" % dim) 

313 return dim 

314 

315 base_dims = [parse_dim(i) for i in base_dims] 

316 derived_dims = [parse_dim(i) for i in derived_dims] 

317 

318 for dim in base_dims: 

319 if (dim in dimensional_dependencies 

320 and (len(dimensional_dependencies[dim]) != 1 or 

321 dimensional_dependencies[dim].get(dim, None) != 1)): 

322 raise IndexError("Repeated value in base dimensions") 

323 dimensional_dependencies[dim] = Dict({dim: 1}) 

324 

325 def parse_dim_name(dim): 

326 if isinstance(dim, Dimension): 

327 return dim 

328 elif isinstance(dim, str): 

329 return Dimension(Symbol(dim)) 

330 elif isinstance(dim, Symbol): 

331 return Dimension(dim) 

332 else: 

333 raise TypeError("unrecognized type %s for %s" % (type(dim), dim)) 

334 

335 for dim in dimensional_dependencies.keys(): 

336 dim = parse_dim(dim) 

337 if (dim not in derived_dims) and (dim not in base_dims): 

338 derived_dims.append(dim) 

339 

340 def parse_dict(d): 

341 return Dict({parse_dim_name(i): j for i, j in d.items()}) 

342 

343 # Make sure everything is a SymPy type: 

344 dimensional_dependencies = {parse_dim_name(i): parse_dict(j) for i, j in 

345 dimensional_dependencies.items()} 

346 

347 for dim in derived_dims: 

348 if dim in base_dims: 

349 raise ValueError("Dimension %s both in base and derived" % dim) 

350 if dim not in dimensional_dependencies: 

351 # TODO: should this raise a warning? 

352 dimensional_dependencies[dim] = Dict({dim: 1}) 

353 

354 base_dims.sort(key=default_sort_key) 

355 derived_dims.sort(key=default_sort_key) 

356 

357 base_dims = Tuple(*base_dims) 

358 derived_dims = Tuple(*derived_dims) 

359 dimensional_dependencies = Dict({i: Dict(j) for i, j in dimensional_dependencies.items()}) 

360 obj = Basic.__new__(cls, base_dims, derived_dims, dimensional_dependencies) 

361 return obj 

362 

363 @property 

364 def base_dims(self): 

365 return self.args[0] 

366 

367 @property 

368 def derived_dims(self): 

369 return self.args[1] 

370 

371 @property 

372 def dimensional_dependencies(self): 

373 return self.args[2] 

374 

375 def _get_dimensional_dependencies_for_name(self, dimension): 

376 if isinstance(dimension, str): 

377 dimension = Dimension(Symbol(dimension)) 

378 elif not isinstance(dimension, Dimension): 

379 dimension = Dimension(dimension) 

380 

381 if dimension.name.is_Symbol: 

382 # Dimensions not included in the dependencies are considered 

383 # as base dimensions: 

384 return dict(self.dimensional_dependencies.get(dimension, {dimension: 1})) 

385 

386 if dimension.name.is_number or dimension.name.is_NumberSymbol: 

387 return {} 

388 

389 get_for_name = self._get_dimensional_dependencies_for_name 

390 

391 if dimension.name.is_Mul: 

392 ret = collections.defaultdict(int) 

393 dicts = [get_for_name(i) for i in dimension.name.args] 

394 for d in dicts: 

395 for k, v in d.items(): 

396 ret[k] += v 

397 return {k: v for (k, v) in ret.items() if v != 0} 

398 

399 if dimension.name.is_Add: 

400 dicts = [get_for_name(i) for i in dimension.name.args] 

401 if all(d == dicts[0] for d in dicts[1:]): 

402 return dicts[0] 

403 raise TypeError("Only equivalent dimensions can be added or subtracted.") 

404 

405 if dimension.name.is_Pow: 

406 dim_base = get_for_name(dimension.name.base) 

407 dim_exp = get_for_name(dimension.name.exp) 

408 if dim_exp == {} or dimension.name.exp.is_Symbol: 

409 return {k: v * dimension.name.exp for (k, v) in dim_base.items()} 

410 else: 

411 raise TypeError("The exponent for the power operator must be a Symbol or dimensionless.") 

412 

413 if dimension.name.is_Function: 

414 args = (Dimension._from_dimensional_dependencies( 

415 get_for_name(arg)) for arg in dimension.name.args) 

416 result = dimension.name.func(*args) 

417 

418 dicts = [get_for_name(i) for i in dimension.name.args] 

419 

420 if isinstance(result, Dimension): 

421 return self.get_dimensional_dependencies(result) 

422 elif result.func == dimension.name.func: 

423 if isinstance(dimension.name, TrigonometricFunction): 

424 if dicts[0] in ({}, {Dimension('angle'): 1}): 

425 return {} 

426 else: 

427 raise TypeError("The input argument for the function {} must be dimensionless or have dimensions of angle.".format(dimension.func)) 

428 else: 

429 if all(item == {} for item in dicts): 

430 return {} 

431 else: 

432 raise TypeError("The input arguments for the function {} must be dimensionless.".format(dimension.func)) 

433 else: 

434 return get_for_name(result) 

435 

436 raise TypeError("Type {} not implemented for get_dimensional_dependencies".format(type(dimension.name))) 

437 

438 def get_dimensional_dependencies(self, name, mark_dimensionless=False): 

439 dimdep = self._get_dimensional_dependencies_for_name(name) 

440 if mark_dimensionless and dimdep == {}: 

441 return {Dimension(1): 1} 

442 return {k: v for k, v in dimdep.items()} 

443 

444 def equivalent_dims(self, dim1, dim2): 

445 deps1 = self.get_dimensional_dependencies(dim1) 

446 deps2 = self.get_dimensional_dependencies(dim2) 

447 return deps1 == deps2 

448 

449 def extend(self, new_base_dims, new_derived_dims=(), new_dim_deps=None): 

450 deps = dict(self.dimensional_dependencies) 

451 if new_dim_deps: 

452 deps.update(new_dim_deps) 

453 

454 new_dim_sys = DimensionSystem( 

455 tuple(self.base_dims) + tuple(new_base_dims), 

456 tuple(self.derived_dims) + tuple(new_derived_dims), 

457 deps 

458 ) 

459 new_dim_sys._quantity_dimension_map.update(self._quantity_dimension_map) 

460 new_dim_sys._quantity_scale_factors.update(self._quantity_scale_factors) 

461 return new_dim_sys 

462 

463 def is_dimensionless(self, dimension): 

464 """ 

465 Check if the dimension object really has a dimension. 

466 

467 A dimension should have at least one component with non-zero power. 

468 """ 

469 if dimension.name == 1: 

470 return True 

471 return self.get_dimensional_dependencies(dimension) == {} 

472 

473 @property 

474 def list_can_dims(self): 

475 """ 

476 Useless method, kept for compatibility with previous versions. 

477 

478 DO NOT USE. 

479 

480 List all canonical dimension names. 

481 """ 

482 dimset = set() 

483 for i in self.base_dims: 

484 dimset.update(set(self.get_dimensional_dependencies(i).keys())) 

485 return tuple(sorted(dimset, key=str)) 

486 

487 @property 

488 def inv_can_transf_matrix(self): 

489 """ 

490 Useless method, kept for compatibility with previous versions. 

491 

492 DO NOT USE. 

493 

494 Compute the inverse transformation matrix from the base to the 

495 canonical dimension basis. 

496 

497 It corresponds to the matrix where columns are the vector of base 

498 dimensions in canonical basis. 

499 

500 This matrix will almost never be used because dimensions are always 

501 defined with respect to the canonical basis, so no work has to be done 

502 to get them in this basis. Nonetheless if this matrix is not square 

503 (or not invertible) it means that we have chosen a bad basis. 

504 """ 

505 matrix = reduce(lambda x, y: x.row_join(y), 

506 [self.dim_can_vector(d) for d in self.base_dims]) 

507 return matrix 

508 

509 @property 

510 def can_transf_matrix(self): 

511 """ 

512 Useless method, kept for compatibility with previous versions. 

513 

514 DO NOT USE. 

515 

516 Return the canonical transformation matrix from the canonical to the 

517 base dimension basis. 

518 

519 It is the inverse of the matrix computed with inv_can_transf_matrix(). 

520 """ 

521 

522 #TODO: the inversion will fail if the system is inconsistent, for 

523 # example if the matrix is not a square 

524 return reduce(lambda x, y: x.row_join(y), 

525 [self.dim_can_vector(d) for d in sorted(self.base_dims, key=str)] 

526 ).inv() 

527 

528 def dim_can_vector(self, dim): 

529 """ 

530 Useless method, kept for compatibility with previous versions. 

531 

532 DO NOT USE. 

533 

534 Dimensional representation in terms of the canonical base dimensions. 

535 """ 

536 

537 vec = [] 

538 for d in self.list_can_dims: 

539 vec.append(self.get_dimensional_dependencies(dim).get(d, 0)) 

540 return Matrix(vec) 

541 

542 def dim_vector(self, dim): 

543 """ 

544 Useless method, kept for compatibility with previous versions. 

545 

546 DO NOT USE. 

547 

548 

549 Vector representation in terms of the base dimensions. 

550 """ 

551 return self.can_transf_matrix * Matrix(self.dim_can_vector(dim)) 

552 

553 def print_dim_base(self, dim): 

554 """ 

555 Give the string expression of a dimension in term of the basis symbols. 

556 """ 

557 dims = self.dim_vector(dim) 

558 symbols = [i.symbol if i.symbol is not None else i.name for i in self.base_dims] 

559 res = S.One 

560 for (s, p) in zip(symbols, dims): 

561 res *= s**p 

562 return res 

563 

564 @property 

565 def dim(self): 

566 """ 

567 Useless method, kept for compatibility with previous versions. 

568 

569 DO NOT USE. 

570 

571 Give the dimension of the system. 

572 

573 That is return the number of dimensions forming the basis. 

574 """ 

575 return len(self.base_dims) 

576 

577 @property 

578 def is_consistent(self): 

579 """ 

580 Useless method, kept for compatibility with previous versions. 

581 

582 DO NOT USE. 

583 

584 Check if the system is well defined. 

585 """ 

586 

587 # not enough or too many base dimensions compared to independent 

588 # dimensions 

589 # in vector language: the set of vectors do not form a basis 

590 return self.inv_can_transf_matrix.is_square