Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Physics extension #953

Closed
wants to merge 4 commits into from

4 participants

Thomas Kluyver Fernando Perez Brian E. Granger Georg Brandl
Thomas Kluyver
Owner

This is just adding an extension, so it shouldn't cause problems elsewhere. I've just done a pull request so others can have a glance over it if they're interested and suggest improvements. Otherwise, I'll merge it in a day or so.

Thanks to @birkenfeld for rewriting this code for IPython >= 0.11.

IPython/extensions/physics.py
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+"""
+IPython (0.11) extension for physical quantity input.
Fernando Perez Owner
fperez added a note

I'd remove that version number from here, it will just get stale pretty quickly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
IPython/extensions/physics.py
((71 lines not shown))
+
+name = r'([_a-zA-Z]\w*)'
+number = r'(-?[\d0-9.eE]+)'
+unit = r'([a-zA-Z1][a-zA-Z0-9/*^-]*)'
+quantity = number + r'\s*' + unit
+
+inline_unit_re = re.compile(r'\((%s)\)' % quantity)
+slash_conv_re = re.compile(r'^(.*?)//\s*%s$' % unit)
+trailing_conv_re = re.compile(r'\s*//\s*%s$' % unit)
+nice_assign_re = re.compile(r'^%s\s*=\s*(%s)$' % (name, quantity))
+quantity_re = re.compile(quantity)
+subst_re = re.compile(r'\?' + name)
+
+def replace_inline(match):
+ return 'Q(\'' + match.group(1).replace('^', '**') + '\')'
+def replace_slash(match):
Fernando Perez Owner
fperez added a note

pep-8: two lines of whitespace between top-level definitions (function/class)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Fernando Perez
Owner

Minor suggestions, otherwise looks good.

Thomas Kluyver
Owner

Done and done, compromising a bit on PEP-8 because those functions are clearly related, so one blank line feels nicer than two.

Fernando Perez fperez commented on the diff
IPython/extensions/physics.py
((136 lines not shown))
+ try:
+ val = raw_input('%s = ' % subst)
+ except EOFError:
+ sys.stdout.write('\n')
+ return
+ if not val:
+ return
+ if quantity_re.match(val):
+ val = '(' + val + ')'
+ expr = expr.replace('?' + subst, val)
+ if unit:
+ expr = '(' + expr + ').inUnitsOf("' + unit + '")'
+ shell.run_cell(expr, False)
+
+
+# monkey-patch a little
Fernando Perez Owner
fperez added a note

I'd put all the monkeypatching at the bottom so it stands out more clearly, in a clearly labeled section such as

#------------------------------------------------------------------------------
# Monkeypatching etc...
#------------------------------------------------------------------------------

That will separate declarations from code execution more clearly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Brian E. Granger
Owner

I am -1 on this being in IPython proper. The original module was added back when IPython was sort of a dumping ground for lots of things. I feel that in order for an extension to be shipped with IPython are couple of conditions should be satisfied:

  • They need to have broad appeal that a significant fraction of IPython users would want.
  • One of the core devs need to commit to their maint.
  • No dependencies or ones that are completely common. The Scientific package does not fall into this category in my mind.

Here is an alternate model for extensions that I think we should pursue and encourage:

  • Extensions are shipped with the packages they apply to. Thus the existing sympy extension should ship with sympy. A numpy extensions should ship with numpy, etc. The extension under review here should ship with Scientific.
  • We should improve our extensions loading API to make it easier to load extensions that are in other projects.

How does that sound?

Fernando Perez
Owner
Thomas Kluyver
Owner

I agree in the general case, but I'd be inclined to include this one, because:

  • It has fairly broad appeal, particularly as a casual thing (i.e. potential users are less likely to put up with downloading and installing it separately)
  • It's not very complex, so it shouldn't be too much effort to maintain.
  • ScientificPython doesn't seem to be actively updated any more, so it's unlikely that it will ship as part of that in the near future. I don't know of any clear alternative package for what this uses. On the plus side, no updates means no API breakage from that side.

If we do want to leave it as a separate thing, Georg already has it in a bitbucket repo. But I really think it will be a lot more use if you can turn it on with one command. We should probably give a clear error message if Scientific isn't installed, though.

Brian E. Granger
Owner

If ScientificPython is not being maintained, that is even more reason to not include the extension in IPython. As Fernando said, someone should maintain it as a gist or on github/bitbucket and we can provide a link in our list of IPython extensions. I guess the bitbucket repo Georg has would work fine.

But we should open up a ticket to improve the extension loading syntax/API.

Fernando Perez
Owner

Despite the fact that I have a very soft spot for this extension, I think in this case Brian's conservative approach is wise: for one thing, we should have test coverage also for extensions, with an eye towards (in the hopefully not too distant future) having a buildbot. Having extensions that require somewhat exotic packages will make it that much harder to ensure that buildbots across platforms execute the full test suite (e.g., getting a windows buildbot with Scientific installed may or may not be trivial...).

An alternative approach would be to have an 'ipython-goodies' (perhaps with a better name) repo that we host, and that is independent from the main repo. For example on Debian/ubuntu, Emacs ships a simlar package with a ton of useful stuff on top of the core package.

Such a repo could contain more specialized extensions, extra magics and profiles, etc. Along with an easy command to do a user installation of the lot, I could see it being very useful. In the long run I would like upstream packages to adopt ipython extensions themselves, but I'm pretty sure there will always be useful, user-contributed things that upstream may (for whatever reason) decide not to package themselves. Such a repo would be a perfect area to maintain such tools and make them easily available to users; if upstream decides to adopt a tool we can always stop carrying it there, but if it doesn't adopt it our repo would provide it to users.

We'd have to think a little bit about the layout of such a repo, because ideally it should be possible for users to simply git clone it somewhere, and have IPython find things in there without requiring a separate manual re-installation every time they update via git pull. Basically we want to make it possible to have a workflow akin to what you get with setup.py develop or manual symlinking of a source tree.

@takluyver, how does this idea sound for the longer term? I think it's ultimately a more sustainable approach, and it also would be something where users would be more likely to contribute new tools that's easier than digging into the core. Basically, think of this as a version-controlled, installable repository of the best of the cookbook...

One caveat: if we go for this, I'd like to table the discussion for a bit so we can focus on closing the hard PRs and critical bugs we have left. I've been trying super hard to stay on top of new PRs, but if we keep putting out new features and ideas, we're never going to release 0.12 :)

Georg Brandl

Coming a bit late to the discussion; I won't shed too many tears if the extension isn't included in IPython itself, but the ScientificPython question can be answered quite easily: the module that's used from there is as good as standalone and has 800 lines, many of which are unit definitions; it could easily be put into the extension source. For me, this wouldn't be code duplication in the common sense because the original code isn't maintained anymore anyway. (It would also do away with the monkey-patching, and I could conceivable also make it a bit prettier.)

If the removal of the dependency is seen as a good thing, I'll do it and push to my bitbucket repo.

Thomas Kluyver
Owner

OK, I can see I'm not going to get my way on this one. I'll withdraw the PR.

I think the best way forwards is to add an %install_ext magic, that will download a file and put it in the extensions directory. Then at least it's just two commands to start using this (%install_ext, %load_ext). We can keep other extensions in gists or whatever seems best. I guess this is the sort of thing to leave until after 0.12 now, though.

@birkenfeld: I made some changes here to drop the variables from the user namespace when the extension is unloaded. The relevant method isn't in 0.11, but once 0.12 is widespread, you might want to reuse the changes. (One upside of bundling it: we know what IPython version we're targeting)

Thomas Kluyver takluyver closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
Showing with 208 additions and 0 deletions.
  1. +208 −0 IPython/extensions/physics.py
208 IPython/extensions/physics.py
View
@@ -0,0 +1,208 @@
+# -*- coding: utf-8 -*-
+"""
+IPython extension for physical quantity input.
+
+https://bitbucket.org/birkenfeld/ipython-physics/
+
+Author: Georg Brandl <georg@python.org>.
+This file has been placed in the public domain.
+
+This is an extension for IPython 0.11 that at the moment mainly enables easy
+input of physical quantities (i.e. numbers with units). It requires the
+"ScientificPython" (not SciPy) package by Konrad Hinsen.
+
+Quick usage examples:
+
+ In: 1 m // cm # convert between units
+ Out: 100 cm # (syntax inspired by Mathematica)
+
+ In: (1 m)/(1 s) # sugar for inline quantity input
+ Out: 1 m/s # in arbitrary expressions
+
+ In: Q('1 m')/Q('1 s') # this is the desugared form
+ Out: 1 m/s
+
+ In: // furlong/fortnight # convert units in last result
+ Out: 6012.8848 furlong/fortnight
+
+ In: alpha = 90 deg # more sugar for assignment: no
+ # parentheses needed
+
+ In: sin(alpha) # angle units work with NumPy
+ Out: 1.0 # trigonometric functions
+
+ In: %tbl sqrt(?x**2 + ?y**2) // cm # quickly tabulate a formula:
+ x = 1 m # provide some values
+ y = 2 m
+ Out: 223.6068 cm # and get the result
+ x = 3 m # ... this continues as long as you
+ y = 4 m # enter new values
+ Out: 500 cm
+
+ In: c0 # important physical constants
+ Out: 2.9979246e+08 m/s
+ In: setprec(4) # set the display precision
+ In: c0
+ Out: 2.998e+08 m/s
+
+The predefined physical constants are:
+
+ c0 -- vacuum speed of light
+ mu0 -- magnetic constant
+ eps0 -- electric constant
+ Grav -- Newton's constant
+ hpl -- Planck's constant
+ hbar -- Planck's constant / 2pi
+ e0 -- elementary charge
+ me -- electron mass
+ mp -- proton mass
+ mn -- neutron mass
+ NA -- Avogadro's number
+ kb -- Boltzmann constant
+
+Please let me know if anything is missing.
+"""
+
+import re
+import sys
+from math import pi
+
+from Scientific.Physics.PhysicalQuantities import PhysicalQuantity, _addUnit
+
+name = r'([_a-zA-Z]\w*)'
+number = r'(-?[\d0-9.eE]+)'
+unit = r'([a-zA-Z1][a-zA-Z0-9/*^-]*)'
+quantity = number + r'\s*' + unit
+
+inline_unit_re = re.compile(r'\((%s)\)' % quantity)
+slash_conv_re = re.compile(r'^(.*?)//\s*%s$' % unit)
+trailing_conv_re = re.compile(r'\s*//\s*%s$' % unit)
+nice_assign_re = re.compile(r'^%s\s*=\s*(%s)$' % (name, quantity))
+quantity_re = re.compile(quantity)
+subst_re = re.compile(r'\?' + name)
+
+def replace_inline(match):
+ return 'Q(\'' + match.group(1).replace('^', '**') + '\')'
+
+def replace_slash(match):
+ expr = match.group(1)
+ unit = str(match.group(2)) # PhysicalQuantity doesn't like Unicode strings
+ if quantity_re.match(expr):
+ return 'Q(\'' + expr + '\').inUnitsOf(%r)' % unit
+ elif not expr:
+ expr = '_'
+ return '(' + expr + ').inUnitsOf(%r)' % unit
+
+def replace_conv(match):
+ return 'Q(\'' + match.group(1).replace('^', '**') + '\').inUnitsOf(%r)' % \
+ str(match.group(4))
+
+def replace_assign(match):
+ return '%s = Q(\'%s\')' % (match.group(1), match.group(2).replace('^', '**'))
+
+
+class QTransformer(object):
+ # XXX: inheriting from PrefilterTransformer as documented gives TypeErrors,
+ # but apparently is not needed after all
+ priority = 99
+ enabled = True
+ def transform(self, line, continue_prompt):
+ line = inline_unit_re.sub(replace_inline, line)
+ if not continue_prompt:
+ line = slash_conv_re.sub(replace_slash, line)
+ line = nice_assign_re.sub(replace_assign, line)
+ return line
+
+def Q(v):
+ try: return PhysicalQuantity(v)
+ except NameError: raise ValueError('invalid unit in %r' % v)
+
+
+def tbl_magic(shell, arg):
+ """tbl <expr>: Evaluate <expr> for a range of parameters, given
+ as "?name" in the expr.
+ """
+ unit = None
+ match = trailing_conv_re.search(arg)
+ if match:
+ arg = arg[:match.start()]
+ unit = match.group(1)
+ substs = sorted(set(subst_re.findall(arg)))
+ if not substs:
+ raise ValueError('no substitutions in expr')
+ while 1:
+ expr = arg
+ for subst in substs:
+ try:
+ val = raw_input('%s = ' % subst)
+ except EOFError:
+ sys.stdout.write('\n')
+ return
+ if not val:
+ return
+ if quantity_re.match(val):
+ val = '(' + val + ')'
+ expr = expr.replace('?' + subst, val)
+ if unit:
+ expr = '(' + expr + ').inUnitsOf("' + unit + '")'
+ shell.run_cell(expr, False)
+
+
+# monkey-patch a little
Fernando Perez Owner
fperez added a note

I'd put all the monkeypatching at the bottom so it stands out more clearly, in a clearly labeled section such as

#------------------------------------------------------------------------------
# Monkeypatching etc...
#------------------------------------------------------------------------------

That will separate declarations from code execution more clearly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+global_precision = [8]
+PhysicalQuantity.__str__ = \
+ lambda self: '%.*g %s' % (global_precision[0], self.value,
+ self.unit.name().replace('**', '^'))
+PhysicalQuantity.__repr__ = PhysicalQuantity.__str__
+PhysicalQuantity.__truediv__ = PhysicalQuantity.__div__
+PhysicalQuantity.__rtruediv__ = PhysicalQuantity.__rdiv__
+PhysicalQuantity.base = property(lambda self: self.inBaseUnits())
+PhysicalQuantity.units = PhysicalQuantity.inUnitsOf
+
+q_transformer = QTransformer()
+
+# essential units :)
+_addUnit('furlong', '201.168*m', 'furlongs')
+_addUnit('fortnight', '1209600*s', '14 days')
+
+
+hpl = Q('6.62606957e-34 J*s')
+newvars = {'Q': Q,
+
+ # setter for custom precision
+ 'setprec': lambda p: global_precision.__setitem__(0, p),
+
+ # Some well-used constants
+ 'c0': Q('299792458. m/s'),
+ 'mu0': Q('4.e-7 pi*N/A**2').base,
+ 'eps0': Q('1 1/mu0/c**2').base,
+ 'Grav': Q('6.67259e-11 m**3/kg/s**2'),
+ 'hpl': hpl,
+ 'hbar': hpl/(2*pi),
+ 'e0': Q('1.60217733e-19 C'),
+ 'me': Q('9.1093897e-31 kg'),
+ 'mp': Q('1.6726231e-27 kg'),
+ 'mn': Q('1.6749274e-27 kg'),
+ 'NA': Q('6.0221367e23 1/mol'),
+ 'kb': Q('1.380658e-23 J/K'),
+}
+
+def load_ipython_extension(ip):
+ # set up simplified quantity input
+ ip.prefilter_manager.register_transformer(q_transformer)
+
+ # quick evaluator
+ ip.define_magic('tbl', tbl_magic)
+
+ # activate true float division
+ exec ip.compile('from __future__ import division', '<input>', 'single') \
+ in ip.user_ns
+
+ # Push our variables into the user namespace
+ ip.push(newvars)
+
+ print 'Unit calculation and physics extensions activated.'
+
+def unload_ipython_extension(ip):
+ ip.prefilter_manager.unregister_transformer(q_transformer)
+ ip.drop_by_id(newvars)
Something went wrong with that request. Please try again.