Skip to content

Commit 203948f

Browse files
authored
Move apply_si_prefix into analyticsClass.py (fossasia#124)
* Move and refactor applySIPrefix * Add unit tests for apply_si_prefix
1 parent 54fa8ea commit 203948f

File tree

3 files changed

+128
-43
lines changed

3 files changed

+128
-43
lines changed

PSL/analyticsClass.py

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from __future__ import print_function
2-
import time
1+
import math
2+
from typing import Tuple
33

44
import numpy as np
55

6+
67
class analyticsClass():
78
"""
89
This class contains methods that allow mathematical analysis such as curve fitting
@@ -37,13 +38,6 @@ def __init__(self):
3738
else:
3839
self.signal = signal
3940

40-
try:
41-
from PSL.commands_proto import applySIPrefix as applySIPrefix
42-
except ImportError:
43-
self.applySIPrefix = None
44-
else:
45-
self.applySIPrefix = applySIPrefix
46-
4741
def sineFunc(self, x, a1, a2, a3, a4):
4842
return a4 + a1 * np.sin(abs(a2 * (2 * np.pi)) * x + a3)
4943

@@ -259,11 +253,11 @@ def sineFitAndDisplay(self, chan, displayObject):
259253
if fitres:
260254
amp, freq, offset, phase = fitres
261255
if amp > 0.05: fit = 'Voltage=%s\nFrequency=%s' % (
262-
self.applySIPrefix(amp, 'V'), self.applySIPrefix(freq, 'Hz'))
256+
apply_si_prefix(amp, 'V'), apply_si_prefix(freq, 'Hz'))
263257
except Exception as e:
264-
fires = None
258+
fitres = None
265259

266-
if not fitres or len(fit) == 0: fit = 'Voltage=%s\n' % (self.applySIPrefix(np.average(chan.get_yaxis()), 'V'))
260+
if not fitres or len(fit) == 0: fit = 'Voltage=%s\n' % (apply_si_prefix(np.average(chan.get_yaxis()), 'V'))
267261
displayObject.setValue(fit)
268262
if fitres:
269263
return fitres
@@ -278,7 +272,7 @@ def rmsAndDisplay(self, data, displayObject):
278272
Fits against a sine function, and writes to the object
279273
'''
280274
rms = self.RMS(data)
281-
displayObject.setValue('Voltage=%s' % (self.applySIPrefix(rms, 'V')))
275+
displayObject.setValue('Voltage=%s' % (apply_si_prefix(rms, 'V')))
282276
return rms
283277

284278
def RMS(self, data):
@@ -298,3 +292,71 @@ def butter_notch_filter(self, data, lowcut, highcut, fs, order=5):
298292
b, a = self.butter_notch(lowcut, highcut, fs, order=order)
299293
y = lfilter(b, a, data)
300294
return y
295+
296+
297+
SI_PREFIXES = {k: v for k, v in zip(range(-24, 25, 3), "yzafpnµm kMGTPEZY")}
298+
SI_PREFIXES[0] = ""
299+
300+
301+
def frexp10(x: float) -> Tuple[float, int]:
302+
"""Return the base 10 fractional coefficient and exponent of x, as pair (m, e).
303+
304+
This function is analogous to math.frexp, only for base 10 instead of base 2.
305+
If x is 0, m and e are both 0. Else 1 <= abs(m) < 10. Note that m * 10**e is not
306+
guaranteed to be exactly equal to x.
307+
308+
Parameters
309+
----------
310+
x : float
311+
Number to be split into base 10 fractional coefficient and exponent.
312+
313+
Returns
314+
-------
315+
(float, int)
316+
Base 10 fractional coefficient and exponent of x.
317+
318+
Examples
319+
--------
320+
>>> frexp10(37)
321+
(3.7, 1)
322+
"""
323+
if x == 0:
324+
coefficient, exponent = 0.0, 0
325+
else:
326+
log10x = math.log10(abs(x))
327+
exponent = int(math.copysign(math.floor(log10x), log10x))
328+
coefficient = x / 10 ** exponent
329+
330+
return coefficient, exponent
331+
332+
333+
def apply_si_prefix(value: float, unit: str, precision: int = 2) -> str:
334+
"""Scale :value: and apply appropriate SI prefix to :unit:.
335+
336+
Parameters
337+
----------
338+
value : float
339+
Number to be scaled.
340+
unit : str
341+
Base unit of :value: (without prefix).
342+
precision : int, optional
343+
:value: will be rounded to :precision: decimal places. The default value is 2.
344+
345+
Returns
346+
-------
347+
str
348+
"<scaled> <prefix><unit>", such that 1 <= <scaled> < 1000.
349+
350+
Examples
351+
-------
352+
apply_si_prefix(0.03409, "V")
353+
'34.09 mV'
354+
"""
355+
coefficient, exponent = frexp10(value)
356+
si_exponent = exponent - (exponent % 3)
357+
si_coefficient = coefficient * 10 ** (exponent % 3)
358+
359+
if abs(si_exponent) > max(SI_PREFIXES):
360+
raise ValueError("Exponent out of range of available prefixes.")
361+
362+
return f"{si_coefficient:.{precision}f} {SI_PREFIXES[si_exponent]}{unit}"

PSL/commands_proto.py

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import math, sys, time, struct
1+
import struct
2+
23

34
# allows to pack numeric values into byte strings
45
Byte = struct.Struct("B") # size 1
@@ -252,34 +253,6 @@
252253
TWELVE_BIT = Byte.pack(12)
253254

254255

255-
def applySIPrefix(value, unit='', precision=2):
256-
neg = False
257-
if value < 0.:
258-
value *= -1
259-
neg = True
260-
elif value == 0.:
261-
return '0 ' # mantissa & exponnt both 0
262-
exponent = int(math.log10(value))
263-
if exponent > 0:
264-
exponent = (exponent // 3) * 3
265-
else:
266-
exponent = (-1 * exponent + 3) // 3 * (-3)
267-
268-
value *= (10 ** (-exponent))
269-
if value >= 1000.:
270-
value /= 1000.0
271-
exponent += 3
272-
if neg:
273-
value *= -1
274-
exponent = int(exponent)
275-
PREFIXES = "yzafpnum kMGTPEZY"
276-
prefix_levels = (len(PREFIXES) - 1) // 2
277-
si_level = exponent // 3
278-
if abs(si_level) > prefix_levels:
279-
raise ValueError("Exponent out range of available prefixes.")
280-
return '%.*f %s%s' % (precision, value, PREFIXES[si_level + prefix_levels], unit)
281-
282-
283256
'''
284257
def reverse_bits(x):
285258
return int('{:08b}'.format(x)[::-1], 2)
@@ -311,5 +284,5 @@ def getLx(f1,f2,f3,Ccal):
311284
b=(f1/f2)**2
312285
c=(2*math.pi*f1)**2
313286
return (a-1)*(b-1)/(Ccal*c)
314-
287+
315288
'''

tests/test_analytics.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import unittest
2+
3+
from PSL import analyticsClass
4+
5+
6+
class TestAnalytics(unittest.TestCase):
7+
def test_frexp10(self):
8+
input_value = 43.0982
9+
expected_result = (4.30982, 1)
10+
self.assertAlmostEqual(analyticsClass.frexp10(input_value), expected_result)
11+
12+
def test_apply_si_prefix_rounding(self):
13+
input_value = {
14+
"value": 7545.230053,
15+
"unit": "J",
16+
}
17+
expected_result = "7.55 kJ"
18+
self.assertEqual(analyticsClass.apply_si_prefix(**input_value), expected_result)
19+
20+
def test_apply_si_prefix_high_precision(self):
21+
input_value = {
22+
"value": -0.000002000008,
23+
"unit": "A",
24+
"precision": 6,
25+
}
26+
expected_result = "-2.000008 µA"
27+
self.assertEqual(analyticsClass.apply_si_prefix(**input_value), expected_result)
28+
29+
def test_apply_si_prefix_low_precision(self):
30+
input_value = {
31+
"value": -1,
32+
"unit": "V",
33+
"precision": 0,
34+
}
35+
expected_result = "-1 V"
36+
self.assertEqual(analyticsClass.apply_si_prefix(**input_value), expected_result)
37+
38+
def test_apply_si_prefix_too_big(self):
39+
input_value = {
40+
"value": 1e27,
41+
"unit": "V",
42+
}
43+
self.assertRaises(ValueError, analyticsClass.apply_si_prefix, **input_value)
44+
45+
def test_apply_si_prefix_too_small(self):
46+
input_value = {
47+
"value": 1e-25,
48+
"unit": "V",
49+
}
50+
self.assertRaises(ValueError, analyticsClass.apply_si_prefix, **input_value)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy