-
Notifications
You must be signed in to change notification settings - Fork 26
/
units.py
275 lines (226 loc) · 7.53 KB
/
units.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# Copyright (c) 2016, German Neuroinformatics Node (G-Node)
# -*- coding: utf-8 -*-
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted under the terms of the BSD License. See
# LICENSE file in the root of the Project.
from __future__ import (absolute_import, division, print_function)
import re
try:
from collections.abc import Sequence
except ImportError:
from collections import Sequence
from ..exceptions import InvalidUnit
strings = (str, bytes)
try:
strings += (basestring,)
except NameError:
pass
# Base32hex alphabet (RFC 4648)
ID_ALPHABET = "0123456789abcdefghijklmnopqrstuv"
# Unit scaling, SI only, substitutions for micro and ohm...
PREFIXES = "(Y|Z|E|P|T|G|M|k|h|da|d|c|m|u|n|p|f|a|z|y)"
UNITS = ("(m|g|s|A|K|mol|cd|Hz|N|Pa|J|W|C|V|F|S|Wb|T|H|lm|lx|Bq|Gy|Sv|kat|l|L|"
"Ohm|%|dB|rad)")
POWER = "(\\^[+-]?[1-9]\\d*)"
PREFIX_FACTORS = {"y": 1.0e-24,
"z": 1.0e-21,
"a": 1.0e-18,
"f": 1.0e-15,
"p": 1.0e-12,
"n": 1.0e-9,
"u": 1.0e-6,
"m": 1.0e-3,
"c": 1.0e-2,
"d": 1.0e-1,
"da": 1.0e1,
"h": 1.0e2,
"k": 1.0e3,
"M": 1.0e6,
"G": 1.0e9,
"T": 1.0e12,
"P": 1.0e15,
"E": 1.0e18,
"Z": 1.0e21,
"Y": 1.0e24}
def sanitizer(unit):
"""
Sanitizes a unit string. That is, it is de-blanked, and mu and µ symbols
are changed to u for micro.
:param unit: The unit that needs to be sanitized.
:returns: the sanitized unit.
:rtype: str
"""
# micro = "\u03bc"
micro = "µ"
# mugr = "\u00b5"
mugr = "μ"
return unit.replace(" ", "").replace("mu", "u").\
replace(micro, "u").replace(mugr, "u")
def is_si(unit):
"""
Determines whether a unit is a recognized SI unit.
:param unit: The unit that needs to be checked.
:returns: True if the unit is an SI unit, false otherwise.
:rtype: bool
"""
return unit and (is_atomic(unit) or is_compound(unit))
def is_atomic(unit):
"""
Checked whether a unit string represents an atomic si unit, i.e. not a
combination.
:param unit: The unit to be checked.
:returns: True if unit is atomic, False otherwise.
:rtype: bool
"""
atomic_unit = re.compile(
"^{prefix}?{unit}{power}?$".format(prefix=PREFIXES,
unit=UNITS,
power=POWER)
)
return atomic_unit.match(unit)
def is_compound(unit):
"""
Checks whether a unit string represents a combination of SI units.
:param unit: The unit string.
:returns: True if the unit string represents a combination of SI units,
False otherwise.
:rtype: bool
"""
atomic_unit = "{prefix}?{unit}{power}?".format(
prefix=PREFIXES, unit=UNITS, power=POWER
)
compound_unit = re.compile(
"({atomic}(\\*|/))+{atomic}".format(atomic=atomic_unit)
)
return unit and compound_unit.search(unit)
def scalable(unit_a, unit_b):
"""
Checks whether units are scalable versions of the same SI unit.
Method works on two lists and compares the corresponding units in both
lists.
:param unit_a: List of unit strings.
:param unit_b: List of unit strings.
:returns: True if all corresponding units are scalable.
:rtype: bool
"""
if (isinstance(unit_a, Sequence) and isinstance(unit_b, Sequence) and
not isinstance(unit_a, strings) and
not isinstance(unit_b, strings)):
if len(unit_a) != len(unit_b):
return False
for a, b in zip(unit_a, unit_b):
if not scalable(a, b):
return False
return True
if not (is_si(unit_a) and is_si(unit_b)):
return False
a_prefix, a_unit, a_power = split(unit_a)
b_prefix, b_unit, b_power = split(unit_b)
if a_unit != b_unit or a_power != b_power:
return False
return True
def scaling(origin, destination):
"""
Returns the scaling factor to convert from one unit to another.
:param origin: The original unit string.
:param destination: The destination unit string.
:returns: The scaling factor.
:rtype: double
"""
scale = 1.0
if not scalable(origin, destination):
raise InvalidUnit(
"Origin unit and destination unit are not scalable version of the "
"same SI unit!",
"nixio.util.scaling"
)
org_prefix, org_unit, org_power = split(origin)
dest_prefix, dest_unit, dest_power = split(destination)
if org_prefix == dest_prefix and org_power == dest_power:
return scale
if not dest_prefix and org_prefix:
scale = PREFIX_FACTORS[org_prefix]
elif not org_prefix and dest_prefix:
scale = 1.0 / PREFIX_FACTORS[dest_prefix]
elif not org_prefix and not dest_prefix:
scale = PREFIX_FACTORS[org_prefix] / PREFIX_FACTORS[dest_prefix]
if org_power:
power = int(org_power)
scale **= power
return scale
def split(combined_unit):
"""
Splits a unit string into magnitude prefix, the base unit, and the power.
:param combined_unit: The unit string.
:returns: A tuple of prefix, base unit, and power.
:rtype: tuple
"""
prefix_re = "(?P<prefix>{})".format(PREFIXES)
unit_re = "(?P<unit>{})".format(UNITS)
power_re = "(?P<power>{})".format(POWER)
pup = re.compile(prefix_re + unit_re + power_re)
pu = re.compile(prefix_re + unit_re)
up = re.compile(unit_re + power_re)
# u = re.compile(unit_re)
# p = re.compile(prefix_re)
match = pup.match(combined_unit)
if match:
prefix = match.group("prefix")
unit = match.group("unit")
power = match.group("power")[1:]
return prefix, unit, power
match = up.match(combined_unit)
if match:
prefix = ""
unit = match.group("unit")
power = match.group("power")[1:]
return prefix, unit, power
match = pu.match(combined_unit)
if match:
prefix = match.group("prefix")
unit = match.group("unit")
power = ""
return prefix, unit, power
prefix = ""
unit = combined_unit
power = ""
return prefix, unit, power
def invert_power(unit):
prefix, unit, power = split(unit)
if not power:
return prefix + unit + "^-1"
if power[0] == "-":
power = power[1:]
else:
power = "^-" + power
return prefix + unit + "^" + power
def split_compound(compound_unit):
"""
Splits a compound unit (like mV/Hz) into the atomic units.
:param compound_unit: The unit string.
:returns: A tuple containing the atomic units.
:rtype: tuple
"""
opt_pup = re.compile(PREFIXES + "?" + UNITS + POWER + "?")
match = opt_pup.match(compound_unit)
sep = ""
atomic_units = []
while match and (match.end() < len(match.string)):
suffix = match.string[match.end():]
suffix = suffix.replace(" ", "")
unit = match.group(0)
if sep == "/":
atomic_units.append(invert_power(unit))
else:
atomic_units.append(unit)
sep = suffix[0]
match = opt_pup.match(suffix[1:])
unit = match.group(0)
if sep == "/":
atomic_units.append(invert_power(unit))
else:
atomic_units.append(unit)
return tuple(atomic_units)