Skip to content
This repository

Physics extension #953

Closed
wants to merge 4 commits into from

4 participants

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

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 @@
  1
+# -*- coding: utf-8 -*-
  2
+"""
  3
+IPython (0.11) extension for physical quantity input.
1
Fernando Perez Owner
fperez added a note October 30, 2011

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))
  71
+
  72
+name = r'([_a-zA-Z]\w*)'
  73
+number = r'(-?[\d0-9.eE]+)'
  74
+unit = r'([a-zA-Z1][a-zA-Z0-9/*^-]*)'
  75
+quantity = number + r'\s*' + unit
  76
+
  77
+inline_unit_re = re.compile(r'\((%s)\)' % quantity)
  78
+slash_conv_re = re.compile(r'^(.*?)//\s*%s$' % unit)
  79
+trailing_conv_re = re.compile(r'\s*//\s*%s$' % unit)
  80
+nice_assign_re = re.compile(r'^%s\s*=\s*(%s)$' % (name, quantity))
  81
+quantity_re = re.compile(quantity)
  82
+subst_re = re.compile(r'\?' + name)
  83
+
  84
+def replace_inline(match):
  85
+    return 'Q(\'' + match.group(1).replace('^', '**') + '\')'
  86
+def replace_slash(match):
1
Fernando Perez Owner
fperez added a note October 30, 2011

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
Collaborator

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 October 30, 2011
IPython/extensions/physics.py
((136 lines not shown))
  136
+            try:
  137
+                val = raw_input('%s = ' % subst)
  138
+            except EOFError:
  139
+                sys.stdout.write('\n')
  140
+                return
  141
+            if not val:
  142
+                return
  143
+            if quantity_re.match(val):
  144
+                val = '(' + val + ')'
  145
+            expr = expr.replace('?' + subst, val)
  146
+        if unit:
  147
+            expr = '(' + expr + ').inUnitsOf("' + unit + '")'
  148
+        shell.run_cell(expr, False)
  149
+
  150
+
  151
+# monkey-patch a little
1
Fernando Perez Owner
fperez added a note October 30, 2011

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
Collaborator

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
Collaborator

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 October 31, 2011
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 1 changed file with 208 additions and 0 deletions. Show diff stats Hide diff stats

  1. 208  IPython/extensions/physics.py
208  IPython/extensions/physics.py
... ...
@@ -0,0 +1,208 @@
  1
+# -*- coding: utf-8 -*-
  2
+"""
  3
+IPython extension for physical quantity input.
  4
+
  5
+https://bitbucket.org/birkenfeld/ipython-physics/
  6
+
  7
+Author: Georg Brandl <georg@python.org>.
  8
+This file has been placed in the public domain.
  9
+
  10
+This is an extension for IPython 0.11 that at the moment mainly enables easy
  11
+input of physical quantities (i.e. numbers with units).  It requires the
  12
+"ScientificPython" (not SciPy) package by Konrad Hinsen.
  13
+
  14
+Quick usage examples:
  15
+
  16
+  In:  1 m // cm                        # convert between units
  17
+  Out: 100 cm                           # (syntax inspired by Mathematica)
  18
+
  19
+  In:  (1 m)/(1 s)                      # sugar for inline quantity input
  20
+  Out: 1 m/s                            # in arbitrary expressions
  21
+
  22
+  In:  Q('1 m')/Q('1 s')                # this is the desugared form
  23
+  Out: 1 m/s
  24
+
  25
+  In:  // furlong/fortnight             # convert units in last result
  26
+  Out: 6012.8848 furlong/fortnight
  27
+
  28
+  In:  alpha = 90 deg                   # more sugar for assignment: no
  29
+                                        # parentheses needed
  30
+
  31
+  In:  sin(alpha)                       # angle units work with NumPy
  32
+  Out: 1.0                              # trigonometric functions
  33
+
  34
+  In:  %tbl sqrt(?x**2 + ?y**2) // cm   # quickly tabulate a formula:
  35
+  x = 1 m                               # provide some values
  36
+  y = 2 m
  37
+  Out: 223.6068 cm                      # and get the result
  38
+  x = 3 m                               # ... this continues as long as you
  39
+  y = 4 m                               # enter new values
  40
+  Out: 500 cm
  41
+
  42
+  In:  c0                               # important physical constants
  43
+  Out: 2.9979246e+08 m/s
  44
+  In:  setprec(4)                       # set the display precision
  45
+  In:  c0
  46
+  Out: 2.998e+08 m/s
  47
+
  48
+The predefined physical constants are:
  49
+
  50
+  c0    -- vacuum speed of light
  51
+  mu0   -- magnetic constant
  52
+  eps0  -- electric constant
  53
+  Grav  -- Newton's constant
  54
+  hpl   -- Planck's constant
  55
+  hbar  -- Planck's constant / 2pi
  56
+  e0    -- elementary charge
  57
+  me    -- electron mass
  58
+  mp    -- proton mass
  59
+  mn    -- neutron mass
  60
+  NA    -- Avogadro's number
  61
+  kb    -- Boltzmann constant
  62
+
  63
+Please let me know if anything is missing.
  64
+"""
  65
+
  66
+import re
  67
+import sys
  68
+from math import pi
  69
+
  70
+from Scientific.Physics.PhysicalQuantities import PhysicalQuantity, _addUnit
  71
+
  72
+name = r'([_a-zA-Z]\w*)'
  73
+number = r'(-?[\d0-9.eE]+)'
  74
+unit = r'([a-zA-Z1][a-zA-Z0-9/*^-]*)'
  75
+quantity = number + r'\s*' + unit
  76
+
  77
+inline_unit_re = re.compile(r'\((%s)\)' % quantity)
  78
+slash_conv_re = re.compile(r'^(.*?)//\s*%s$' % unit)
  79
+trailing_conv_re = re.compile(r'\s*//\s*%s$' % unit)
  80
+nice_assign_re = re.compile(r'^%s\s*=\s*(%s)$' % (name, quantity))
  81
+quantity_re = re.compile(quantity)
  82
+subst_re = re.compile(r'\?' + name)
  83
+
  84
+def replace_inline(match):
  85
+    return 'Q(\'' + match.group(1).replace('^', '**') + '\')'
  86
+
  87
+def replace_slash(match):
  88
+    expr = match.group(1)
  89
+    unit = str(match.group(2))  # PhysicalQuantity doesn't like Unicode strings
  90
+    if quantity_re.match(expr):
  91
+        return 'Q(\'' + expr + '\').inUnitsOf(%r)' % unit
  92
+    elif not expr:
  93
+        expr = '_'
  94
+    return '(' + expr + ').inUnitsOf(%r)' % unit
  95
+
  96
+def replace_conv(match):
  97
+    return 'Q(\'' + match.group(1).replace('^', '**') + '\').inUnitsOf(%r)' % \
  98
+        str(match.group(4))
  99
+
  100
+def replace_assign(match):
  101
+    return '%s = Q(\'%s\')' % (match.group(1), match.group(2).replace('^', '**'))
  102
+
  103
+
  104
+class QTransformer(object):
  105
+    # XXX: inheriting from PrefilterTransformer as documented gives TypeErrors,
  106
+    # but apparently is not needed after all
  107
+    priority = 99
  108
+    enabled = True
  109
+    def transform(self, line, continue_prompt):
  110
+        line = inline_unit_re.sub(replace_inline, line)
  111
+        if not continue_prompt:
  112
+            line = slash_conv_re.sub(replace_slash, line)
  113
+            line = nice_assign_re.sub(replace_assign, line)
  114
+        return line
  115
+
  116
+def Q(v):
  117
+    try: return PhysicalQuantity(v)
  118
+    except NameError: raise ValueError('invalid unit in %r' % v)
  119
+
  120
+
  121
+def tbl_magic(shell, arg):
  122
+    """tbl <expr>: Evaluate <expr> for a range of parameters, given
  123
+    as "?name" in the expr.
  124
+    """
  125
+    unit = None
  126
+    match = trailing_conv_re.search(arg)
  127
+    if match:
  128
+        arg = arg[:match.start()]
  129
+        unit = match.group(1)
  130
+    substs = sorted(set(subst_re.findall(arg)))
  131
+    if not substs:
  132
+        raise ValueError('no substitutions in expr')
  133
+    while 1:
  134
+        expr = arg
  135
+        for subst in substs:
  136
+            try:
  137
+                val = raw_input('%s = ' % subst)
  138
+            except EOFError:
  139
+                sys.stdout.write('\n')
  140
+                return
  141
+            if not val:
  142
+                return
  143
+            if quantity_re.match(val):
  144
+                val = '(' + val + ')'
  145
+            expr = expr.replace('?' + subst, val)
  146
+        if unit:
  147
+            expr = '(' + expr + ').inUnitsOf("' + unit + '")'
  148
+        shell.run_cell(expr, False)
  149
+
  150
+
  151
+# monkey-patch a little
  152
+global_precision = [8]
  153
+PhysicalQuantity.__str__ = \
  154
+    lambda self: '%.*g %s' % (global_precision[0], self.value,
  155
+                              self.unit.name().replace('**', '^'))
  156
+PhysicalQuantity.__repr__ = PhysicalQuantity.__str__
  157
+PhysicalQuantity.__truediv__ = PhysicalQuantity.__div__
  158
+PhysicalQuantity.__rtruediv__ = PhysicalQuantity.__rdiv__
  159
+PhysicalQuantity.base = property(lambda self: self.inBaseUnits())
  160
+PhysicalQuantity.units = PhysicalQuantity.inUnitsOf
  161
+
  162
+q_transformer = QTransformer()
  163
+
  164
+# essential units :)
  165
+_addUnit('furlong', '201.168*m', 'furlongs')
  166
+_addUnit('fortnight', '1209600*s', '14 days')
  167
+
  168
+
  169
+hpl = Q('6.62606957e-34 J*s')
  170
+newvars = {'Q': Q,
  171
+
  172
+    # setter for custom precision
  173
+    'setprec': lambda p: global_precision.__setitem__(0, p),
  174
+    
  175
+    # Some well-used constants
  176
+    'c0': Q('299792458. m/s'),
  177
+    'mu0': Q('4.e-7 pi*N/A**2').base,
  178
+    'eps0': Q('1 1/mu0/c**2').base,
  179
+    'Grav': Q('6.67259e-11 m**3/kg/s**2'),
  180
+    'hpl': hpl,
  181
+    'hbar': hpl/(2*pi),
  182
+    'e0': Q('1.60217733e-19 C'),
  183
+    'me': Q('9.1093897e-31 kg'),
  184
+    'mp': Q('1.6726231e-27 kg'),
  185
+    'mn': Q('1.6749274e-27 kg'),
  186
+    'NA': Q('6.0221367e23 1/mol'),
  187
+    'kb': Q('1.380658e-23 J/K'),
  188
+}
  189
+
  190
+def load_ipython_extension(ip):
  191
+    # set up simplified quantity input
  192
+    ip.prefilter_manager.register_transformer(q_transformer)
  193
+    
  194
+    # quick evaluator
  195
+    ip.define_magic('tbl', tbl_magic)
  196
+
  197
+    # activate true float division
  198
+    exec ip.compile('from __future__ import division', '<input>', 'single') \
  199
+        in ip.user_ns
  200
+
  201
+    # Push our variables into the user namespace
  202
+    ip.push(newvars)
  203
+
  204
+    print 'Unit calculation and physics extensions activated.'
  205
+
  206
+def unload_ipython_extension(ip):
  207
+    ip.prefilter_manager.unregister_transformer(q_transformer)
  208
+    ip.drop_by_id(newvars)
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.