[Python-ideas] SI scale factors in Python

Stephen J. Turnbull turnbull.stephen.fw at u.tsukuba.ac.jp
Sat Aug 27 08:04:18 EDT 2016


MRAB writes:

 > When you divide 2 values, they could have the same or different units, 
 > but must have the same colours. The result will have a combination of 
 > the units (some might also cancel out), but will have the same colours.
 > 
 >      # "amp" is a unit, "current" is a colour.
 >      # The result is a ratio of currents.
 >      6 amp current / 2 amp current == 3 current

I don't understand why a ratio would retain color.  What's the
application?

For example, in circuit analysis, if "current" is a color, I would
expect "potential" and "resistance" to be colors, too.  But from I1*R1
= V = I2*R2, we have I1/I2 = R2/R1 in a parallel circuit, so unitless
ratios of color current become unitless ratios of color resistance.
Furthermore that ratio might arise from physical phenomena such as
temperature-varying resistance, and be propagated to another physical
phenomenon such as the deflection of a meter's needle.

What might color tell us about these computations?  (Note: I'm pretty
sure that MRAB didn't choose "current" as a parallel to "resistance".
Nevertheless, the possibility of propagation of values across color
boundaries to be necessary and I don't see how color is going to be
used.)

My own take is that units specify possible operations, ie, they are
nothing more than a partial specification of a type.  Rather than
speculate on additional attributes that might useful in conjunction
with units, we should see if there are convenient ways to describe the
constraints that units produce on behavior of types.  Ie, by creating
types VoltType(UnitType), AmpereType(UnitType), and OhmType(UnitType),
and specifying the "equation" VoltType = AmpereType * OhmType on those
types, the __mul__ and __div__ operators would be modified to
implement the expected operations as a function of UnitType.  That is,
there would be a helper impose_type_expression_equivalence() that
would take the string "VoltType = AmpereType * OhmType" and manipulate
the derived type methods appropriately.

One aspect of this approach is that we can conveniently derive
concrete units such as

    V = VoltType(1)               # Some users might prefer the unit
                                  # Volt, variable V, thus "VoltType"
    mA = AmpereType(1e-3)         # SI scale prefix!
    kΩ = OhmType(1e3)            # Unicode! <wink/>

They don't serve the OP's request (he *doesn't* want type checking, he
*does* want syntax), but I prefer these anyway:

    10*V == (2*mA)*(5*kΩ)

Developers of types for circuit analysis derived from UnitType might
prefer different names that reflect the type being measured rather
than the unit, eg Current or CurrentType instead of AmpereType.

There is no problem with units like Joule (in syntax-based proposals,
it collides with the imaginary unit) and Kelvin (in syntax-based
proposals, it collides with a non-SI prefix that nevertheless is so
commonly used that both the OP and Steven d'Aprano say should be
recognized as "kilo").

Another advantage (IMHO) is that "reified" units can be treated as
equivalent to "real" (aka standard or base) units.  What I mean is
that New York is not approximately 4 Mm from Los Angeles (that would
give most people a WTF feeling), it's about 4000 km.  While I realize
programmers will be able to do that conversion, this flexibility
allows people to use units that feel natural to them.  If you want to
miles and feet, you can define them as

    ft = LengthType(12/39.37)        # base unit is meter per SI
    mi = 5280*ft

very conveniently.  Using this approach, Ken's example that Nick
rewrote to use type hinting would look like this:

    from circuit_units import uA, V, mV, kOhm, u_second, VoltType
    us = u_second                              # Use project convention.
                                               # u_second = SecondType(1e-6) 
                                               # A gratuitous style change.
    expected: VoltType                         # With so few declarations,
                                               # I prefer "predeclaration".
                                               # There is no millivolt type,
                                               # so derived units are all
                                               # consistent with this variable. 
                                               # A gratuitous style change.
    for delta in [-0.5*uA, 0*uA, 0.5*uA]:      # uA = AmpereType(1e-6)
                                               # I dislike [-0.5, 0, 0.5]*uA,
                                               # but it could be implemented.
        input = 2.75*uA + delta
        wait(1*us)                             # The "1*" is redundant.
        expected = (100*kOhm)*input            # kOhm = OhmType(1e3)
        tolerance = 2.2*mV                     # mV = VoltType(1e-3)
        fails = check_output(expected, tolerance)
        print('%s: I(in)=%rA, measured V(out)=%rV, expected V(out)=%rV, diff=%rV.' % (
            'FAIL' if fails else 'pass',
            input, get_output(), expected, get_output() - expected
        ))

Hmm: need for only *one* variable declaration.  This is very much to
my personal taste, YMMV.

The main question is whether this device could support efficient
computation.  All of these units are objects with math dunders that
have to dispatch on type (or else they need to produce "expression
Types" such as Ampere_Ohm, but I don't think type checkers would
automatically know that Volt = Ampere_Ohm).  This clearly can't
compare to the efficiency of NewType(float).  But AIUI, one
NewType(float) can't mix with another, which is not the behavior we
want here.  We could do

    VoltType = NewType('Volt', float)
    AmpereType = NewType('Ampere', float)
    WattType = NewType('Watt', float)

    def power(potential, current):
        return WattType(float(Volt)*float(Ampere))

but this is not very readable, and error-prone IMO.  It's also less
efficient than the "zero cost" that NewType promises for types like
User_ID (https://www.python.org/dev/peps/pep-0484/#newtype-helper-function).

I suppose it would be feasible (though ugly) to provide two
implementations of VoltType, one as a "real" class as I propose above,
and the other simply VoltType = float.  The former would be used for
type checking, the latter for production computations.  Perhaps such a
process could be automated in mypy?

A final advantage is that I suppose that it should be possible to
implement "color" as MRAB proposes through the Python type system.  We
don't have to define it now, but can take advantage of the benefits of
"units as types" approach immediately.

I don't know how to implement impose_type_expression_equivalence(),
yet, so this is not a proposal for the stdlib.  But the necessary
definitions by hand are straightforward, though tedious.  Individual
implementations of units can be done *now*, without change to Python,
AFAICS.  Computational efficiency is an issue, but one that doesn't
matter to educational applications, for example.

Steve


More information about the Python-ideas mailing list