sceptic-biomem/ScEpTIC/emulator/energy/system_energy_model.py
2026-07-10 10:38:57 +02:00

912 lines
34 KiB
Python

import gc
from collections import defaultdict
from ScEpTIC.emulator.custom_devices import CustomDevice
from ScEpTIC.emulator.energy import energy_utils
from ScEpTIC.emulator.energy.buffer import EnergyBufferModel
from ScEpTIC.emulator.energy.energy_source import EnergySourceModel
from ScEpTIC.emulator.energy.energy_source.synthetic_energy_source import SyntheticEnergySource
from ScEpTIC.emulator.energy.energy_harvester import EnergyHarvesterModel
from ScEpTIC.emulator.energy.energy_harvester.generic import GenericEnergyHarvester
from ScEpTIC.emulator.energy.memory.physical_memory_energy_model import PhysicalMemoryEnergyModel
from ScEpTIC.emulator.energy.mcu import MCUEnergyModel, MCUPowerState, MCUClockCycleAction
from ScEpTIC.emulator.energy.mcu_peripheral import MCUPeripheral
from ScEpTIC.emulator.energy.options import ComponentVoltageSource, PowerOffCondition
from ScEpTIC.emulator.energy.power_state_event import PowerStateEvent
from ScEpTIC.emulator.energy.state_retention import StateRetentionEnergyModel
from ScEpTIC.emulator.energy.timekeeper import TimekeeperModel
from ScEpTIC.emulator.energy.voltage_regulator import NoRegulator, VoltageRegulatorModel
from ScEpTIC.exceptions import ConfigurationException
class SystemEnergyModel:
"""
Class to model the system energy consumption
"""
def __init__(self):
# index 0 -> operation mode; index 1 -> MCU_CLOCK_ACTION
# {'total': {'total': 0.0, MCU_ACTION_1: 0.0,} , 'op_mode_1': {...}}
self.elapsed_time = defaultdict(lambda: defaultdict(float))
self.elapsed_ticks = defaultdict(lambda: defaultdict(int))
# index 0 -> component; index 1 -> operation mode; index 2 -> MCU_CLOCK_ACTION
# {'total': {'total': {'total': 0.0, MCU_ACTION1: 0.0,}, 'component1': {...}}
self.used_energy = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
self.vreg_energy_loss = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
# index 0 -> operation mode; index 1-> MCUPowerState
self.harvested_energy = defaultdict(lambda: defaultdict(float))
# standard components
self.energy_buffer = None
self.voltage_regulator = None
self.voltage_regulator_buffer = None
self.voltage_regulator_mcu = None
self.mcu = None
self.timekeeper = None
# additional components of the system
self.additional_components = {
ComponentVoltageSource.ENERGY_BUFFER: {},
ComponentVoltageSource.VOLTAGE_REGULATOR: {}
}
# custom devices - elements with their own logic and setup (e.g., the model of the D2VFS board)
self.custom_devices = []
# mcu_peripheral
self.mcu_peripherals = []
self.physical_memories = []
# state retention model
self.state_retention = None
# Power off condition
self.power_off_condition = PowerOffCondition.POWER_STATE_EVENT
self.power_off_events = []
self.power_off_voltage_threshold = 0.0
# Power failure
self.automatic_check_for_power_failures = False
self.cached_power_failure = False
self.cached_energy = 0.0
self.cached_harvested_energy = 0.0
self.power_failures = 0
# Instant values
self.instant_V = 0.0
self.instant_E = 0.0
self.instant_EH = 0.0
# Energy source model
self.energy_source = None
self.energy_harvester = None
# Collect signals
self.collect_signals = False
self.signals = []
self.custom_signals_keys = []
self.custom_signals_strings = []
self.custom_signals = {}
self._init_signals()
self.total_time = 0.0
self.lpm_precise_ticks = False
def enable_lpm_precise_ticks(self):
"""
Enables LPM simulation of t = mcu tick
"""
self.lpm_precise_ticks = True
def disable_lpm_precise_ticks(self):
"""
Revers LPM simulation of t = source_sample_time
"""
self.lpm_precise_ticks = False
def get_simulation_time(self):
"""
Returns total elapsed time in s
"""
return self.elapsed_time['total']['total']
def reset_simulation_time(self):
self.elapsed_time = defaultdict(lambda: defaultdict(float))
def get_elapsed_ticks(self):
"""
Returns total elapsed clock ticks
"""
return self.elapsed_ticks['total']['total']
def get_used_energy(self):
"""
Returns total used energy
"""
return self.used_energy['total']['total']['total']
def reset_used_energy(self):
"""
Resets the total energy usage
"""
self.used_energy['total']['total']['total'] = 0.0
def disable_stats(self):
"""
Disables statistics collection
"""
self.__update_stats, self.__disabled_update_stats = self.__disabled_update_stats, self.__update_stats
def init_custom_signals(self, data):
"""
Set the strings and keys of custom signals collected during the simulation
:param data: a list of tuples (key, string)
"""
if not isinstance(data, list):
raise ConfigurationException(f"set_custom_signal_strings() requires a list of tuples.")
del self.custom_signals_strings
del self.custom_signals_keys
self.custom_signals_strings = []
self.custom_signals_keys = []
for key, name in data:
self.custom_signals_keys.append(key)
self.custom_signals_strings.append(name)
self.custom_signals[key] = None
self._init_signals()
def set_custom_signal(self, name, val):
"""
Set the custom signals collected during the simulation
:param name: the signal name
:param val: the signal value
"""
self.custom_signals[name] = val
def get_stats(self):
"""
:return: the simulation statistics and the name of each label
"""
names = {
'time': 'Elapsed Time',
'ticks': 'Simulated Ticks',
'energy': 'Energy Consumption',
'harvested_energy': 'Harvested Energy',
'voltage_regulator_loss': 'Energy - Voltage Regulator Loss',
'power_failures': 'Simulated Power Failures',
'n_state_saves': 'Number of State Saves',
'n_restores': 'Number of State Restores',
'state_max_regs': 'Max Registers Saved',
'state_max_cells': 'Max Memory Cells Saved',
}
stats = {
'time': self.elapsed_time,
'ticks': self.elapsed_ticks,
'energy': self.used_energy,
'harvested_energy': self.harvested_energy,
'voltage_regulator_loss': self.vreg_energy_loss,
'power_failures': self.power_failures,
'n_state_saves': self.state_retention.n_state_save,
'n_restores': self.state_retention.n_state_restore,
'state_max_regs': self.state_retention.max_registers,
'state_max_cells': self.state_retention.max_memory_cells,
}
return names, stats
def get_collected_signals(self):
"""
:return: the collected signals
"""
return self.signals
def set_power_off_condition(self, power_off_condition):
"""
Sets the power off condition for the MCU
:param power_off_condition: a PowerOffCondition
"""
if not isinstance(power_off_condition, PowerOffCondition):
raise ConfigurationException(f"Wrong power off condition: {power_off_condition.__class__.__name__} is not a PowerOffCondition")
self.power_off_condition = power_off_condition
def set_power_off_voltage(self, voltage):
"""
Sets the power off voltage (considered only when power off condition = PowerOffCondition.VOLTAGE_THRESHOLD)
:param voltage: the power off voltage
"""
self.power_off_voltage_threshold = float(voltage)
def set_power_failures_automatic_check(self, val):
"""
Sets how power failures are controlled
:param val: True/False
"""
self.automatic_check_for_power_failures = val
def add_power_off_event(self, event):
"""
Adds a PowerStateEvent to the list of events that will cause the system to power off
:param event: a PowerStateEvent
"""
if not isinstance(event, PowerStateEvent):
raise ConfigurationException(f"add_power_off_event() requires a PowerStateEvent, {event.__class__.__name__} provided.")
self.power_off_events.append(event)
def attach_energy_buffer(self, energy_buffer):
"""
Attaches an energy buffer to the system model
:param energy_buffer: the energy buffer (must inherit from EnergyBufferModel)
"""
if self.energy_buffer is not None:
raise ConfigurationException("Energy buffer already atteched!")
if not isinstance(energy_buffer, EnergyBufferModel):
raise ConfigurationException("Wrong energy buffer model!")
self.energy_buffer = energy_buffer
if self.voltage_regulator is not None:
self.voltage_regulator.attach_voltage_source(self.energy_buffer)
for component in self.additional_components[ComponentVoltageSource.ENERGY_BUFFER]:
component.attach_voltage_source(self.energy_buffer)
def attach_energy_harvester(self, energy_harvester):
"""
Attaches an energy harvester to the system model
:param energy_harvester: the energy harvester
"""
if self.energy_harvester is not None:
raise ConfigurationException("Energy harvester already attached!")
if not isinstance(energy_harvester, EnergyHarvesterModel):
raise ConfigurationException("Wrong energy harvester model!")
self.energy_harvester = energy_harvester
if self.energy_source is not None:
self.energy_harvester.attach_energy_source(self.energy_source)
def attach_mcu(self, mcu):
"""
Attaches a MCU to the system model
:param mcu: the MCU (must inherit from MCUEnergyModel)
"""
if self.mcu is not None:
raise ConfigurationException("MCU already attached!")
if not isinstance(mcu, MCUEnergyModel):
raise ConfigurationException("Wrong mcu energy model!")
self.mcu = mcu
if self.voltage_regulator is not None:
self.mcu.attach_voltage_source(self.voltage_regulator)
for peripheral in self.mcu_peripherals:
peripheral.attach_mcu(self.mcu)
self.mcu.attach_peripheral(peripheral)
def attach_timekeeper(self, timekeeper):
"""
Attaches a timekeeper to the system model
:param timekeeper: the timekeeper
"""
if self.timekeeper is not None:
raise ConfigurationException("Timekeeper already attached!")
if not isinstance(timekeeper, TimekeeperModel):
raise ConfigurationException("Wrong timekeeper model!")
self.timekeeper = timekeeper
self.timekeeper.attach_system_model(self)
def attach_voltage_regulator(self, voltage_regulator):
"""
Attaches a voltage regulator to the system model
:param voltage_regulator: the voltage regulator (must inherit from VoltageRegulatorModel)
"""
if self.voltage_regulator is not None:
raise ConfigurationException("Voltage regulator already attached!")
if voltage_regulator is not None and not isinstance(voltage_regulator, VoltageRegulatorModel):
raise ConfigurationException("Wrong voltage regulator energy model!")
# Crate dummy regulator to preserve overall logic
if voltage_regulator is None:
voltage_regulator = NoRegulator()
self.voltage_regulator = voltage_regulator
if self.energy_buffer is not None:
self.voltage_regulator.attach_voltage_source(self.energy_buffer)
if self.mcu is not None:
self.mcu.attach_voltage_source(self.voltage_regulator)
for component in self.additional_components[ComponentVoltageSource.VOLTAGE_REGULATOR]:
component.attach_voltage_source(self.voltage_regulator)
def attach_component(self, name, component, target):
"""
Attaches an additional component to the energy buffer, the voltage regulator, or the MCU.
:param name: component name for stats
:param component: the component
:param target: ComponentVoltageSource enum specifying the voltage source for the device
"""
if not isinstance(target, ComponentVoltageSource):
raise ConfigurationException(f"Wrong target {target.__class__.__name__} for component {component.__class__.__name__}")
if isinstance(component, PhysicalMemoryEnergyModel):
self.physical_memories.append(component)
name = component.get_name()
self.additional_components[target][name] = component
if target == ComponentVoltageSource.ENERGY_BUFFER:
if self.energy_buffer is not None:
component.attach_voltage_source(self.energy_buffer)
elif target == ComponentVoltageSource.VOLTAGE_REGULATOR:
if self.voltage_regulator is not None:
component.attach_voltage_source(self.voltage_regulator)
else:
raise ConfigurationException(f"Wrong target '{target.__class__.__name__}' for component {name}")
if hasattr(component, 'attach_system_model') and callable(getattr(component, 'attach_system_model')):
component.attach_system_model(self)
# attach peripheral to MCU
if isinstance(component, MCUPeripheral):
self.mcu_peripherals.append(component)
if self.mcu is not None:
component.attach_mcu(self.mcu)
self.mcu.attach_peripheral(component)
def attach_custom_device(self, custom_device):
"""
Attaches a custom device to the system model and initializes it.
Note that a custom device consists in a model of an ad-hoc device, which may include multiple components and additional logic.
:param custom_device: the custom device (must inherit from CustomDevice)
"""
if not isinstance(custom_device, CustomDevice):
raise ConfigurationException(f"Wrong custom device model! {custom_device.__class__.__name__} is not a valid CustomDevice")
if custom_device in self.custom_devices:
raise ConfigurationException(f"{custom_device.__class__.__name__} already attached!")
self.custom_devices.append(custom_device)
custom_device.setup(self)
def attach_state_retention_model(self, state_retention):
"""
Attaches a state retention model to the system model
:param state_retention: the state retention model (must inherit from StateRetentionEnergyModel)
"""
if self.state_retention is not None:
raise ConfigurationException("State retention model already attached!")
if not isinstance(state_retention, StateRetentionEnergyModel):
raise ConfigurationException("Wrong state retention energy model!")
self.state_retention = state_retention
self.state_retention.attach_system_model(self)
def attach_energy_source_model(self, energy_source):
"""
Attaches an energy source model to the system model
:param energy_source: the energy source model
"""
if self.energy_source is not None:
raise ConfigurationException("Energy source model already attached!")
if energy_source is None:
energy_source = SyntheticEnergySource(5.0)
if not isinstance(energy_source, EnergySourceModel):
raise ConfigurationException("Wrong energy source model!")
self.energy_source = energy_source
if self.energy_harvester is not None:
self.energy_harvester.attach_energy_source(self.energy_source)
def get_power_state_events(self):
"""
:return: a list of the power state events occurred
"""
events = self.mcu.get_power_state_events()
for peripheral in self.mcu_peripherals:
events.extend(peripheral.get_power_state_events())
return events
def _init_signals(self, run_gc_collect=True):
"""
Inserts signals names onto the signal collection list
:param run_gc_collect: runs the garbage collector after deleting the saved signals
"""
if self.collect_signals:
del self.signals
if run_gc_collect:
gc.collect()
self.signals = []
data = [
'Time',
'Energy Source Voltage',
'Energy Buffer Voltage',
'Voltage Regulator Output Voltage',
'Energy Buffer Available Energy',
'Energy Harvested',
'Energy Consumed',
'MCU Frequency',
'MCU Power State',
'ADC Power State',
]
for device in self.custom_devices:
for signal_string in device.get_signals_strings():
data.append(signal_string)
for custom_signal in self.custom_signals_strings:
data.append(custom_signal)
self.signals.append(data)
def _collect_signals(self, t):
"""
Collect the voltage trace
:param t: elapsed time
"""
if self.collect_signals:
# Simulation signals
data = [
self.total_time,
self.energy_source.get_voltage(),
self.energy_buffer.get_voltage(),
self.voltage_regulator.get_voltage(),
self.energy_buffer.get_energy(),
self.instant_EH,
self.instant_E,
self.mcu.frequency,
str(self.mcu.mcu_power_state),
str(self.mcu.adc_power_state),
]
# Devices signals
for device in self.custom_devices:
for signal in device.get_signals():
data.append(signal)
# Custom signals
for key in self.custom_signals_keys:
data.append(self.custom_signals[key])
self.signals.append(data)
self.total_time += t
def __disabled_update_stats(self, op_mode_name, additional_op_mode_name, is_wait_cycle, mcu_clock_action, elapsed_time, n_ticks, energy_draws, e_harvested):
"""
Disabled method
"""
return
def __update_stats(self, op_mode_name, additional_op_mode_name, is_wait_cycle, mcu_clock_action, elapsed_time, n_ticks, energy_draws, e_harvested):
"""
Updates internal statistics
:param op_mode_name: current operation identifier (string) for metrics identification
:param additional_op_mode_name: additional operation identifier (string) for metrics identification
:param is_wait_cycle: if current cycle is a wait cycle
:param mcu_clock_action: Clock cycle action of the MCU
:param elapsed_time: time elapsed in the simulation
:param n_ticks: number of ticks simulated
:param energy_draws: energy drawn by the entire circuit calculated with calculate_energy_draws()
:param e_harvested: energy harvested from the environment
"""
wait_cycle_mask = "_WAIT" if is_wait_cycle else ""
op_mode_name = "{}{}".format(str(op_mode_name), wait_cycle_mask)
mcu_clock_action = "{}{}".format(str(mcu_clock_action), wait_cycle_mask)
mcu_state = "{}{}".format(str(self.mcu.get_mcu_state()), wait_cycle_mask)
# Unpack dict
energy_drawn = energy_draws['energy_drawn']
vreg_energy_loss = energy_draws['vreg_energy_loss']
energy_components = energy_draws['energy_components']
energy_loss_components = energy_draws['energy_loss_components']
# Operation mode names
op_modes = ['total', op_mode_name]
if additional_op_mode_name is not None:
additional_op_mode_name = "{}{}".format(str(additional_op_mode_name), wait_cycle_mask)
op_modes.append(additional_op_mode_name)
for op_mode in op_modes:
for mcu_action in ['total', mcu_clock_action]:
self.elapsed_time[op_mode][mcu_action] += elapsed_time
self.elapsed_ticks[op_mode][mcu_action] += n_ticks
self.used_energy['total'][op_mode][mcu_action] += energy_drawn
self.vreg_energy_loss['total'][op_mode][mcu_action] += vreg_energy_loss
for component in energy_components.keys():
energy = energy_components[component]
vreg_loss = energy_loss_components[component]
self.used_energy[component][op_mode][mcu_action] += energy
self.vreg_energy_loss[component][op_mode][mcu_action] += vreg_loss
for mcu_st in ['total', mcu_state]:
self.harvested_energy[op_mode][mcu_st] += e_harvested
def get_elapsed_time(self):
"""
:return: the time elapsed during a simulation tick / step
"""
mcu_state = self.mcu.get_mcu_state()
# If energy source is set and MCU in LPM / OFF -> recharging energy buffer from harvested energy
# -> fast forward to next sampled interval
if self.energy_source is not None and (mcu_state == MCUPowerState.OFF or (mcu_state == MCUPowerState.LPM and not self.lpm_precise_ticks)):
return self.energy_source.get_current_sample_remaining_time()
return self.mcu.get_mcu_cycle_time()
def calculate_energy_draws(self, mcu_clock_action, elapsed_time):
"""
Calculates the energy draw of the simulated circuit and returns it
:param mcu_clock_action: Clock cycle action of the MCU
:param elapsed_time: the time elapsed
:return: a dictionary with all the energy draws types
"""
energy_components = {}
energy_loss_components = {}
# Energy consumed from the output of the voltage regulator
e = self.mcu.get_drained_energy(mcu_clock_action, elapsed_time)
energy_components['mcu'] = e
energy_from_regulator = e
energy_loss_components['mcu'] = 0.0
# Energy consumed by devices attached to voltage regulator
for name, component in self.additional_components[ComponentVoltageSource.VOLTAGE_REGULATOR].items():
e = component.get_drained_energy(elapsed_time)
energy_components[name] = e
energy_from_regulator += e
energy_loss_components[name] = 0.0
# energy drawn by voltage regulator from energy buffer
energy_drawn = self.voltage_regulator.get_drained_energy(energy_from_regulator, elapsed_time)
# energy loss due to voltage regulator (in)efficiency
vreg_energy_loss = energy_drawn - energy_from_regulator
if energy_from_regulator > 0.0:
# Calculate the energy lost due to voltage regulator for each connected component
for name in energy_components.keys():
e_ratio = energy_components[name] / energy_from_regulator
energy_loss_components[name] = vreg_energy_loss * e_ratio
# Energy consumed by devices attached to energy buffer
for name, component in self.additional_components[ComponentVoltageSource.ENERGY_BUFFER].items():
e = component.get_drained_energy(elapsed_time)
energy_components[name] = e
energy_drawn += e
energy_loss_components[name] = 0.0
retval = {
'energy_drawn': energy_drawn,
'vreg_energy_loss': vreg_energy_loss,
'energy_from_regulator': energy_from_regulator,
'energy_components': energy_components,
'energy_loss_components': energy_loss_components,
}
return retval
def run_step(self, mcu_clock_action, op_mode_name, is_wait_cycle=False, from_custom_device=None, additional_op_mode_name=None):
"""
Runs a simulation step
:param mcu_clock_action: Clock cycle action of the MCU
:param op_mode_name: current operation identifier (string) for metrics identification
:param is_wait_cycle: specifies if the execution happens due to a wait cycle
:param from_custom_device: name of the cust device that is triggering the step execution
:param additional_op_mode_name: additional op_mode_name for metrics identification
:return: the number of clock cycles executed by the MCU
"""
# Automatic check enabled -> return 0 ticks if power failure occurred
if self.automatic_check_for_power_failures:
# Check for a possible power failure (do not reset cache)
self.cached_power_failure = self.power_failure_occurring(False)
if self.cached_power_failure:
return 0
devices_ticks = self.run_custom_devices_logic(from_custom_device)
n_ticks = 1
# Set ticks to 0 if current operation is a NOP
if mcu_clock_action == MCUClockCycleAction.LPM_NOP or mcu_clock_action == MCUClockCycleAction.NOP_OFF_RECHARGE:
n_ticks = 0
elapsed_time = self.get_elapsed_time()
self._collect_signals(elapsed_time)
energy_draws = self.calculate_energy_draws(mcu_clock_action, elapsed_time)
energy_drawn = energy_draws['energy_drawn']
# Instantaneous Values (pre-recharge)
self.instant_V = self.energy_buffer.get_voltage()
self.instant_E = energy_drawn
# Energy source voltage intervals -> [(voltage, time),...]
voltage_intervals = self.energy_harvester.get_voltage_intervals(elapsed_time)
# Get equivalent resistances
charge_r = self.energy_harvester.get_equivalent_resistance()
e_harvested = self.energy_buffer.update(voltage_intervals, energy_drawn, charge_r, elapsed_time, self.cached_power_failure)
# Instant EH = e_harvested if charge + e_drawn
self.instant_EH = e_harvested
# if a cached power failure occurred, it means that power failures are checked manually, as otherwise the
# execution would not reach this point
# -> save the consumed energy and decrement it later
# Note: self.energy_buffer.update() keeps the voltage constant if self.cached_power_failure is set
if self.cached_power_failure:
self.cached_energy += energy_drawn
self.cached_harvested_energy += e_harvested
# Collect metrics
self.__update_stats(op_mode_name, additional_op_mode_name, is_wait_cycle, mcu_clock_action, elapsed_time, n_ticks, energy_draws, e_harvested)
self.mcu.update_stats(elapsed_time, n_ticks)
# Account for wait cycles
if not is_wait_cycle:
wait_cycles = self.mcu.get_action_wait_cycles(mcu_clock_action)
for _ in range(wait_cycles):
n_ticks += self.run_step(mcu_clock_action, op_mode_name, True, from_custom_device, additional_op_mode_name)
# Physical memory access latency
if mcu_clock_action == MCUClockCycleAction.PHYSICAL_MEMORY_ACCESS:
for memory in self.physical_memories:
name = memory.get_name()
wait_cycles = memory.get_wait_cycles(self.mcu.get_frequency())
# simulate wait cycles
for _ in range(wait_cycles):
n_ticks += self.run_step(mcu_clock_action, op_mode_name, True, from_custom_device, name)
# reset memories' operations queue
memory.reset_operations_queue()
# LPM exit
if mcu_clock_action == MCUClockCycleAction.LPM_EXIT:
# Turn on MCU and init custom devices
n_ticks += self.init()
return n_ticks + devices_ticks
def get_drained_energy(self):
"""
:return: the used energy
"""
return self.used_energy['total']['total']['total']
def init(self, custom_devices_print_enabled=True):
"""
Initializes the MCU and custom devices
:param custom_devices_print_enabled: enables/disables custom devices messages
:return: the number of clock cycles executed by the MCU
"""
n_ticks = 0
self.mcu.set_mcu_state(MCUPowerState.ON)
for device in self.custom_devices:
n_ticks += device.init(custom_devices_print_enabled)
return n_ticks
def run_custom_devices_logic(self, skip_device=None):
"""
Runs all custom device logic
:param skip_device: device to skip
:return: the number of clock cycles executed by the MCU during custom device operations
"""
n_ticks = 0
for device in self.custom_devices:
if device.name != skip_device:
n_ticks += device.run_logic()
return n_ticks
def power_failure_occurred(self):
"""
:return: if a power failure occurred during previous operations
"""
return self.cached_power_failure
def power_failure_occurring(self, reset_cache=True):
"""
:param reset_cache: reset the cache if a cached power failure occurred
:return: if the MCU needs to be shut down / a power failure is occurring
"""
if self.cached_power_failure:
# reset cache (the manual control is happening)
if reset_cache:
# e_harvested - e_drawn -> energy drawn (this is negative if e_harvested < e_drawn)
net_e_drawn = self.cached_harvested_energy - self.cached_energy
self.energy_buffer.increment_energy(net_e_drawn)
self.cached_power_failure = False
self.cached_energy = 0.0
self.cached_harvested_energy = 0.0
return True
# keep this call here, as it updates also all the power state events
events = self.get_power_state_events()
# voltage threshold
if self.power_off_condition == PowerOffCondition.ENERGY_BUFFER_VOLTAGE_THRESHOLD:
voltage = self.energy_buffer.get_voltage()
return voltage < self.power_off_voltage_threshold
# event-based
elif self.power_off_condition == PowerOffCondition.POWER_STATE_EVENT:
if len(events) == 0:
return False
for poff_event in self.power_off_events:
if poff_event in events:
return True
return False
else:
raise ConfigurationException("Power off condition not set.")
def record_power_failure(self):
"""
Records the occurrence of a power failure
"""
self.power_failures += 1
self.mcu.power_failure_callback()
def reset(self, run_gc_collect=True):
"""
Resets statistics
:param run_gc_collect: runs the garbage collector after deleting the saved signals
"""
del self.elapsed_time
self.elapsed_time = defaultdict(lambda: defaultdict(float))
del self.elapsed_ticks
self.elapsed_ticks = defaultdict(lambda: defaultdict(int))
del self.used_energy
self.used_energy = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
del self.vreg_energy_loss
self.vreg_energy_loss = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
# remove garbage
if run_gc_collect:
gc.collect()
self.power_failures = 0
self.cached_power_failure = False
self.cached_energy = 0.0
self.cached_harvested_energy = 0.0
self.instant_V = 0.0
self.instant_E = 0.0
self.instant_EH = 0.0
self._init_signals(run_gc_collect)
self.total_time = 0.0
self.state_retention.reset()
self.mcu.reset()
def circuit_equivalent_resistance(self, mcu_clock_action, energy_draws=None, elapsed_time=None):
"""
Calculates the circuit equivalent resistance (excluding the energy harvester)
:param mcu_clock_action: Clock cycle action of the MCU
:param energy_draws: pre-computed energy draws
:param elapsed_time: elapsed time in the pre-computed energy draws
:return: the circuit equivalent resistance
"""
if energy_draws is None:
energy_drawn = self.calculate_energy_draws(mcu_clock_action, elapsed_time)['energy_drawn']
else:
energy_drawn = energy_draws['energy_drawn']
return energy_utils.R_from_energy_t(self.energy_buffer.get_voltage(), energy_drawn, elapsed_time)
def execute_full_recharge(self, mcu_clock_action, op_mode_name, v_target, v_supply):
"""
Executes a full recharge of the energy buffer
:param mcu_clock_action: Clock cycle action of the MCU
:param op_mode_name: current operation identifier (string) for metrics identification
:param v_target: target voltage
:param v_supply: voltage of the power supply
"""
v_buff = self.energy_buffer.get_voltage()
# Check non-charging condition and return
if v_buff > v_target or v_buff > v_supply or v_target > v_supply:
return
# Equivalent resistance
charge_r = self.energy_harvester.get_equivalent_resistance()
# Calculate time required to recharge
t_recharge = self.energy_buffer.calculate_recharge_time_to_voltage(v_target, v_supply, charge_r)
energy_draws = self.calculate_energy_draws(MCUClockCycleAction.NOP_OFF_RECHARGE, t_recharge)
# Calculate harvested energy
e_harvested = self.energy_buffer.calculate_recharge_energy_to_voltage(v_target)
e_harvested += energy_draws['energy_drawn']
self.__update_stats(op_mode_name, None, False, mcu_clock_action, t_recharge, 0, energy_draws, e_harvested)
# Refill energy buffer to v_supply
self.energy_buffer.set_voltage(v_target)
def get_component(self, name):
for components in self.additional_components.values():
for c_name, component in components.items():
if c_name == name:
return component