Skip to content

Commit 30c6046

Browse files
Ken KundertKen Kundert
authored andcommitted
added support for scaling with binary scale factors
1 parent 5e48f7f commit 30c6046

File tree

3 files changed

+107
-40
lines changed

3 files changed

+107
-40
lines changed

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,5 @@ def predicate(self, name, attr, *args, **kwargs):
294294
suppress_warnings = [
295295
'misc.highlighting_failure',
296296
# suppress warning about the inability to parse a literal text block
297+
'autosummary.import_cycle',
297298
]

quantiphy/quantiphy.py

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,11 +1795,12 @@ def scale(self, scale, cls=None):
17951795
are taken to the be desired units and the behavior is the same as
17961796
if a string were given, except that *cls* defaults to the given
17971797
subclass.
1798+
:type scale: real, pair, function, string, or quantity
1799+
17981800
:arg class cls:
17991801
Class to use for return value. If not given, the class of self is
18001802
used it the units do not change, in which case :class:`Quantity` is
18011803
used.
1802-
:type scale: real, pair, function, string, or quantity
18031804
18041805
:raises UnknownConversion(QuantiPhyError, KeyError):
18051806
A unit conversion was requested and there is no corresponding unit
@@ -3145,9 +3146,6 @@ def all_from_si_fmt(cls, text, **kwargs):
31453146
# Unit Conversions {{{1
31463147
# UnitConversion class {{{2
31473148
class UnitConversion(object):
3148-
_unit_conversions = {}
3149-
_known_units = set()
3150-
31513149
# description {{{3
31523150
"""
31533151
Creates a unit converter.
@@ -3286,6 +3284,11 @@ class UnitConversion(object):
32863284
32873285
"""
32883286

3287+
_unit_conversions = {}
3288+
_known_units = set()
3289+
_support_si_sf_scaling = True
3290+
_support_bin_sf_scaling = True
3291+
32893292
# constructor {{{3
32903293
def __init__(self, to_units, from_units, slope=1, intercept=0):
32913294
self.slope = slope
@@ -3465,6 +3468,26 @@ def clear_all(cls):
34653468
cls._unit_conversions = {}
34663469
cls._known_units = set()
34673470

3471+
@classmethod
3472+
def enable_scaling(cls, si_scaling=None, bin_scaling=None):
3473+
"""By default the given or desired units in a unit conversion or scaling
3474+
may include scale factors. This is true for both SI and binary scale
3475+
factors. The scale factor is provided as a prefix on the units. In
3476+
rare cases the acceptance of scale factors may create problems. You can
3477+
use this method to disable support for interpreting scale factors in
3478+
unit conversions.
3479+
3480+
si_scaling (bool):
3481+
Enables or disables support for SI scale factor scaling.
3482+
3483+
bin_scaling (bool):
3484+
Enables or disables support for binary scale factor scaling.
3485+
"""
3486+
if si_scaling is not None:
3487+
cls._support_si_sf_scaling = si_scaling
3488+
if bin_scaling is not None:
3489+
cls._support_bin_sf_scaling = bin_scaling
3490+
34683491
# fixture() {{{3
34693492
@staticmethod
34703493
def fixture(converter_func):
@@ -3569,6 +3592,8 @@ def _convert_units(cls, to_units, from_units, value):
35693592
# If you want this functionality, simply use:
35703593
# Quantity(value, from_units).scale(to_units)
35713594

3595+
orig_to_units, orig_from_units = to_units, from_units
3596+
35723597
def get_converter(to_units, from_units):
35733598
# handle unity scale factor conversions
35743599
if (
@@ -3586,45 +3611,43 @@ def get_converter(to_units, from_units):
35863611
# b. there is no scale factor on the from_units
35873612
# c. there are scale factors on both the to_ and from_units
35883613

3614+
# separate scale factor from units
3615+
def extract_sf(units):
3616+
# check for binary scale factor, all of which are two characters
3617+
if cls._support_bin_sf_scaling:
3618+
sf, unit = units[:2], units[2:]
3619+
if sf in BINARY_MAPPINGS:
3620+
return sf, unit, BINARY_MAPPINGS[sf]
3621+
3622+
# check for SI scale factor, all of which are 1 character
3623+
if cls._support_si_sf_scaling:
3624+
sf, unit = units[:1], units[1:]
3625+
if sf in MAPPINGS:
3626+
return sf, unit, float('1' + MAPPINGS[sf])
3627+
3628+
return None, units, 1
3629+
3630+
# separate scale factor from units
35893631
# handle known-unit cases for to_units
3590-
to_sf = None
3591-
to_resolved = to_units in cls._known_units # case 1
3592-
if not to_resolved:
3593-
to_prefix, to_suffix = to_units[:1], to_units[1:]
3594-
to_resolved = to_prefix in ALL_SF and to_suffix in cls._known_units
3595-
if to_resolved:
3596-
to_sf, to_units = to_prefix, to_suffix # case 2
3632+
if to_units in cls._known_units: # case 1
3633+
to_sf, to_scale = None, 1
3634+
else: # case 2 or 3
3635+
to_sf, to_units, to_scale = extract_sf(to_units)
35973636

35983637
# handle known-unit cases for from_units
3599-
from_sf = None
3600-
from_resolved = from_units in cls._known_units # case 1
3601-
if not from_resolved:
3602-
from_prefix, from_suffix = from_units[:1], from_units[1:]
3603-
from_resolved = (
3604-
from_prefix in ALL_SF and from_suffix in cls._known_units
3605-
)
3606-
if from_resolved:
3607-
from_sf, from_units = from_prefix, from_suffix # case 2
3608-
3609-
# handle same-unit cases
3610-
if not to_resolved and not from_resolved: # case 3
3611-
if to_units == from_suffix and from_prefix in ALL_SF: # case 3a
3612-
from_sf, from_units = from_prefix, from_suffix
3613-
elif from_units == to_suffix and to_prefix in ALL_SF: # case 3b
3614-
to_sf, to_units = to_prefix, to_suffix
3615-
elif from_prefix in ALL_SF and to_prefix in ALL_SF: # case 3c
3616-
to_sf, to_units = to_prefix, to_suffix
3617-
from_sf, from_units = from_prefix, from_suffix
3638+
if from_units in cls._known_units: # case 1
3639+
from_sf, from_scale = None, 1
3640+
else: # case 2 or 3
3641+
from_sf, from_units, from_scale = extract_sf(from_units)
36183642

3619-
def get_sf(sf):
3620-
if sf is None:
3621-
return 1
3622-
return float('1' + MAPPINGS[sf])
3643+
# handle unknown unit cases (to- and from- must have same units)
3644+
if to_units == from_units: # case 3
3645+
return to_units, from_units, to_scale, from_scale
36233646

3624-
if to_sf or from_sf:
3625-
return to_units, from_units, get_sf(to_sf), get_sf(from_sf)
3647+
if to_units in cls._known_units or from_units in cls._known_units: # case 2
3648+
return to_units, from_units, to_scale, from_scale
36263649

3627-
raise UnknownConversion(to_units=to_units, from_units=from_units)
3650+
raise UnknownConversion(to_units=orig_to_units, from_units=orig_from_units)
36283651

36293652
to_units, from_units, to_sf, from_sf = get_converter(to_units, from_units)
36303653

@@ -3633,7 +3656,10 @@ def get_sf(sf):
36333656
value = Quantity(value, from_units)
36343657
if to_units == from_units:
36353658
return from_sf * value / to_sf
3636-
converter = cls._unit_conversions[(to_units, from_units)]
3659+
try:
3660+
converter = cls._unit_conversions[(to_units, from_units)]
3661+
except KeyError:
3662+
raise UnknownConversion(to_units=orig_to_units, from_units=orig_from_units)
36373663
return converter(value.scale(from_sf)) / to_sf
36383664

36393665

@@ -3664,7 +3690,7 @@ def __str__(self):
36643690
UnitConversion('K', 'R °R', 5/9, 0)
36653691

36663692
# Length/Distance conversions {{{2
3667-
UnitConversion('m', 'micron', 1/1000000)
3693+
UnitConversion('m', 'micron microns', 1/1000000)
36683694
UnitConversion('m', 'Å angstrom', 1/10000000000)
36693695
UnitConversion('m', 'mi mile miles', 1609.344)
36703696
UnitConversion('m', 'ft feet', 0.3048)

tests/test_unit_conversion2.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
UnitConversion,
66
UnknownConversion,
77
)
8-
from pytest import approx, fixture, raises
8+
from pytest import approx, fixture, raises, mark
9+
parametrize = mark.parametrize
10+
911

1012
@fixture
1113
def initialize_unit_conversions():
@@ -139,3 +141,41 @@ def test_cc(initialize_unit_conversions):
139141
assert as_tuple('25 cc', scale='mL') == approx((25, 'mL'))
140142
assert as_tuple('25 mcc', scale='L', ignore_sf=True) == approx((25e-6, 'L'))
141143
assert as_tuple('25 cc', scale='L') == approx((0.025, 'L'))
144+
145+
@parametrize(
146+
"value, to_units, expected", [
147+
('10e6 s', 's', '10,000,000 s'), # same units no scale factor
148+
('10e6 s', 'ks', '10,000 ks'), # same units with to scale factor
149+
('10e3 ks', 's', '10,000,000 s'), # same units with from scale factor
150+
('10e3 ks', 'Ms', '10 Ms'), # same units with to and from scale factor
151+
('10e6 s', 'sec', '10,000,000 sec'), # equiv units no scale factor
152+
('10e6 s', 'ksec','10,000 ksec'), # equiv units with to scale factor
153+
('10e3 ksec','s', '10,000,000 s'), # equiv units with from scale factor
154+
('10e3 ksec','Ms', '10 Ms'), # equiv units with to and from scale factor
155+
('10e6 sec', 's', '10,000,000 s'), # equiv units no scale factor
156+
('10e6 sec', 'ks', '10,000 ks'), # equiv units with to scale factor
157+
('10e3 ks', 'sec', '10,000,000 sec'), # equiv units with from scale factor
158+
('10e3 ks', 'Msec','10 Msec'), # equiv units with to and from scale factor
159+
('10e6 x', 'x', '10,000,000 x'), # unknown units no scale factor
160+
('10e6 x', 'kx', '10,000 kx'), # unknown units with to scale factor
161+
('10e3 kx', 'x', '10,000,000 x'), # unknown units with from scale factor
162+
('10e3 kx', 'Mx', '10 Mx'), # unknown units with to and from scale factor
163+
('10e3 kx', 'My', 'ERR My✗kx'), # incompatible units
164+
('10e3 fuzz','buzz','10,000 buzz'), # known units that start with a sf
165+
('10e3 buzz','fuzz','10,000 fuzz'), # known units that start with a sf
166+
('10e3 fuzz','g', 'ERR g✗fuzz'), # incompatible units, one starts with sf
167+
('10e3 g', 'fuzz','ERR fuzz✗g'), # incompatible units, other starts with sf
168+
]
169+
)
170+
def test_scaling( initialize_unit_conversions, value, to_units, expected):
171+
UnitConversion('s', 'sec second seconds')
172+
UnitConversion('g', 'lb lbs', 453.59237)
173+
UnitConversion("fuzz", "buzz")
174+
175+
q = Quantity(value)
176+
try:
177+
scaled = q.scale(to_units)
178+
rendered = scaled.fixed(show_commas=True)
179+
except UnknownConversion as e:
180+
rendered = f"ERR {e.kwargs['to_units']}{e.kwargs['from_units']}"
181+
assert rendered == expected, q

0 commit comments

Comments
 (0)