├── .gitignore ├── test_prefilter.py ├── pyproject.toml ├── README.md └── ipython_physics.py /.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__ 2 | -------------------------------------------------------------------------------- /test_prefilter.py: -------------------------------------------------------------------------------- 1 | from physics import Q 2 | 3 | def test_format(): 4 | a = Q('1.45 m') 5 | s = "{}".format(a) 6 | assert s == '1.45 m' 7 | s = "{:.4e}".format(a) 8 | assert s == '1.4500E+0 m' 9 | s = "{.value}".format(a) 10 | assert s == '1.45' 11 | s = "%s" % a 12 | assert s == '1.45 m' 13 | 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "ipython_physics" 7 | authors = [{name = "Georg Brandl", email = "georg@python.org"}] 8 | readme = "README.md" 9 | classifiers = ["License :: OSI Approved :: MIT License"] 10 | dynamic = ["version", "description"] 11 | 12 | [project.urls] 13 | Home = "https://github.com/birkenfeld/ipython-physics" 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ipython-physics 2 | --------------- 3 | 4 | This is an extension for IPython 0.11+ that at the moment mainly enables easy 5 | input of physical quantities (i.e. numbers with units). The implementation 6 | is adapted from Konrad Hinsen's "Scientific.Physics.PhysicalQuantities" module. 7 | 8 | If the "uncertainties" module (http://github.com/newville/uncertainties/) is 9 | installed, you can also enter values with standard errors wherever you can enter 10 | quantities, see the examples below. 11 | 12 | You can have a look at a short notebook tutorial put together by Hans Fangohr at 13 | http://www.southampton.ac.uk/~fangohr/blog/physical-quantities-numerical-value-with-units-in-python.html. 14 | 15 | Quick installation advice: 16 | 17 | Place `physics.py` in any directory on PYTHONPATH, or add its directory to 18 | `sys.path` in your IPython `ipython_config.py` file. Then you can load it 19 | either in the config file or on the command line as described here: 20 | http://ipython.org/ipython-doc/dev/config/extensions/index.html 21 | 22 | Quick usage examples: 23 | 24 | ``` 25 | In: 1 m // cm # convert between units 26 | Out: 100 cm # (syntax inspired by Mathematica) 27 | 28 | In: (1 m)/(1 s) # sugar for inline quantity input 29 | Out: 1 m/s # in arbitrary expressions 30 | 31 | In: Quantity('1 m')/Quantity('1 s') # this is the desugared form 32 | Out: 1 m/s 33 | 34 | In: // furlong/fortnight # convert units in last result 35 | Out: 6012.8848 furlong/fortnight 36 | 37 | In: alpha = 90 deg # more sugar for assignment: no 38 | # parentheses needed 39 | 40 | In: sin(alpha) # angle units work with NumPy 41 | Out: 1.0 # trigonometric functions 42 | 43 | In: m = 80 +/- 5 kg # calculating with uncertainties 44 | In: v = 130 +/- 10 m/s # (needs the "uncertainties" module) 45 | In: 0.5 * m * v**2 // kJ 46 | Out: 676 +/- 112.25445 kJ 47 | 48 | In: %tbl sqrt(?x**2 + ?y**2) // cm # quickly tabulate a formula: 49 | x = 1 m # provide some values 50 | y = 2 m 51 | Out: 223.6068 cm # and get the result 52 | x = 3 m # ... this continues as long as you 53 | y = 4 m # enter new values 54 | Out: 500 cm 55 | 56 | In: c0 # important physical constants 57 | Out: 2.9979246e+08 m/s 58 | In: setprec(4) # set the display precision 59 | In: c0 60 | Out: 2.998e+08 m/s 61 | ``` 62 | 63 | The predefined constants are: 64 | 65 | ``` 66 | pi 67 | e 68 | c0 -- vacuum speed of light 69 | mu0 -- magnetic constant 70 | eps0 -- electric constant 71 | Grav -- Newton's constant 72 | hpl -- Planck's constant 73 | hbar -- Planck's constant / 2pi 74 | e0 -- elementary charge 75 | me -- electron mass 76 | mp -- proton mass 77 | mn -- neutron mass 78 | NA -- Avogadro's number 79 | kb -- Boltzmann constant 80 | g0 -- Standard earth gravity 81 | R -- Universal gas constant 82 | alpha -- fine structure constant 83 | Ry -- Rydberg constant 84 | mu_n -- Magnetic moment of the neutron 85 | gamma -- Gyromagnetic ratio of the neutron 86 | h0 -- dimensionless Hubble parameter 87 | ``` 88 | 89 | Please let me know if anything is missing. 90 | -------------------------------------------------------------------------------- /ipython_physics.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | IPython 5+ extension for physical quantity input. 4 | See README.txt for usage examples. 5 | 6 | Author: Georg Brandl . 7 | This file has been placed in the public domain. 8 | """ 9 | 10 | __version__ = "0.1.0" 11 | 12 | import re 13 | import sys 14 | from math import pi 15 | from functools import reduce, total_ordering 16 | 17 | import numpy as np 18 | 19 | # note: deprecated since IPython 7 20 | from IPython.core.inputtransformer import InputTransformer 21 | 22 | # allow uncertain values if the "uncertainties" package is available 23 | try: 24 | from uncertainties import ufloat, Variable, AffineScalarFunc 25 | import uncertainties.umath as unp 26 | uncertain = (Variable, AffineScalarFunc) 27 | 28 | def valuetype(val_unit): 29 | v, u = val_unit 30 | if isinstance(v, uncertain): 31 | return v 32 | return ufloat((v, u)) 33 | except ImportError: 34 | uncertain = () 35 | unp = np 36 | 37 | def valuetype(val_unit): 38 | return val_unit[0] 39 | 40 | 41 | class UnitError(ValueError): 42 | pass 43 | 44 | 45 | # Adapted from ScientificPython: 46 | # Written by Konrad Hinsen 47 | # with contributions from Greg Ward 48 | # last revision: 2007-5-25 49 | 50 | class NumberDict(dict): 51 | """Dictionary storing numerical values. 52 | 53 | An instance of this class acts like an array of number with generalized 54 | (non-integer) indices. A value of zero is assumed for undefined 55 | entries. NumberDict instances support addition, and subtraction with other 56 | NumberDict instances, and multiplication and division by scalars. 57 | """ 58 | 59 | def __getitem__(self, item): 60 | try: 61 | return dict.__getitem__(self, item) 62 | except KeyError: 63 | return 0 64 | 65 | def __add__(self, other): 66 | sum_dict = NumberDict() 67 | for key in self: 68 | sum_dict[key] = self[key] 69 | for key in other: 70 | sum_dict[key] = sum_dict[key] + other[key] 71 | return sum_dict 72 | 73 | def __radd__(self, other): 74 | return NumberDict(other) + self 75 | 76 | def __sub__(self, other): 77 | sum_dict = NumberDict() 78 | for key in self: 79 | sum_dict[key] = self[key] 80 | for key in other: 81 | sum_dict[key] = sum_dict[key] - other[key] 82 | return sum_dict 83 | 84 | def __rsub__(self, other): 85 | return NumberDict(other) - self 86 | 87 | def __mul__(self, other): 88 | new = NumberDict() 89 | for key in self: 90 | new[key] = other*self[key] 91 | return new 92 | __rmul__ = __mul__ 93 | 94 | def __truediv__(self, other): 95 | new = NumberDict() 96 | for key in self: 97 | new[key] = self[key]/other 98 | return new 99 | 100 | 101 | # Type checks 102 | 103 | def isPhysicalUnit(x): 104 | return hasattr(x, 'factor') and hasattr(x, 'powers') 105 | 106 | 107 | def isPhysicalQuantity(x): 108 | return hasattr(x, 'value') and hasattr(x, 'unit') 109 | 110 | 111 | @total_ordering 112 | class PhysicalUnit(object): 113 | """Physical unit. 114 | 115 | A physical unit is defined by a name (possibly composite), a scaling factor, 116 | and the exponentials of each of the SI base units that enter into it. Units 117 | can be multiplied, divided, and raised to integer powers. 118 | """ 119 | 120 | def __init__(self, names, factor, powers, offset=0): 121 | """ 122 | @param names: a dictionary mapping each name component to its 123 | associated integer power (e.g. C{{'m': 1, 's': -1}}) 124 | for M{m/s}). As a shorthand, a string may be passed 125 | which is assigned an implicit power 1. 126 | @param factor: a scaling factor 127 | @param powers: the integer powers for each of the nine base units 128 | @param offset: an additive offset to the base unit (used only for 129 | temperatures) 130 | """ 131 | if isinstance(names, str): 132 | self.names = NumberDict() 133 | self.names[names] = 1 134 | else: 135 | self.names = names 136 | self.factor = factor 137 | self.offset = offset 138 | self.powers = powers 139 | 140 | def set_name(self, name): 141 | self.names = NumberDict() 142 | self.names[name] = 1 143 | 144 | def name(self): 145 | num = '' 146 | denom = '' 147 | for unit in self.names: 148 | power = self.names[unit] 149 | if power < 0: 150 | denom = denom + '/' + unit 151 | if power < -1: 152 | denom = denom + '**' + str(-power) 153 | elif power > 0: 154 | num = num + '*' + unit 155 | if power > 1: 156 | num = num + '**' + str(power) 157 | if len(num) == 0: 158 | num = '1' 159 | if denom == '': 160 | return '' 161 | else: 162 | num = num[1:] 163 | return num + denom 164 | 165 | @property 166 | def is_dimensionless(self): 167 | return not reduce(lambda a, b: a or b, self.powers) 168 | 169 | @property 170 | def is_angle(self): 171 | return self.powers[7] == 1 and \ 172 | reduce(lambda a, b: a + b, self.powers) == 1 173 | 174 | def __str__(self): 175 | return self.name() 176 | 177 | def __repr__(self): 178 | return '' 179 | 180 | def __eq__(self, other): 181 | if self.powers != other.powers: 182 | raise UnitError('Incompatible units') 183 | return self.factor == other.factor 184 | 185 | def __lt__(self, other): 186 | if self.powers != other.powers: 187 | raise UnitError('Incompatible units') 188 | return self.factor < other.factor 189 | 190 | def __mul__(self, other): 191 | if self.offset != 0 or (isPhysicalUnit(other) and other.offset != 0): 192 | raise UnitError('Cannot multiply units with non-zero offset') 193 | if isPhysicalUnit(other): 194 | return PhysicalUnit(self.names + other.names, 195 | self.factor * other.factor, 196 | [a+b for (a,b) in zip(self.powers, other.powers)]) 197 | else: 198 | return PhysicalUnit(self.names + {str(other): 1}, 199 | self.factor * other, self.powers, 200 | self.offset * other) 201 | 202 | __rmul__ = __mul__ 203 | 204 | def __truediv__(self, other): 205 | if self.offset != 0 or (isPhysicalUnit(other) and other.offset != 0): 206 | raise UnitError('Cannot divide units with non-zero offset') 207 | if isPhysicalUnit(other): 208 | return PhysicalUnit(self.names - other.names, 209 | self.factor / other.factor, 210 | [a-b for (a,b) in zip(self.powers, other.powers)]) 211 | else: 212 | return PhysicalUnit(self.names+{str(other): -1}, 213 | self.factor/other, self.powers) 214 | 215 | def __rtruediv__(self, other): 216 | if self.offset != 0 or (isPhysicalUnit(other) and other.offset != 0): 217 | raise UnitError('Cannot divide units with non-zero offset') 218 | if isPhysicalUnit(other): 219 | return PhysicalUnit(other.names - self.names, 220 | other.factor/self.factor, 221 | [a-b for (a,b) in zip(other.powers, self.powers)]) 222 | else: 223 | return PhysicalUnit({str(other): 1} - self.names, 224 | other / self.factor, 225 | [-x for x in self.powers]) 226 | 227 | def __pow__(self, other): 228 | if self.offset != 0: 229 | raise UnitError('Cannot exponentiate units with non-zero offset') 230 | if isinstance(other, int): 231 | return PhysicalUnit(other*self.names, pow(self.factor, other), 232 | [x*other for x in self.powers]) 233 | if isinstance(other, float): 234 | inv_exp = 1./other 235 | rounded = int(np.floor(inv_exp + 0.5)) 236 | if abs(inv_exp-rounded) < 1.e-10: 237 | if reduce(lambda a, b: a and b, 238 | map(lambda x, e=rounded: x%e == 0, self.powers)): 239 | f = pow(self.factor, other) 240 | p = [x/rounded for x in self.powers] 241 | if reduce(lambda a, b: a and b, 242 | map(lambda x, e=rounded: x%e == 0, 243 | self.names.values())): 244 | names = self.names/rounded 245 | else: 246 | names = NumberDict() 247 | if f != 1.: 248 | names[str(f)] = 1 249 | for i in range(len(p)): 250 | names[_base_names[i]] = p[i] 251 | return PhysicalUnit(names, f, p) 252 | else: 253 | raise UnitError('Illegal exponent %f' % other) 254 | raise UnitError('Only integer and inverse integer exponents allowed') 255 | 256 | def conversion_factor_to(self, other): 257 | """Return conversion factor to another unit.""" 258 | if self.powers != other.powers: 259 | raise UnitError('Incompatible units') 260 | if self.offset != other.offset and self.factor != other.factor: 261 | raise UnitError(('Unit conversion (%s to %s) cannot be expressed ' + 262 | 'as a simple multiplicative factor') % 263 | (self.name(), other.name())) 264 | return self.factor/other.factor 265 | 266 | def conversion_tuple_to(self, other): 267 | """Return conversion factor and offset to another unit.""" 268 | if self.powers != other.powers: 269 | raise UnitError('Incompatible units') 270 | 271 | # let (s1,d1) be the conversion tuple from 'self' to base units 272 | # (ie. (x+d1)*s1 converts a value x from 'self' to base units, 273 | # and (x/s1)-d1 converts x from base to 'self' units) 274 | # and (s2,d2) be the conversion tuple from 'other' to base units 275 | # then we want to compute the conversion tuple (S,D) from 276 | # 'self' to 'other' such that (x+D)*S converts x from 'self' 277 | # units to 'other' units 278 | # the formula to convert x from 'self' to 'other' units via the 279 | # base units is (by definition of the conversion tuples): 280 | # ( ((x+d1)*s1) / s2 ) - d2 281 | # = ( (x+d1) * s1/s2) - d2 282 | # = ( (x+d1) * s1/s2 ) - (d2*s2/s1) * s1/s2 283 | # = ( (x+d1) - (d1*s2/s1) ) * s1/s2 284 | # = (x + d1 - d2*s2/s1) * s1/s2 285 | # thus, D = d1 - d2*s2/s1 and S = s1/s2 286 | factor = self.factor / other.factor 287 | offset = self.offset - (other.offset * other.factor / self.factor) 288 | return (factor, offset) 289 | 290 | 291 | # Helper functions 292 | 293 | def _findUnit(unit): 294 | if isinstance(unit, str): 295 | name = unit.strip().replace('^', '**').replace('µ', 'mu').replace('°', 'deg') 296 | try: 297 | unit = eval(name, _unit_table) 298 | except NameError: 299 | raise UnitError('Invalid or unknown unit in %r' % unit) 300 | for cruft in ['__builtins__', '__args__']: 301 | try: del _unit_table[cruft] 302 | except: pass 303 | if not isPhysicalUnit(unit): 304 | raise UnitError(str(unit) + ' is not a unit') 305 | return unit 306 | 307 | 308 | def _convertValue(value, src_unit, target_unit): 309 | (factor, offset) = src_unit.conversion_tuple_to(target_unit) 310 | return (value + offset) * factor 311 | 312 | 313 | @total_ordering 314 | class PhysicalQuantity(object): 315 | """Physical quantity with units. 316 | 317 | PhysicalQuantity instances allow addition, subtraction, multiplication, and 318 | division with each other as well as multiplication, division, and 319 | exponentiation with numbers. Addition and subtraction check that the units 320 | of the two operands are compatible and return the result in the units of the 321 | first operand. A limited set of mathematical functions (from numpy) is 322 | applicable as well. 323 | """ 324 | 325 | global_precision = 8 326 | 327 | _number = re.compile(r'([+-]?[0-9]+(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?)' 328 | r'(?:\s+\+\/-\s+([+-]?[0-9]+(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?))?') 329 | 330 | def __init__(self, value, unit=None, stdev=None): 331 | """There are two constructor calling patterns: 332 | 333 | 1. PhysicalQuantity(value, unit), where value is any number and unit is 334 | a string defining the unit 335 | 336 | 2. PhysicalQuantity(value_with_unit), where value_with_unit is a string 337 | that contains both the value and the unit, i.e. '1.5 m/s'. This form 338 | is provided for more convenient interactive use. 339 | """ 340 | if unit is not None: 341 | self.value = valuetype((value, stdev or 0)) 342 | self.unit = _findUnit(unit) 343 | else: 344 | s = value.strip() 345 | match = self._number.match(s) 346 | if match is None: 347 | raise UnitError('No number found in %r' % value) 348 | self.value = valuetype((float(match.group(1)), 349 | float(match.group(2) or 0))) 350 | self.unit = _findUnit(s[match.end(0):]) 351 | 352 | def __str__(self): 353 | prec = self.global_precision 354 | unit = self.unit.name().replace('**', '^') 355 | if isinstance(self.value, uncertain): 356 | stdev = self.value.std_dev() 357 | if stdev: 358 | return '%.*g +/- %.*g %s' % (prec, self.value.nominal_value, 359 | prec, stdev, unit) 360 | return '%.*g %s' % (prec, self.value.nominal_value, unit) 361 | return '%.*g %s' % (prec, self.value, unit) 362 | 363 | def __repr__(self): 364 | return self.__str__() 365 | 366 | def _sum(self, other, sign1, sign2): 367 | if not isPhysicalQuantity(other): 368 | raise UnitError('Incompatible types') 369 | new_value = sign1 * self.value + \ 370 | sign2 * other.value * other.unit.conversion_factor_to(self.unit) 371 | return self.__class__(new_value, self.unit) 372 | 373 | def __add__(self, other): 374 | return self._sum(other, 1, 1) 375 | 376 | __radd__ = __add__ 377 | 378 | def __sub__(self, other): 379 | return self._sum(other, 1, -1) 380 | 381 | def __rsub__(self, other): 382 | return self._sum(other, -1, 1) 383 | 384 | def __eq__(self, other): 385 | diff = self._sum(other, 1, -1) 386 | return diff.value == 0 387 | 388 | def __lt__(self, other): 389 | diff = self._sum(other, 1, -1) 390 | return diff.value < 0 391 | 392 | def __mul__(self, other): 393 | if not isPhysicalQuantity(other): 394 | return self.__class__(self.value * other, self.unit) 395 | value = self.value * other.value 396 | unit = self.unit * other.unit 397 | if unit.is_dimensionless: 398 | return value * unit.factor 399 | else: 400 | return self.__class__(value, unit) 401 | 402 | __rmul__ = __mul__ 403 | 404 | def __div__(self, other): 405 | if not isPhysicalQuantity(other): 406 | return self.__class__(self.value / other, self.unit) 407 | value = self.value / other.value 408 | unit = self.unit / other.unit 409 | if unit.is_dimensionless: 410 | return value * unit.factor 411 | else: 412 | return self.__class__(value, unit) 413 | 414 | def __rdiv__(self, other): 415 | if not isPhysicalQuantity(other): 416 | return self.__class__(other / self.value, pow(self.unit, -1)) 417 | value = other.value / self.value 418 | unit = other.unit / self.unit 419 | if unit.is_dimensionless: 420 | return value * unit.factor 421 | else: 422 | return self.__class__(value, unit) 423 | 424 | __truediv__ = __div__ 425 | __rtruediv__ = __rdiv__ 426 | 427 | def __pow__(self, other): 428 | if isPhysicalQuantity(other): 429 | raise UnitError('Exponents must be dimensionless') 430 | return self.__class__(pow(self.value, other), pow(self.unit, other)) 431 | 432 | def __rpow__(self, other): 433 | raise UnitError('Exponents must be dimensionless') 434 | 435 | def __abs__(self): 436 | return self.__class__(abs(self.value), self.unit) 437 | 438 | def __pos__(self): 439 | return self 440 | 441 | def __neg__(self): 442 | return self.__class__(-self.value, self.unit) 443 | 444 | def __nonzero__(self): 445 | return self.value != 0 446 | 447 | def __format__(self, *args, **kw): 448 | return "{1:{0}} {2}".format(args[0],self.value, self.unit) 449 | 450 | def convert(self, unit): 451 | """Change the unit and adjust the value such that the combination is 452 | equivalent to the original one. The new unit must be compatible with the 453 | previous unit of the object. 454 | """ 455 | unit = _findUnit(unit) 456 | self.value = _convertValue(self.value, self.unit, unit) 457 | self.unit = unit 458 | 459 | def _round(self, x): 460 | if np.greater(x, 0.): 461 | return np.floor(x) 462 | else: 463 | return np.ceil(x) 464 | 465 | def to(self, *units): 466 | """Express the quantity in different units. If one unit is specified, a 467 | new PhysicalQuantity object is returned that expresses the quantity in 468 | that unit. If several units are specified, the return value is a tuple 469 | of PhysicalObject instances with with one element per unit such that the 470 | sum of all quantities in the tuple equals the the original quantity and 471 | all the values except for the last one are integers. This is used to 472 | convert to irregular unit systems like hour/minute/second. 473 | """ 474 | units = [_findUnit(x) for x in units] 475 | if len(units) == 1: 476 | unit = units[0] 477 | value = _convertValue(self.value, self.unit, unit) 478 | return self.__class__(value, unit) 479 | else: 480 | units.sort() 481 | result = [] 482 | value = self.value 483 | unit = self.unit 484 | for i in range(len(units)-1,-1,-1): 485 | value = value*unit.conversion_factor_to(units[i]) 486 | if i == 0: 487 | rounded = value 488 | else: 489 | rounded = self._round(value) 490 | result.append(self.__class__(rounded, units[i])) 491 | value = value - rounded 492 | unit = units[i] 493 | return tuple(result) 494 | 495 | @staticmethod 496 | def any_to(qty, unit): 497 | if not isPhysicalQuantity(qty): 498 | qty = PhysicalQuantity(qty, 'rad') 499 | return qty.to(unit) 500 | 501 | @property 502 | def base(self): 503 | """Returns the same quantity converted to base units.""" 504 | new_value = self.value * self.unit.factor 505 | num = '' 506 | denom = '' 507 | for i in range(9): 508 | unit = _base_names[i] 509 | power = self.unit.powers[i] 510 | if power < 0: 511 | denom += '/' + unit 512 | if power < -1: 513 | denom += '**' + str(-power) 514 | elif power > 0: 515 | num += '*' + unit 516 | if power > 1: 517 | num += '**' + str(power) 518 | if len(num) == 0: 519 | num = '1' 520 | if denom == '': 521 | return new_value 522 | else: 523 | num = num[1:] 524 | return self.__class__(new_value, num + denom) 525 | 526 | @property 527 | def cgs(self): 528 | """Returns the same quantity converted to cgs units.""" 529 | new_value = self.value * self.unit.factor 530 | num = '' 531 | denom = '' 532 | for i in range(9): 533 | 534 | unit_name = _base_names[i] 535 | cgs_name = _cgs_names[i] 536 | power = self.unit.powers[i] 537 | 538 | conversion_factor = Q('1 '+unit_name).to(cgs_name).value 539 | new_value *= conversion_factor**power 540 | 541 | if power < 0: 542 | denom += '/' + cgs_name 543 | if power < -1: 544 | denom += '**' + str(-power) 545 | elif power > 0: 546 | num += '*' + cgs_name 547 | if power > 1: 548 | num += '**' + str(power) 549 | if len(num) == 0: 550 | num = '1' 551 | if denom == '': 552 | return new_value 553 | else: 554 | num = num[1:] 555 | 556 | return self.__class__(new_value, num + denom) 557 | 558 | # implementations of special functions, used by numpy ufuncs 559 | 560 | def sqrt(self): 561 | return pow(self, 0.5) 562 | 563 | def sin(self): 564 | if self.unit.is_angle: 565 | return unp.sin(self.value * 566 | self.unit.conversion_factor_to(_unit_table['rad'])) 567 | else: 568 | raise UnitError('Argument of sin must be an angle') 569 | 570 | def cos(self): 571 | if self.unit.is_angle: 572 | return unp.cos(self.value * 573 | self.unit.conversion_factor_to(_unit_table['rad'])) 574 | else: 575 | raise UnitError('Argument of cos must be an angle') 576 | 577 | def tan(self): 578 | if self.unit.is_angle: 579 | return unp.tan(self.value * 580 | self.unit.conversion_factor_to(_unit_table['rad'])) 581 | else: 582 | raise UnitError('Argument of tan must be an angle') 583 | 584 | 585 | Q = PhysicalQuantity 586 | 587 | 588 | # SI unit definitions 589 | 590 | _base_names = ['m', 'kg', 's', 'A', 'K', 'mol', 'cd', 'rad', 'sr'] 591 | 592 | _base_units = [ 593 | ('m', PhysicalUnit('m', 1., [1,0,0,0,0,0,0,0,0])), 594 | ('g', PhysicalUnit('g', 0.001, [0,1,0,0,0,0,0,0,0])), 595 | ('s', PhysicalUnit('s', 1., [0,0,1,0,0,0,0,0,0])), 596 | ('A', PhysicalUnit('A', 1., [0,0,0,1,0,0,0,0,0])), 597 | ('K', PhysicalUnit('K', 1., [0,0,0,0,1,0,0,0,0])), 598 | ('mol', PhysicalUnit('mol', 1., [0,0,0,0,0,1,0,0,0])), 599 | ('cd', PhysicalUnit('cd', 1., [0,0,0,0,0,0,1,0,0])), 600 | ('rad', PhysicalUnit('rad', 1., [0,0,0,0,0,0,0,1,0])), 601 | ('sr', PhysicalUnit('sr', 1., [0,0,0,0,0,0,0,0,1])), 602 | ] 603 | 604 | _cgs_names = ['cm', 'g', 's', 'abA', 'K', 'mol', 'cd', 'rad', 'sr'] 605 | 606 | 607 | _prefixes = [ 608 | ('Y', 1.e24), ('Z', 1.e21), ('E', 1.e18), ('P', 1.e15), ('T', 1.e12), 609 | ('G', 1.e9), ('M', 1.e6), ('k', 1.e3), ('h', 1.e2), ('da', 1.e1), 610 | ('d', 1.e-1), ('c', 1.e-2), ('m', 1.e-3), ('mu', 1.e-6), ('n', 1.e-9), 611 | ('p', 1.e-12), ('f', 1.e-15), ('a', 1.e-18), ('z', 1.e-21), 612 | ('y', 1.e-24), 613 | ] 614 | 615 | _unit_table = {} 616 | 617 | for unit in _base_units: 618 | _unit_table[unit[0]] = unit[1] 619 | 620 | def _addUnit(name, unit, comment=''): 621 | if name in _unit_table: 622 | raise KeyError('Unit ' + name + ' already defined') 623 | if type(unit) == type(''): 624 | unit = eval(unit, _unit_table) 625 | for cruft in ['__builtins__', '__args__']: 626 | try: del _unit_table[cruft] 627 | except: pass 628 | unit.set_name(name) 629 | _unit_table[name] = unit 630 | 631 | def _addPrefixed(unit): 632 | _prefixed_names = [] 633 | for prefix in _prefixes: 634 | name = prefix[0] + unit 635 | _addUnit(name, prefix[1]*_unit_table[unit]) 636 | _prefixed_names.append(name) 637 | 638 | 639 | # SI derived units; these automatically get prefixes 640 | _unit_table['kg'] = PhysicalUnit('kg', 1., [0,1,0,0,0,0,0,0,0]) 641 | 642 | _addUnit('Hz', '1/s', 'Hertz') 643 | _addUnit('N', 'm*kg/s**2', 'Newton') 644 | _addUnit('Pa', 'N/m**2', 'Pascal') 645 | _addUnit('J', 'N*m', 'Joule') 646 | _addUnit('W', 'J/s', 'Watt') 647 | _addUnit('C', 's*A', 'Coulomb') 648 | _addUnit('V', 'W/A', 'Volt') 649 | _addUnit('F', 'C/V', 'Farad') 650 | _addUnit('ohm', 'V/A', 'Ohm') 651 | _addUnit('S', 'A/V', 'Siemens') 652 | _addUnit('Wb', 'V*s', 'Weber') 653 | _addUnit('T', 'Wb/m**2', 'Tesla') 654 | _addUnit('H', 'Wb/A', 'Henry') 655 | _addUnit('lm', 'cd*sr', 'Lumen') 656 | _addUnit('lx', 'lm/m**2', 'Lux') 657 | _addUnit('Bq', '1/s', 'Becquerel') 658 | _addUnit('Gy', 'J/kg', 'Gray') 659 | _addUnit('Sv', 'J/kg', 'Sievert') 660 | _addUnit('kat', 'mol/s', 'Katal') 661 | 662 | _addUnit('abA', '10*A', 'Abampere') 663 | 664 | del _unit_table['kg'] 665 | 666 | for unit in list(_unit_table): 667 | _addPrefixed(unit) 668 | 669 | # Fundamental constants, as far as needed to define other units 670 | _unit_table['pi'] = np.pi 671 | _addUnit('c0', '299792458.*m/s', 'speed of light') 672 | _addUnit('mu0', '4.e-7*pi*N/A**2', 'permeability of vacuum') 673 | _addUnit('eps0', '1/mu0/c0**2', 'permittivity of vacuum') 674 | _addUnit('hplanck', '6.62606957e-34*J*s', 'Planck constant') 675 | _addUnit('hbar', 'hplanck/(2*pi)', 'Planck constant / 2pi') 676 | _addUnit('e0', '1.602176565e-19*C', 'elementary charge') 677 | _addUnit('me', '9.10938291e-31*kg', 'electron mass') 678 | _addUnit('kb', '1.3806488e-23*J/K', 'Boltzmann constant') 679 | 680 | # Time units 681 | _addUnit('min', '60*s', 'minute') 682 | _addUnit('h', '60*min', 'hour') 683 | _addUnit('d', '24*h', 'day') 684 | _addUnit('wk', '7*d', 'week') 685 | _addUnit('yr', '365.25*d', 'year') 686 | _addPrefixed('yr') 687 | _addUnit('fortnight', '1209600*s', '14 days') 688 | 689 | # Length units 690 | _addUnit('inch', '2.54*cm', 'inch') 691 | _addUnit('ft', '12*inch', 'foot') 692 | _addUnit('yd', '3*ft', 'yard') 693 | _addUnit('mi', '5280.*ft', '(British) mile') 694 | _addUnit('nmi', '1852.*m', 'Nautical mile') 695 | _addUnit('Ang', '1.e-10*m', 'Angstrom') 696 | _addUnit('AA', '1.e-10*m', 'Angstrom') 697 | _addUnit('lyr', 'c0*yr', 'light year') 698 | _addUnit('Bohr', '4*pi*eps0*hbar**2/me/e0**2', 'Bohr radius') 699 | _addUnit('furlong', '201.168*m', 'furlongs') 700 | _addUnit('au', '149597870691*m', 'astronomical unit') 701 | 702 | # Area units 703 | _addUnit('ha', '10000*m**2', 'hectare') 704 | _addUnit('acres', 'mi**2/640', 'acre') 705 | _addUnit('b', '1.e-28*m', 'barn') 706 | 707 | # Volume units 708 | _addUnit('l', 'dm**3', 'liter') 709 | _addUnit('dl', '0.1*l', 'deci liter') 710 | _addUnit('cl', '0.01*l', 'centi liter') 711 | _addUnit('ml', '0.001*l', 'milli liter') 712 | _addUnit('mul', '0.000001*l', 'micro liter') 713 | _addUnit('tsp', '4.92892159375*ml', 'teaspoon') 714 | _addUnit('tbsp', '3*tsp', 'tablespoon') 715 | _addUnit('floz', '2*tbsp', 'fluid ounce') 716 | _addUnit('cup', '8*floz', 'cup') 717 | _addUnit('pt', '16*floz', 'pint') 718 | _addUnit('qt', '2*pt', 'quart') 719 | _addUnit('galUS', '4*qt', 'US gallon') 720 | _addUnit('galUK', '4.54609*l', 'British gallon') 721 | 722 | # Mass units 723 | _addUnit('t', '1000*kg', 'Metric ton') 724 | _addUnit('amu', '1.660538921e-27*kg', 'atomic mass units') 725 | _addUnit('Da', '1*amu', 'Dalton') 726 | _addUnit('oz', '28.349523125*g', 'ounce') 727 | _addUnit('lb', '16*oz', 'pound') 728 | _addUnit('ton', '2000*lb', 'US ton') 729 | 730 | # Force units 731 | _addUnit('dyn', '1.e-5*N', 'dyne (cgs unit)') 732 | 733 | # Energy units 734 | _addUnit('erg', '1.e-7*J', 'erg (cgs unit)') 735 | _addUnit('eV', 'e0*V', 'electron volt') 736 | _addUnit('Hartree', 'me*e0**4/16/pi**2/eps0**2/hbar**2', 'Wavenumbers/inverse cm') 737 | _addUnit('Ken', 'kb*K', 'Kelvin as energy unit') 738 | _addUnit('cal', '4.184*J', 'thermochemical calorie') 739 | _addUnit('kcal', '1000*cal', 'thermochemical kilocalorie') 740 | _addUnit('cali', '4.1868*J', 'international calorie') 741 | _addUnit('kcali', '1000*cali', 'international kilocalorie') 742 | _addUnit('Btu', '1055.05585262*J', 'British thermal unit') 743 | _addUnit('tTNT', '4.184*(10**9)*J', 'ton TNT') 744 | _addUnit('MtTNT', '4.184*(10**15)*J', 'Mega ton TNT') 745 | 746 | _addPrefixed('eV') 747 | 748 | # Electromagnetic units 749 | _addUnit('G', '1e-4*T', 'Gauss') 750 | _addUnit('Oe', '79.5774715*A/m', 'Oersted') 751 | 752 | _addPrefixed('G') 753 | _addPrefixed('Oe') 754 | 755 | # Power units 756 | _addUnit('hp', '745.7*W', 'horsepower') 757 | 758 | # Pressure units 759 | _addUnit('bar', '1.e5*Pa', 'bar (cgs unit)') 760 | _addUnit('mbar', '1.e2*Pa', 'millibar') 761 | _addUnit('kbar', '1.e8*Pa', 'kilobar') 762 | _addUnit('atm', '101325.*Pa', 'standard atmosphere') 763 | _addUnit('torr', 'atm/760', 'torr = mm of mercury') 764 | _addUnit('psi', '6894.75729317*Pa', 'pounds per square inch') 765 | 766 | # Angle units 767 | _addUnit('deg', 'pi*rad/180', 'degrees') 768 | _addUnit('arcmin', 'pi*rad/180/60', 'minutes of arc') 769 | _addUnit('arcsec', 'pi*rad/180/3600', 'seconds of arc') 770 | _unit_table['cycles'] = 2*np.pi 771 | 772 | # Temperature units -- can't use the 'eval' trick that _addUnit provides 773 | # for degC and degF because you can't add units 774 | kelvin = _findUnit('K') 775 | _addUnit('degR', '(5./9.)*K', 'degrees Rankine') 776 | _addUnit('degC', PhysicalUnit(None, 1.0, kelvin.powers, 273.15), 777 | 'degrees Celcius') 778 | _addUnit('degF', PhysicalUnit(None, 5./9., kelvin.powers, 459.67), 779 | 'degree Fahrenheit') 780 | del kelvin 781 | 782 | # Radiation-related units 783 | _addUnit('Ci', '3.7e10*Bq', 'Curie') 784 | _addUnit('rem', '0.01*Sv', 'Rem') 785 | 786 | _addPrefixed('Ci') 787 | _addPrefixed('rem') 788 | 789 | # Astronomical units 790 | _addUnit('Msol', '1.98892e30*kg', 'solar mass') 791 | _addUnit('Lsol', '3.839e26*W', 'solar luminosity') 792 | _addUnit('pc', '3.08568025e16*m') 793 | _addPrefixed('pc') 794 | 795 | # Important physical constants 796 | _constants = [ 797 | ('pi', np.pi), 798 | ('e', np.e), 799 | ('c0', Q('299792458. m/s')), 800 | ('mu0', Q('4.e-7 pi*N/A**2').base), 801 | ('eps0', Q('1 1/mu0/c0**2').base), 802 | ('Grav', Q('6.67384e-11 m**3/kg/s**2')), 803 | ('hpl', Q('6.62606957e-34 J*s')), 804 | ('hbar', Q('6.62606957e-34 J*s')/(2*pi)), 805 | ('e0', Q('1.602176565e-19 C')), 806 | ('me', Q('9.10938291e-31 kg')), 807 | ('mp', Q('1.672621777e-27 kg')), 808 | ('mn', Q('1.674927351e-27 kg')), 809 | ('NA', Q('6.02214129e23 1/mol')), 810 | ('kb', Q('1.3806488e-23 J/K')), 811 | ('g0', Q('9.80665 m/s**2')), 812 | ('R', Q('8.3144621 J/mol/K')), 813 | ('alpha', 7.2973525698e-3), 814 | ('Ry', Q('10973731.568539 1/m')), 815 | ('mu_n', Q('-0.96623647e-26 J/T')), 816 | ('gamma', Q('183.247179 MHz/T')), 817 | ('h0', 0.704), # WMAP-7 + BAO constraint 818 | ('sigmaT', Q('6.652453e-29 m**2')), 819 | ] 820 | 821 | name = r'([_a-zA-Z]\w*)' 822 | number = r'(-?[\d0-9.eE-]+)' 823 | unit = r'([a-zA-Z1°µ][a-zA-Z0-9°µ/*^-]*)' 824 | quantity = number + r'(?:\s+\+\/-\s+' + number + ')?' + r'\s+' + unit 825 | 826 | inline_unit_re = re.compile(r'\((%s)\)' % quantity) 827 | slash_conv_re = re.compile(r'^(.*?)//\s*%s$' % unit) 828 | slash_last_re = re.compile(r'^()\(/, %s\)$' % unit) 829 | trailing_conv_re = re.compile(r'\s*//\s*%s$' % unit) 830 | nice_assign_re = re.compile(r'^%s\s*=\s*(%s)$' % (name, quantity)) 831 | quantity_re = re.compile(quantity) 832 | subst_re = re.compile(r'\?' + name) 833 | 834 | def replace_inline(match): 835 | """Replace an inline unit expression, e.g. ``(1 m)``, by valid Python code 836 | using a Quantity call. 837 | """ 838 | return '(Quantity(\'' + match.group(1) + '\'))' 839 | 840 | def replace_slash(match): 841 | """Replace a double-slash unit conversion, e.g. ``c // km/s``, by valid 842 | Python code using a Quantity call. 843 | """ 844 | expr = match.group(1) 845 | unit = str(match.group(2)) # PhysicalQuantity doesn't like Unicode strings 846 | if quantity_re.match(expr): 847 | expr = 'Quantity(\'' + expr + '\')' 848 | elif not expr: 849 | expr = '_' 850 | else: 851 | expr = '(' + expr + ')' 852 | if unit == 'base': 853 | return '(' + expr + ').base' 854 | if unit == 'cgs': 855 | return '(' + expr + ').cgs' 856 | else: 857 | return 'Quantity.any_to(%s, %r)' % (expr, unit) 858 | 859 | def replace_assign(match): 860 | """Replace a pretty assignment, e.g. ``B = 1 T``, by valid Python code using 861 | a Quantity call. 862 | """ 863 | return '%s = Quantity(\'%s\')' % (match.group(1), match.group(2)) 864 | 865 | 866 | class QTransformer(object): 867 | """IPython command line transformer that recognizes and replaces unit 868 | expressions. 869 | """ 870 | # XXX: inheriting from PrefilterTransformer as documented gives TypeErrors, 871 | # but apparently is not needed after all 872 | priority = 99 873 | enabled = True 874 | 875 | def transform(self, line, continue_prompt): 876 | line = inline_unit_re.sub(replace_inline, line) 877 | if not continue_prompt: 878 | line = slash_conv_re.sub(replace_slash, line) 879 | line = nice_assign_re.sub(replace_assign, line) 880 | # lines that look like ``(/, unit)`` have been ``// unit`` but 881 | # already preprocessed by IPython, let's recognize them 882 | line = slash_last_re.sub(replace_slash, line) 883 | return line 884 | 885 | 886 | def input_transformer(lines): 887 | new_lines = [] 888 | for line in lines: 889 | line = inline_unit_re.sub(replace_inline, line) 890 | line = slash_conv_re.sub(replace_slash, line) 891 | line = nice_assign_re.sub(replace_assign, line) 892 | # lines that look like ``(/, unit)`` have been ``// unit`` but 893 | # already preprocessed by IPython, let's recognize them 894 | line = slash_last_re.sub(replace_slash, line) 895 | new_lines.append(line) 896 | return new_lines 897 | 898 | 899 | class QInputTransformer(InputTransformer): 900 | def push(self, line): 901 | return input_transformer([line])[0] 902 | 903 | def reset(self): 904 | pass 905 | 906 | 907 | def tbl_magic(shell, arg): 908 | """tbl : Evaluate for a range of parameters, given 909 | as "?name" in the expr. 910 | """ 911 | unit = None 912 | match = trailing_conv_re.search(arg) 913 | if match: 914 | arg = arg[:match.start()] 915 | unit = match.group(1) 916 | substs = sorted(set(subst_re.findall(arg))) 917 | if not substs: 918 | raise ValueError('no substitutions in expr') 919 | while 1: 920 | expr = arg 921 | for subst in substs: 922 | try: 923 | val = input('%s = ' % subst) 924 | except EOFError: 925 | sys.stdout.write('\n') 926 | return 927 | if not val: 928 | return 929 | if quantity_re.match(val): 930 | val = '(' + val + ')' 931 | expr = expr.replace('?' + subst, val) 932 | if unit: 933 | expr = 'Quantity.any_to((' + expr + '), %r)' % unit 934 | if hasattr(shell, 'shell'): # later IPython versions 935 | shell.shell.run_cell(expr, False) 936 | else: 937 | shell.run_cell(expr, False) 938 | 939 | 940 | q_transformer = QTransformer() 941 | 942 | 943 | def load_ipython_extension(ip): 944 | # set up simplified quantity input 945 | ip.user_ns['Q'] = Q 946 | ip.user_ns['Quantity'] = Q 947 | ip.prefilter_manager.register_transformer(q_transformer) 948 | 949 | # add notebook transformers 950 | try: 951 | import IPython.core.inputtransformer2 952 | except ImportError: 953 | ip.input_transformer_manager.logical_line_transforms.append( 954 | QInputTransformer()) 955 | else: 956 | # IPython 7 957 | ip.input_transformer_manager.line_transforms.append(input_transformer) 958 | 959 | # setter for custom precision 960 | ip.user_ns['setprec'] = \ 961 | lambda p: setattr(PhysicalQuantity, 'global_precision', p) 962 | # quick evaluator 963 | # TODO: this old API is no longer supported by IPython. 964 | # Need to adapt it to the @register_line_magic API. 965 | if hasattr(ip, 'define_magic'): 966 | ip.define_magic('tbl', tbl_magic) 967 | 968 | # add constants of nature 969 | for const, value in _constants: 970 | ip.user_ns[const] = value 971 | 972 | print('Unit calculation and physics extensions activated.') 973 | 974 | 975 | def unload_ipython_extension(ip): 976 | ip.prefilter_manager.unregister_transformer(q_transformer) 977 | --------------------------------------------------------------------------------