Source code for spicelib.sim.sim_stepping

#!/usr/bin/env python

# -------------------------------------------------------------------------------
#
#  ███████╗██████╗ ██╗ ██████╗███████╗██╗     ██╗██████╗
#  ██╔════╝██╔══██╗██║██╔════╝██╔════╝██║     ██║██╔══██╗
#  ███████╗██████╔╝██║██║     █████╗  ██║     ██║██████╔╝
#  ╚════██║██╔═══╝ ██║██║     ██╔══╝  ██║     ██║██╔══██╗
#  ███████║██║     ██║╚██████╗███████╗███████╗██║██████╔╝
#  ╚══════╝╚═╝     ╚═╝ ╚═════╝╚══════╝╚══════╝╚═╝╚═════╝
#
# Name:        sim_stepping.py
# Purpose:     Spice Simulation Library intended to automate the exploring of
#              design corners, try different models and different parameter
#              settings.
#
# Author:      Nuno Brum (nuno.brum@gmail.com)
#
# Created:     31-07-2020
# License:     refer to the LICENSE file
# -------------------------------------------------------------------------------

__author__ = "Nuno Canto Brum <nuno.brum@gmail.com>"
__copyright__ = "Copyright 2017, Fribourg Switzerland"

import logging
from collections.abc import Callable, Iterable
from functools import wraps
from pathlib import Path

_logger = logging.getLogger("spicelib.SimStepper")
from ..editor.primitives import ValueType
from ..editor.spice_editor import SpiceEditor
from .sim_runner import AnyRunner, CallbackType, CallbackArgsType


class StepInfo:
    def __init__(self, what: str, elem: str, iterable: Iterable):
        self.what = what
        self.elem = elem
        self.iter = iterable

    def __len__(self):
        return len(list(self.iter))

    def __str__(self):
        return f"Iteration on {self.what} {self.elem} : {self.iter}"


[docs] class SimStepper(AnyRunner): """This class is intended to be used for simulations with many parameter sweeps. This provides a more user-friendly interface than the SpiceEditor/SimRunner class when there are many parameters to be stepped. Using the SpiceEditor/SimRunner classes a loop needs to be added for each dimension of the simulations. A typical usage would be as follows: .. code-block:: python netlist = SpiceEditor("my_circuit.asc") runner = SimRunner(parallel_sims=4) for dmodel in ("BAT54", "BAT46WJ") netlist.set_element_model("D1", model) # Sets the Diode D1 model for res_value1 in sweep(2.2, 2,4, 0.2): # Steps from 2.2 to 2.4 with 0.2 increments netlist.set_component_value('R1', res_value1) # Updates the resistor R1 value to be 3.3k for temperature in sweep(0, 80, 20): # Makes temperature step from 0 to 80 degrees in 20 degree steps netlist.set_parameters(temp=80) # Sets the simulation temperature to be 80 degrees for res_value2 in (10, 25, 32): netlist.set_component_value('R2', res_value2) # Updates the resistor R2 value to be 3.3k runner.run(netlist) runner.wait_completion() # Waits for the Spice simulations to complete With SimStepper the same thing can be done as follows, resulting in a cleaner code: .. code-block:: python netlist = SpiceEditor("my_circuit.asc") Stepper = SimStepper(netlist, SimRunner(parallel_sims=4, output_folder="./output")) Stepper.add_model_sweep('D1', "BAT54", "BAT46WJ") Stepper.add_component_sweep('R1', sweep(2.2, 2,4, 0.2)) # Steps from 2.2 to 2.4 with 0.2 increments Stepper.add_parameter_sweep('temp', sweep(0, 80, 20)) # Makes temperature step from 0 to 80 degrees in 20 # degree steps Stepper.add_component_sweep('R2', (10, 25, 32)) # Updates the resistor R2 value to be 3.3k Stepper.run_all() Another advantage of using SimStepper is that it can optionally use the .SAVEBIAS in the first simulation and then use the .LOADBIAS command at the subsequent ones to speed up the simulation times. """ def __init__(self, editor: SpiceEditor, runner: AnyRunner): self.runner = runner self.editor = editor self.iter_list: list[StepInfo] = [] self.current_values = {} self.sim_info = {} @wraps(SpiceEditor.add_instruction, updated=()) # updated=() solves conflict between wraps and abstract classes def add_instruction(self, instruction: str): self.editor.add_instruction(instruction) @wraps(SpiceEditor.add_instructions, updated=()) # updated=() solves conflict between wraps and abstract classes def add_instructions(self, *instructions) -> None: self.editor.add_instructions(*instructions) @wraps(SpiceEditor.remove_instruction, updated=()) # updated=() solves conflict between wraps and abstract classes def remove_instruction(self, instruction) -> bool: return self.editor.remove_instruction(instruction) @wraps(SpiceEditor.remove_Xinstruction, updated=()) # updated=() solves conflict between wraps and abstract classes def remove_Xinstruction(self, search_pattern: str) -> bool: return self.editor.remove_Xinstruction(search_pattern) @wraps(SpiceEditor.set_parameters, updated=()) # updated=() solves conflict between wraps and abstract classes def set_parameters(self, **kwargs): self.editor.set_parameters(**kwargs) self.current_values.update(**kwargs) @wraps(SpiceEditor.set_parameter, updated=()) # updated=() solves conflict between wraps and abstract classes def set_parameter(self, param: str, value: ValueType) -> None: self.editor.set_parameter(param, value) self.current_values[param] = value @wraps(SpiceEditor.set_component_values, updated=()) # updated=() solves conflict between wraps and abstract classes def set_component_values(self, **kwargs): self.editor.set_component_values(**kwargs) self.current_values.update(**kwargs) @wraps(SpiceEditor.set_component_value, updated=()) # updated=() solves conflict between wraps and abstract classes def set_component_value(self, device: str, value: ValueType) -> None: self.editor.set_component_value(device, value) self.current_values[device] = value @wraps(SpiceEditor.set_element_model, updated=()) # updated=() solves conflict between wraps and abstract classes def set_element_model(self, element: str, model: str) -> None: self.editor.set_element_model(element, model) self.current_values[element] = model
[docs] def add_param_sweep(self, param: str, iterable: Iterable): """Adds a dimension to the simulation, where the param is swept.""" self.iter_list.append(StepInfo("param", param, iterable))
[docs] def add_value_sweep(self, comp: str, iterable: Iterable): """Adds a dimension to the simulation, where a component value is swept.""" # The next line raises an ComponentNotFoundError if the component doesn't exist _ = self.editor.get_component_value(comp) self.iter_list.append(StepInfo("component", comp, iterable))
[docs] def add_model_sweep(self, comp: str, iterable: Iterable): """Adds a dimension to the simulation, where a component model is swept.""" # The next line raises an ComponentNotFoundError if the component doesn't exist _ = self.editor.get_component_value(comp) self.iter_list.append(StepInfo("model", comp, iterable))
[docs] def total_number_of_simulations(self): """Returns the total number of simulations foreseen.""" total = 1 for step in self.iter_list: l = len(step) if l: total *= l else: _logger.debug(f"'{step}' is empty.") return total
[docs] def run_all(self, callback: CallbackType = None, callback_args: CallbackArgsType = None, switches: list | None = None, timeout: float | None = None, wait_completion: bool = True, filenamer: Callable[[dict[str, str]], str] | None = None, exe_log: bool = False, ) -> None: """ Runs all sweeps configured with the methods: - add_value_sweep() - add_model_sweep() - add_param_sweep() This function will call the SimRunner run method for each combination of the sweeps defined. The parameters are mostly the same as in the SimRunner.run() method, except the filenamer and wait_completion parameters. :param callback: See the SimRunner run method. :param callback_args: See the SimRunner run method. :param switches: Command line switches override :param timeout: See the SimRunner run method. :param wait_completion: See the SimRunner run method. :param filenamer: A function that receives a dictionary in keyword form (``**dict``) and returns a string. This string will be passed to the run_filename parameter on the SimRunner run method. It is important that the function assures a unique filename per simulation. :param exe_log: See the SimRunner run method. :returns: Nothing """ iter_no = 0 iterators = [iter(step.iter) for step in self.iter_list] while True: while 0 <= iter_no < len(self.iter_list): try: value = iterators[iter_no].__next__() except StopIteration: iterators[iter_no] = iter(self.iter_list[iter_no].iter) iter_no -= 1 continue self.current_values[self.iter_list[iter_no].elem] = value if self.iter_list[iter_no].what == 'param': self.editor.set_parameter(self.iter_list[iter_no].elem, value) elif self.iter_list[iter_no].what == 'component': self.editor.set_component_value(self.iter_list[iter_no].elem, value) elif self.iter_list[iter_no].what == 'model': self.editor.set_element_model(self.iter_list[iter_no].elem, value) else: # TODO: develop other types of sweeps EX: add .STEP instruction raise ValueError("Not Supported sweep") iter_no += 1 if iter_no < 0: break run_filename = filenamer(**self.current_values) if filenamer else None task = self.runner.run(self.editor, callback=callback, callback_args=callback_args, switches=switches, timeout=timeout, run_filename=run_filename, exe_log=exe_log) # Now storing the simulation information if task and task.netlist_file: sim_info = self.current_values.copy() sim_info['netlist'] = task.netlist_file.name self.sim_info[task.runno] = sim_info iter_no = len(self.iter_list) - 1 # Resets the counter to start next iteration if wait_completion: # Now waits for the simulations to end self.runner.wait_completion()
[docs] def export_step_info(self, export_filename: Path | str, delimiter: str = ";"): """ Exports the stepping values to a CSV file. It writes a row per each simulation done. The columns are all the values that were set during the session. The value on each row is the value of the parameter or component value/model at each simulation. This information can also be accessed using the sim_info attribute. The sim_info is a dictionary where the keys are the simulation number (runno) and the values are an enclosed dictionary containing the parameters there were set for that simulation. :param export_filename: export file path :param delimiter: delimiter character on the CSV """ import csv rows = [runno for runno in self.sim_info] rows.sort() # Extract column names from the first dictionary fieldnames = ['runno'] + list(self.sim_info[rows[0]].keys()) # Open a CSV file for writing with open(export_filename, 'w', newline='') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=delimiter, quotechar='"', quoting=csv.QUOTE_NONNUMERIC) # Write the header writer.writeheader() # Write the data for runno in rows: row_data_with_id = {'runno': runno} row_data_with_id.update(self.sim_info[runno]) writer.writerow(row_data_with_id)
@property def okSim(self): """Number of successful simulations""" return self.runner.okSim @property def runno(self): """Number simulations done.""" return self.runner.runno @property def failSim(self): """Number of failed simulations""" return self.runner.failSim