diff options
-rwxr-xr-x | lib/dfatool.py | 199 |
1 files changed, 121 insertions, 78 deletions
diff --git a/lib/dfatool.py b/lib/dfatool.py index 340a6e5..7d6d5c9 100755 --- a/lib/dfatool.py +++ b/lib/dfatool.py @@ -360,7 +360,9 @@ def _preprocess_measurement(measurement): 'triggers' : len(trigidx), 'first_trig' : trigidx[0] * 10, 'calibration' : caldata, - 'trace' : mim.analyze_states(charges, trigidx, vcalfunc) + 'trace' : mim.analyze_states(charges, trigidx, vcalfunc), + 'has_mimosa_error' : mim.is_error, + 'mimosa_errors' : mim.errors, } return processed_data @@ -585,6 +587,8 @@ class RawData: Each filename element corresponds to a measurement run. It must be a tar archive with the following contents: + Version 0: + * `setup.json`: measurement setup. Must contain the keys `state_duration` (how long each state is active, in ms), `mimosa_voltage` (voltage applied to dut, in V), and `mimosa_shunt` (shunt value, in Ohm) * `src/apps/DriverEval/DriverLog.json`: PTA traces and parameters for this benchmark. @@ -1827,33 +1831,53 @@ class MIMOSA: MIMOSA log loader for DFA traces with auto-calibration. Expects a MIMOSA log file generated via dfatool and a dfatool-generated - benchmark: There is an automatic calibration step at the start and the - trigger pin is high iff a transition is active. The resulting data - is a list of state/transition/state/transition/... measurements. + benchmark. A MIMOSA log consists of a series of measurements. Each measurement + gives the total charge (in pJ) and binary buzzer/trigger value during a 10µs interval. + + There must be a calibration run consisting of at least two seconds with disconnected DUT, + two seconds with 1 kOhm (984 Ohm), and two seconds with 100 kOhm (99013 Ohm) resistor at + the start. The first ten seconds of data are reserved for calbiration and must not contain + measurements, as trigger/buzzer signals are ignored in this time range. + + Resulting data is a list of state/transition/state/transition/... measurements. """ - def __init__(self, voltage, shunt, verbose = True): + def __init__(self, voltage: float, shunt: int, verbose = True): """ Initialize MIMOSA loader for a specific voltage and shunt setting. - arguments: - voltage -- MIMOSA voltage used for measurements - shunt -- Shunt value in Ohms - verbose -- notify about invalid data and the likes + :param voltage: MIMOSA DUT supply voltage (V) + :para mshunt: MIMOSA Shunt (Ohms) + :param verbose: print notices about invalid data on STDOUT? """ self.voltage = voltage self.shunt = shunt self.verbose = verbose self.r1 = 984 # "1k" self.r2 = 99013 # "100k" + self.is_error = False + self.errors = list() def charge_to_current_nocal(self, charge): - u"""Convert charge per 10µs to mean currents without accounting for calibration.""" + u""" + Convert charge per 10µs (in pJ) to mean currents (in µA) without accounting for calibration. + + :param charge: numpy array of charges (pJ per 10µs) as returned by `load_data` or `load_file` + + :returns: numpy array of mean currents (µA per 10µs) + """ ua_max = 1.836 / self.shunt * 1000000 ua_step = ua_max / 65535 return charge * ua_step def _load_tf(self, tf): + u""" + Load MIMOSA log data from an open `tarfile` instance. + + :param tf: `tarfile` instance + + :returns: (numpy array of charges (pJ per 10µs), numpy array of triggers (0/1 int, per 10µs)) + """ num_bytes = tf.getmember('/tmp/mimosa//mimosa_scale_1.tmp').size charges = np.ndarray(shape=(int(num_bytes / 4)), dtype=np.int32) triggers = np.ndarray(shape=(int(num_bytes / 4)), dtype=np.int8) @@ -1868,19 +1892,37 @@ class MIMOSA: return charges, triggers - def load_data(self, raw_data): - """Load a MIMOSA log archive from a raw bytestring.""" + def load_data(self, raw_data, first_is_state = True): + u""" + Load MIMOSA log data from a MIMOSA log file passed as raw byte string + + :param raw_data: MIMOSA log file, passed as raw byte string + + :returns: (numpy array of charges (pJ per 10µs), numpy array of triggers (0/1 int, per 10µs)) + """ + self.first_is_state = first_is_state with io.BytesIO(raw_data) as data_object: with tarfile.open(fileobj = data_object) as tf: return self._load_tf(tf) def load_file(self, filename): - """Load a MIMOSA log archive from a filename.""" + u""" + Load MIMOSA log data from a MIMOSA log file + + :param filename: MIMOSA log file + + :returns: (numpy array of charges (pJ per 10µs), numpy array of triggers (0/1 int, per 10µs)) + """ with tarfile.open(filename) as tf: return self._load_tf(tf) def currents_nocal(self, charges): - u"""Convert charge per 10µs to mean currents without accounting for calibration.""" + u""" + Convert charges (pJ per 10µs) to mean currents without accounting for calibration. + + :param charges: numpy array of charges (pJ per 10µs) + + :returns: numpy array of currents (mean µA per 10µs)""" ua_max = 1.836 / self.shunt * 1000000 ua_step = ua_max / 65535 return charges.astype(np.double) * ua_step @@ -1889,41 +1931,45 @@ class MIMOSA: """ Return indexes of trigger edges (both 0->1 and 1->0) in log data. - arguments: - triggers -- trigger array as returned by load_data - Ignores the first 10 seconds, which are used for calibration and may - contain bogus triggers due to DUT resets. Returns a list of int. + contain bogus triggers due to DUT resets. + + :param triggers: trigger array (int, 0/1) as returned by load_data + + :returns: list of int (trigger indices, e.g. [2000000, ...] means the first trigger appears in charges/currents interval 2000000 -> 20s after start of measurements. Keep in mind that each interval is 10µs long, not 1µs, so index values are not µs timestamps) """ trigidx = [] - prevtrig = triggers[0] + prevtrig = triggers[999999] + + # if the first trigger is high (i.e., trigger/buzzer pin is active before the benchmark starts), + # something went wrong and are unable to determine when the first + # transition starts. + if prevtrig != 0: + self.is_error = True + self.errors.append('Unable to find start of first transition (log starts with trigger == {} != 0)'.format(prevtrig)) + # the device is reset for MIMOSA calibration in the first 10s and may # send bogus interrupts -> bogus triggers for i in range(1000000, triggers.shape[0]): trig = triggers[i] if trig != prevtrig: - # Due to MIMOSA's integrate-read-reset cycle, the trigger - # appears two points (20µs) before the corresponding data + # Due to MIMOSA's integrate-read-reset cycle, the charge/current + # interval belonging to this trigger comes two intervals (20µs) later trigidx.append(i+2) prevtrig = trig return trigidx def calibration_edges(self, currents): - """ + u""" Return start/stop indexes of calibration measurements. - arguments: - currents -- uncalibrated currents as reported by MIMOSA. For best results, + :param currents: uncalibrated currents as reported by MIMOSA. For best results, it may help to use a running mean, like so: - currents = running_mean(currents_nocal(..., 10)) - - Returns six indexes: - - Disconnected start - - Disconnected stop - - R1 (1 kOhm) start - - R1 (1 kOhm) stop - - R2 (100 kOhm) start - - R2 (100 kOhm) stop + `currents = running_mean(currents_nocal(..., 10))` + + :returns: indices of calibration events in MIMOSA data: + (disconnect start, disconnect stop, R1 (1k) start, R1 (1k) stop, R2 (100k) start, R2 (100k) stop) + indices refer to charges/currents arrays, so 0 refers to the first 10µs interval, 1 to the second, and so on. """ r1idx = 0 r2idx = 0 @@ -1940,30 +1986,29 @@ class MIMOSA: def calibration_function(self, charges, cal_edges): u""" - Calculate calibration function from previously determined calibration phase. - - arguments: - charges -- raw charges from MIMOSA - cal_edges -- calibration edges as returned by calibration_edges - - returns (calibration_function, calibration_data): - calibration_function -- charge in pJ (float) -> current in uA (float). - Converts the amount of charge in a 10 µs interval to the - mean current during the same interval. - calibration_data -- dict containing the following keys: - edges -- calibration points in the log file, in µs - offset -- ... - offset2 -- ... - slope_low -- ... - slope_high -- ... - add_low -- ... - add_high -- .. - r0_err_uW -- mean error of uncalibrated data at "∞ Ohm" in µW - r0_std_uW -- standard deviation of uncalibrated data at "∞ Ohm" in µW - r1_err_uW -- mean error of uncalibrated data at 1 kOhm - r1_std_uW -- stddev at 1 kOhm - r2_err_uW -- mean error at 100 kOhm - r2_std_uW -- stddev at 100 kOhm + Calculate calibration function from previously determined calibration edges. + + :param charges: raw charges from MIMOSA + :param cal_edges: calibration edges as returned by calibration_edges + + :returns: (calibration_function, calibration_data): + calibration_function -- charge in pJ (float) -> current in uA (float). + Converts the amount of charge in a 10 µs interval to the + mean current during the same interval. + calibration_data -- dict containing the following keys: + edges -- calibration points in the log file, in µs + offset -- ... + offset2 -- ... + slope_low -- ... + slope_high -- ... + add_low -- ... + add_high -- .. + r0_err_uW -- mean error of uncalibrated data at "∞ Ohm" in µW + r0_std_uW -- standard deviation of uncalibrated data at "∞ Ohm" in µW + r1_err_uW -- mean error of uncalibrated data at 1 kOhm + r1_std_uW -- stddev at 1 kOhm + r2_err_uW -- mean error at 100 kOhm + r2_std_uW -- stddev at 100 kOhm """ dis_start, dis_end, r1_start, r1_end, r2_start, r2_end = cal_edges if dis_start < 0: @@ -2073,27 +2118,25 @@ class MIMOSA: def analyze_states(self, charges, trigidx, ua_func): u""" - Split log data into states and transitions and return mean power and duration for each element. - - arguments: - charges -- raw charges (each element describes the charge transferred during 10 µs) - trigidx -- "charges" indexes corresponding to a trigger edge - ua_func -- charge -> current function as returned by calibration_function - - returns a list of (alternating) states and transitions. - Each element is a dict containing: - - isa: 'state' oder 'transition' - - clip_rate: range(0..1) Anteil an Clipping im Energieverbrauch - - raw_mean: Mittelwert der Rohwerte - - raw_std: Standardabweichung der Rohwerte - - uW_mean: Mittelwert der (kalibrierten) Leistungsaufnahme - - uW_std: Standardabweichung der (kalibrierten) Leistungsaufnahme - - us: Dauer - - Nur falls isa == 'transition': - - timeout: Dauer des vorherigen Zustands - - uW_mean_delta_prev: Differenz zwischen uW_mean und uW_mean des vorherigen Zustands - - uW_mean_delta_next: Differenz zwischen uW_mean und uW_mean des Folgezustands + Split log data into states and transitions and return duration, energy, and mean power for each element. + + :param charges: raw charges (each element describes the charge in pJ transferred during 10 µs) + :param trigidx: "charges" indexes corresponding to a trigger edge, see `trigger_edges` + :param ua_func: charge(pJ) -> current(µA) function as returned by `calibration_function` + + :returns: list of states and transitions, starting with a state and ending with a transition. + Each element is a dict containing: + * `isa`: 'state' or 'transition' + * `clip_rate`: range(0..1) Anteil an Clipping im Energieverbrauch + * `raw_mean`: Mittelwert der Rohwerte + * `raw_std`: Standardabweichung der Rohwerte + * `uW_mean`: Mittelwert der (kalibrierten) Leistungsaufnahme + * `uW_std`: Standardabweichung der (kalibrierten) Leistungsaufnahme + * `us`: Dauer + if isa == 'transition, it also contains: + * `timeout`: Dauer des vorherigen Zustands + * `uW_mean_delta_prev`: Differenz zwischen uW_mean und uW_mean des vorherigen Zustands + * `uW_mean_delta_next`: Differenz zwischen uW_mean und uW_mean des Folgezustands """ previdx = 0 is_state = True |