diff --git a/.github/workflows/test_code.yml b/.github/workflows/test_python_api.yml similarity index 93% rename from .github/workflows/test_code.yml rename to .github/workflows/test_python_api.yml index 1077aaba7..506f273bf 100644 --- a/.github/workflows/test_code.yml +++ b/.github/workflows/test_python_api.yml @@ -1,5 +1,5 @@ --- -name: Lint and Test +name: Lint and Test Python API # https://docs.github.com/en/free-pro-team@latest/actions/guides/building-and-testing-python on: @@ -34,6 +34,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | + sudo apt install -y ngspice python -m pip install --upgrade pip pip install -r requirements.txt # pip install . diff --git a/docs/source/common-python-api.rst b/docs/source/common-python-api.rst index 3afab7188..dc0471e01 100644 --- a/docs/source/common-python-api.rst +++ b/docs/source/common-python-api.rst @@ -87,4 +87,125 @@ Constants ^^^^^^^^^ 1. ``COMMON_PLATFORMS_PREFIX_MAP`` - This is a dictionary of common platforms (currently sky130) and their cell naming prefixes. See the ``cell()`` def in the ``generate_verilog()`` function for more information on how to use it. \ No newline at end of file + This is a dictionary of common platforms (currently sky130) and their cell naming prefixes. See the ``cell()`` def in the ``generate_verilog()`` function for more information on how to use it. + +2. Simulation (``common.simulation``) +##################################################### +This module exports functions used to simulate SPICE testbenches with multiple parameters. + +This module supports the use of `Mako `_ templating library to insert parameters into SPICE templates. + +Functions +^^^^^^^^^ +1. ``run_simulations(parameters: dict, platform: str, simulation_dir: str, template_path: str, runs_dir: str, sim_tool: str, num_concurrent_sims: int, netlist_path: str) -> int`` + + Generates configurations of all combinations of the given ``parameters`` and runs simulations for each case. The testbench SPICE file, configuration parameters, and the ouptut for each run are generated in ``{simulation_dir}/{runs_dir}``. + + The testbench SPICE file given by ``template_path`` follows the `Mako `_ templating syntax. Use the ``${parameter}`` syntax for inserting parameters in the file. The following parameters are automatically inserted during each run. + + - ``run_number`` (int): The number/index of the run/configuration. + - ``sim_tool`` (str): Command for the simulation tool used. + - ``platform`` (str): The platform/PDK. + - ``template`` (str): Path to the SPICE testbench template. + - ``netlist_path`` (str): Absolute path to the SPICE netlist of the design to be simulated. + + Example SPICE template: (From the `Temperature Sensor Generator `_) + .. code-block:: spice + + .lib '${model_file}' ${model_corner} + .include '${netlist_path}' + + .param temp_var = ${temp} + .param vvdd = 1.8 + .param sim_end = '800m/exp(0.04*temp_var)' + + Each configuration is run/simulated in a directory in the ``runs_dir``. Each run directory contains the final SPICE testbench with the parameters inserted, a ``parameters.txt`` file containing the values of each parameter, and the output log file. + + ``parameters`` is a dict with keys corresponding to the parameter's name and the values of one of the following types. + + 1. A constant value. + The value of this parameter will be the same for every configuration/run. + .. code-block:: python + + {'param': 'value'} + + 2. Array of integer/float/string constants. + Each of the values in the array will be swept. + .. code-block:: python + + {'param': [1, 2, 3, 8]} + # OR + { + 'param': { + 'values': [1, 2, 3, 8] + } + } + + 3. Increments. + All values starting from ``start`` (included) and ending at ``end`` (included if it is ``start + n * step``) will be swept with a step of ``step``. The default value for ``step`` is ``1``. + .. code-block:: python + + {'param': { + 'start': 10, + 'end': 50, + 'step': 10 + }} + # param will take values 10, 20, 30, 40, 50 + + Example parameters: + .. code-block:: python + + # Runs 10 total simulations + # Sweeps through all temperatures from 10 to 100 (both included) with increments of 10. + example1 = { + 'temp': {'start': 10, 'end': 100, 'step': 10} + } + + # Runs 9 total simulations + # Sweeps through all the 3 input voltages as well as all the 3 temperatures + example2 = { + 'input_voltage': [1, 2, 3], + 'temp': [20, 30, 40] + } + + # Runs 4 total simulations + # Duty cycle and aux_spice_path remain the same in all simulations + # input_voltage is swept + example3 = { + 'duty_cycle': 10, + 'aux_spice_path': 'auxcell.cdl', + 'input_voltage': [1, 2, 3] + } + + See the generators' Python files in ``tools/`` for more examples. + + Arguments: + - ``parameters`` (dict): Dictionary of parameters. Explained above. + - ``platform`` (str): Platform/PDK. (eg: ``sky130hd```) + - ``simulation_dir`` (str): Path to the directory where the simulation source files are placed and the outputs will be generated. (Default: ``simulations``) + - ``template_path`` (str): Path to the SPICE template file for the testbench. (Default: ``templates/template.sp``) + - ``runs_dir`` (str): Path to a directory inside the ``simulation_dir`` directory where the outputs for the simulations will be generated. (Default: ``runs``) + - ``sim_tool`` (str): Command for the simulation tool. ``ngspice``, ``xyce``, and ``finesim`` are supported. (Default: ``ngspice``) + - ``num_concurrent_sims`` (int): The maximum number of concurrent simulations. (Default: ``4``) + - ``netlist_path`` (str): Path to the SPICE netlist inside the ``simulation_dir`` of the design to be simulated. (Default: ``netlist.sp``) + + **Returns (int)**: The total number of simulations run. + + Overall example: (From the `Temperature Sensor Generator `_) + .. code-block:: python + + run_simulations( + parameters={ + 'temp': {'start': tempStart, 'end': tempStop, 'step': tempStep}, + 'model_file': model_file, + 'model_corner': platformConfig['model_corner'], + 'nominal_voltage': platformConfig['nominal_voltage'], + 'design_name': designName + }, + platform="sky130hd", + simulation_dir="simulations", + template_path=os.path.join("templates", f"tempsenseInst_{simTool}.sp"), + runs_dir=f"run/prePEX_inv{num_inv}_header{num_header}/", + sim_tool=simTool, + netlist_path=dstNetlist + ) \ No newline at end of file diff --git a/openfasoc/generators/common/__init__.py b/openfasoc/generators/common/__init__.py index ae57c6291..c1ca2645e 100644 --- a/openfasoc/generators/common/__init__.py +++ b/openfasoc/generators/common/__init__.py @@ -5,6 +5,8 @@ - `common.verilog_generation` 1. `generate_verilog(parameters: dict, src_dir: str, out_dir: str) -> None`: Used to generate synthesizable Verilog files (for OpenROAD flow) from source Mako-based Verilog templates. 2. `COMMON_PLATFORMS_PREFIX_MAP` (dict): This is a dictionary of common platforms (currently sky130) and their cell naming prefixes. +- `common.simulation` + 1. `run_simulations()`: Used to run SPICE testbenches with multiple parameters. See individual function documentation for more information on a particular function. """ \ No newline at end of file diff --git a/openfasoc/generators/common/simulation/__init__.py b/openfasoc/generators/common/simulation/__init__.py new file mode 100644 index 000000000..90150528e --- /dev/null +++ b/openfasoc/generators/common/simulation/__init__.py @@ -0,0 +1,104 @@ +"""A common simulation used in OpenFASOC generators. + +Sweeps all combinations of given parameters and runs parallel SPICE simulations with different configurations based on the parameters. + +Exported functions: +- `run_simulations()` +""" + +from os import path, makedirs +from common.simulation.simulation_config import _generate_configs +from common.simulation.simulation_run import _run_simulations + +def run_simulations( + parameters: dict, + platform: str, + simulation_dir: str = "simulations", + template_path: str = path.join("templates", "template.sp"), + runs_dir: str = "runs", + sim_tool: str = "ngspice", + num_concurrent_sims: int = 4, + netlist_path: str = "netlist.sp" +) -> int: + """Runs SPICE simulations. + + Generates configurations of all combinations of the given `parameters` and simulates each case. The testbench SPICE file, configuration parameters, and the output for each run are generated in the `simulation_dir/runs_dir` directory. + + The testbench SPICE file given by `template_path` follows the [Mako](https://makotemplates.org) templating syntax. Use the `${parameter}` syntax for inserting parameters in the file. The following parameters are automatically inserted during each run. + - `run_number` (int): The number/index of the run/configuration. + - `sim_tool` (str): Command for the simulation tool used. + - `platform` (str): The platform/PDK. + - `template` (str): Path to the SPICE testbench template. + - `netlist_path` (str): Absolute path to the SPICE netlist of the design to be simulated. + + Each configuration is run/simulated in a directory in the `runs_dir`. Each run directory contains the final SPICE testbench with the parameters inserted, a `parameters.txt` file containing the values of each parameter, and the output log file. + + `parameters` is a dict with keys corresponding to the parameter's name and the values of one of the following types. + 1. A constant value. + The value of this parameter will be the same for every configuration/run. + ``` + {'param': 'value'} + ``` + + 2. Array of integer/float/string constants. + Each of the values in the array will be swept. + ``` + {'param': [1, 2, 3, 8]} + # OR + { + 'param': { + 'values': [1, 2, 3, 8] + } + } + ``` + + 3. Increments. + All values starting from `start` and ending at `end` will be swept with a step of `step`. The default value for `step` is `1`. + ``` + {'param': { + 'start': 10, + 'end': 50, + 'step': 10 + }} + # param will take values 10, 20, 30, 40, 50 + ``` + + Arguments: + - `parameters` (dict): Parameters used to generate the runs. + - `platform` (str): Platform/PDK. + - `simulation_dir` (str = "simulations"): Path to the directory where the simulation source files are placed and the outputs will be generated. + - `template_path` (str = "templates/template.sp"): Path to the SPICE template file for the testbench. (The template is a SPICE file with [Mako](https://makotemplates.org) templating syntax) + - `runs_dir` (str = "runs"): Path to a directory inside the `simulation_dir` directory where the outputs for the simulations will be generated. + - `sim_tool` (str = "ngspice"): Command for the simulation tool. + - `num_concurrent_sims` (int = 4): The maximum number of concurrent simulations. + - `netlist_path` (str = "netlist.sp"): Path to the SPICE netlist of the design to be simulated. + + Returns (int): The number of simulations run. + """ + + runs_dir_path = path.join(simulation_dir, runs_dir) + template = path.join(simulation_dir, template_path) + + if not path.exists(runs_dir_path): + makedirs(runs_dir_path) + + config_number = _generate_configs( + parameters=parameters, + sim_tool=sim_tool, + platform=platform, + template=template, + netlist_path=netlist_path, + runs_dir_path=runs_dir_path + ) + + print(f"Number of configurations: {config_number}") + print(f"Number of concurrent simulations: {num_concurrent_sims}") + + _run_simulations( + num_configs=config_number, + num_concurrent_sims=num_concurrent_sims, + sim_tool=sim_tool, + runs_dir_path=runs_dir_path + ) + + return config_number diff --git a/openfasoc/generators/common/simulation/simulation_config.py b/openfasoc/generators/common/simulation/simulation_config.py new file mode 100644 index 000000000..135d8d070 --- /dev/null +++ b/openfasoc/generators/common/simulation/simulation_config.py @@ -0,0 +1,195 @@ +"""Sweeps all combinations of given parameters and generates the configurations. + +This module is part of the simulation common module. Functions for generating final SPICE files from a template are exported. + +Configurations are generated from a template SPICE file that follows the [Mako](https://makotemplates.org) syntax. All possible combinations of the input parameters are swept. The final SPICE files are generated from the template by inserting the parameter combinations. Each file is called a "config" or "configuration." + +Exported functions: +- `_generate_configs()` +- `_generate_run_parameters()` +- `_generate_config()` + +See individual functions for further documentation. +""" + +from os import path, makedirs +from shutil import rmtree +from common.verilog_generation import _generate_file + +def _generate_configs( + parameters: dict, + sim_tool: str, + platform: str, + template: str, + netlist_path: str, + runs_dir_path: str +) -> int: + """Generates configurations for the simulations. + + Generates a directory for each configuration in the runs directory given by `runs_dir_path`. Each configuration includes a testbench SPICE file (`sim_[run_number].sp`) corresponding to the configuration and a `parameters.txt` file containing the values of each parameter in the particular configuration. + + `parameters` is a dict with keys corresponding to the parameter's name and the values of one of the following types. + 1. A constant value. + The value of this parameter will be the same for every configuration/run. + ``` + {'param': 'value'} + ``` + + 2. Array of integer/float/string constants. + Each of the values in the array will be swept. + ``` + {'param': [1, 2, 3, 8]} + # OR + { + 'param': { + 'values': [1, 2, 3, 8] + } + } + ``` + + 3. Increments. + All values starting from `start` and ending at `end` will be swept with a step of `step`. The default value for `step` is `1`. + ``` + {'param': { + 'start': 10, + 'end': 50, + 'step': 10 + }} + # param will take values 10, 20, 30, 40, 50 + ``` + + Arguments: + - `parameters` (dict): Parameters used to generate the runs. + - `sim_tool` (str): Command for the simulation tool. + - `platform` (str): Platform/PDK. + - `template` (str): Path to the SPICE template file for the testbench. (The template is a SPICE file with [Mako](https://makotemplates.org) templating syntax) + - `netlist_path` (str): Path to the netlist file used for simulation. (Absolute path to this file will be added as a Mako parameter) + - `runs_dir_path` (str): Path to the directory in which the simulation runs will be generated. + """ + parameters_iterator = {} + + for parameter in parameters.items(): + parameter_values = [] + + if type(parameter[1]) is dict: + if 'values' in parameter[1]: + parameter_values = parameter[1]['values'] + + elif 'start' in parameter[1] and 'end' in parameter[1]: + value = parameter[1]['start'] + step = parameter[1]['step'] if 'step' in parameter[1] else 1 + + while value <= parameter[1]['end']: + parameter_values.append(value) + value += step + + elif type(parameter[1]) is list: + parameter_values = parameter[1] + + else: + parameter_values = [parameter[1]] + + parameters_iterator[parameter[0]] = { + 'values': parameter_values, + 'i': 0 + } + + num_params = len(parameters_iterator.keys()) + config_number = 0 + configs_generated = num_params == 0 + while not configs_generated: + config_number += 1 + _generate_config( + run_parameters=_generate_run_parameters( + parameters_iterator=parameters_iterator, + config_number=config_number, + sim_tool=sim_tool, + platform=platform, + template=template, + netlist_path=netlist_path + ), + config_number=config_number, + runs_dir_path=runs_dir_path, + template=template + ) + + change_next_param = True + for i, parameter in enumerate(parameters_iterator.items()): + if change_next_param: + parameter[1]['i'] += 1 + + change_next_param = parameter[1]['i'] == len(parameter[1]['values']) + + if change_next_param: + parameter[1]['i'] = 0 + + if i == num_params - 1 and change_next_param: + configs_generated = True + + return config_number + +def _generate_run_parameters( + parameters_iterator: dict, + config_number: int, + sim_tool: str, + platform: str, + template: str, + netlist_path: str +) -> dict: + """Generates and returns parameters for a given run. + + Generates and returns the final parameters for a given run from the parameters iterator and the number of the config. + + Arguments: + - `parameters_iterator` (dict): A dictionary with keys equal to the parameter name and values of the following format: `{'values': list[str], 'i': int}`. Here `values` is a list of all the possible values the particular parameter can take, and `i` is the value selected for the current config. + - `config_number` (str): The number/index of the configuration. + - `sim_tool` (str): Command for the simulation tool. + - `platform` (str): Platform/PDK. + - `template` (str): Path to the SPICE template file for the testbench. (The template is a SPICE file with [Mako](https://makotemplates.org) templating syntax) + - `netlist_path` (str): Path to the netlist file used for simulation. (Absolute path to this file will be added as a Mako parameter) + """ + run_parameters = { + 'run_number': config_number, + 'sim_tool': sim_tool, + 'platform': platform, + 'template': template, + 'netlist_path': path.abspath(netlist_path), + 'root_dir': path.abspath('.') + } + + for parameter in parameters_iterator.items(): + run_parameters[parameter[0]] = parameter[1]['values'][parameter[1]['i']] + + return run_parameters + +def _generate_config( + run_parameters: dict, + config_number: int, + runs_dir_path: str, + template: str +) -> None: + """Generates the files required for a particular configuration. + + Generates the files (SPICE testbench file and `parameters.txt`) for a particular configuration/run in the directory for the run. + + Arguments: + - `run_parameters` (dict): Parameters for the particular run. + - `config_number` (str): The number/index of the configuration. + - `runs_dir_path` (str): Path to the directory in which the simulation runs will be generated. + - `template` (str): Path to the SPICE template file for the testbench. (The template is a SPICE file with [Mako](https://makotemplates.org) templating syntax) + """ + run_dir_path = path.join(runs_dir_path, str(config_number)) + + if not path.exists(run_dir_path): + makedirs(run_dir_path) + else: + rmtree(run_dir_path) + makedirs(run_dir_path) + + open(path.join(run_dir_path, 'parameters.txt'), "w").write(str(run_parameters)) + + _generate_file( + input_path=template, + output_path=path.join(run_dir_path, f"sim_{config_number}.sp"), + parameters=run_parameters + ) \ No newline at end of file diff --git a/openfasoc/generators/common/simulation/simulation_run.py b/openfasoc/generators/common/simulation/simulation_run.py new file mode 100644 index 000000000..df5fb39dd --- /dev/null +++ b/openfasoc/generators/common/simulation/simulation_run.py @@ -0,0 +1,151 @@ +"""Runs SPICE simulations in parallel. + +This module is part of the simulation common module. Functions for running simulations from SPICE files generated using the `simulation_config` module are exported. + +SPICE files are generated by the `simulation_config` module in sub-directories. Each SPICE file is run using the given simulation tool and the outputs and log files are generated in the same sub-directory. The simulations can be run in parallel with a configurable number of concurrent simulations. + +Exported functions: +- `_run_simulations()` +- `_run_config()` +- `_threaded_run()` + +See individual functions for further documentation. +""" + +from os import path +from common.simulation.utils import _print_progress +import time +import threading +import subprocess + +def _run_simulations( + num_configs: int, + num_concurrent_sims: int, + sim_tool: str, + runs_dir_path: str +): + """Runs (simulates) the generated configurations. + + Runs `num_configs` number of simulations, with configurations in the `runs_dir_path` directory. + + Arguments: + - `num_configs` (int): Number of configurations generated in the `runs_dir_path` directory. + - `num_concurrent_sims` (int): The maximum number of concurrent simulations. + - `sim_tool` (str): Path to the directory in which the simulation runs will be generated. + - `runs_dir_path` (str): Path to the directory in which the simulation runs will be generated. + """ + + simulation_state = { + 'ongoing_sims': 0, + 'completed_sims': 0, + 'failed_sims': 0 + } + + def thread_on_exit(exit_status: int, state=simulation_state): + if exit_status == 0: + state['ongoing_sims'] -= 1 + state['completed_sims'] += 1 + else: + state['ongoing_sims'] -= 1 + state['failed_sims'] += 1 + + start_time = int(time.time()) + run_number = 1 + + while (simulation_state['completed_sims'] + simulation_state['failed_sims']) < num_configs: + if simulation_state['ongoing_sims'] < num_concurrent_sims and run_number <= num_configs: + _run_config( + sim_tool=sim_tool, + run_dir=path.join(runs_dir_path, str(run_number)), + run_number=run_number, + on_exit=thread_on_exit + ).start() + + simulation_state['ongoing_sims'] += 1 + run_number += 1 + + _print_progress(num_configs, simulation_state['completed_sims'], simulation_state['failed_sims'], start_time) + time.sleep(1) + + _print_progress(num_configs, simulation_state['completed_sims'], simulation_state['failed_sims'], start_time, end='\n') + +def _run_config( + sim_tool: str, + run_dir: str, + run_number: int, + on_exit +): + """Runs (simulates) a particular configuration. + + Runs the `run_number`th configuration in the `run_dir` directory. + + Arguments: + - `sim_tool` (str): Command for the simulation tool. + - `run_dir` (str): Path to the directory in which the run configuration exists. + - `run_number` (int): The number/index of the run configuration. + - `on_exit` (function): An optional function to run when the simulation completes. + """ + + return threading.Thread( + target=_threaded_run, + args=(sim_tool, run_dir, run_number, on_exit), + daemon=True + ) + +def _threaded_run( + sim_tool: str, + run_dir: str, + run_number: int, + on_exit +): + """Runs a particular simulation using `sim_tool` in a `threading.Thread`. + + Arguments: + - `sim_tool` (str): Command for the simulation tool. + - `run_dir` (str): Path to the directory in which the run configuration exists. + - `run_number` (int): The number/index of the run configuration. + - `on_exit` (function): An optional function to run when the simulation completes. + """ + + try: + if sim_tool == "ngspice": + subprocess.run( + [ + "ngspice", + "-b", + "-o", + f"sim_{run_number}.log", + f"sim_{run_number}.sp" + ], + cwd=run_dir, + capture_output=True + ) + elif sim_tool == "xyce": + subprocess.run( + [ + "xyce", + "-l", + f"sim_{run_number}.log", + "-o", + f"sim_{run_number}", + f"sim_{run_number}.sp" + ], + cwd=run_dir, + capture_output=True + ) + elif sim_tool == "finesim": + subprocess.run( + [ + "finesim", + "-o", + f"sim_{run_number}", + "-spice", + f"sim_{run_number}.sp" + ], + cwd=run_dir, + capture_output=True + ) + except: + return on_exit(1) + + on_exit(0) \ No newline at end of file diff --git a/openfasoc/generators/common/simulation/utils.py b/openfasoc/generators/common/simulation/utils.py new file mode 100644 index 000000000..7a028fcdf --- /dev/null +++ b/openfasoc/generators/common/simulation/utils.py @@ -0,0 +1,36 @@ +"""Utility functions for the simulation common module. + +Functions for displaying the simulation progress and formatting the output are exported. + +Exported functions: +- `_print_progress()` +- `_format_elapsed_time()` + +See individual functions for further documentation. +""" + +import time + +def _print_progress(total_runs: int, completed_sims: int, failed_sims: int, start_time: int, end: str = '\r'): + """Displays the simulation progress. + + Displays the number of simulations completed, total simulations and the time elapsed. + """ + print(f"Completed: {completed_sims}. Failed: {failed_sims}. Total: {total_runs}. Elapsed time: {_format_elapsed_time(start_time)}{f'. Average: {_format_elapsed_time(start_time, completed_sims)} per simulation.' if completed_sims > 0 else ''}", end=end) + +def _format_elapsed_time(start_time: int, divisor: int = 1): + """Formats the elapsed time (in seconds) into hours, minutes, and seconds format. + """ + elapsed_seconds = int((int(time.time()) - start_time) / divisor) + + if elapsed_seconds > 60 * 60: + hours, minutes = divmod(elapsed_seconds, 60 * 60) + minutes, seconds = divmod(minutes, 60) + return f"{hours}h {minutes}m {seconds}s" + + elif elapsed_seconds > 60: + minutes, seconds = divmod(elapsed_seconds, 60) + return f"{minutes}m {seconds}s" + + else: + return f"{elapsed_seconds}s" \ No newline at end of file diff --git a/openfasoc/generators/temp-sense-gen/simulations/.gitignore b/openfasoc/generators/temp-sense-gen/simulations/.gitignore new file mode 100644 index 000000000..e5224d533 --- /dev/null +++ b/openfasoc/generators/temp-sense-gen/simulations/.gitignore @@ -0,0 +1 @@ +run \ No newline at end of file diff --git a/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_finesim.sp b/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_finesim.sp index 5ae7a22b0..f6eed1040 100644 --- a/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_finesim.sp +++ b/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_finesim.sp @@ -19,11 +19,11 @@ + method=gear + measdgt=10 -.lib '@model_file' @model_corner -.include '@netlist' +.lib '${model_file}' ${model_corner} +.include '${netlist_path}' -.param temp_var = @temp -.param vvdd = @voltage +.param temp_var = ${temp} +.param vvdd = ${nominal_voltage} .param sim_end = 800m/exp(0.04*temp_var) *@modelingxi1 en out VDD VIN VSS TEMP_sensor @@ -33,7 +33,7 @@ *@verification+ DOUT[19] DOUT[1] DOUT[20] DOUT[21] DOUT[22] DOUT[23] DOUT[2] *@verification+ DOUT[3] DOUT[4] DOUT[5] DOUT[6] DOUT[7] DOUT[8] DOUT[9] RESET_COUNTERn *@verification+ SEL_CONV_TIME[0] SEL_CONV_TIME[1] SEL_CONV_TIME[2] SEL_CONV_TIME[3] -*@verification+ VDD VIN VSS en lc_out out outb tempsenseInst +*@verification+ VDD VIN VSS en lc_out out outb ${design_name} vCLK_REF CLK_REF 0 pulse 0 'vvdd' 12u 1n 1n '4/32768' '8/32768' vRESET_COUNTERn RESET_COUNTERn 0 pwl 0 0 5u 0 '5u+1n' 'vvdd' diff --git a/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_ngspice.sp b/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_ngspice.sp index b11ad81a9..e80470212 100644 --- a/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_ngspice.sp +++ b/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_ngspice.sp @@ -1,7 +1,7 @@ .OPTION sim_la .OPTION autostop -.OPTION temp = @temp +.OPTION temp = ${temp} .OPTION + artist=2 + ingold=2 @@ -19,10 +19,10 @@ + method=gear + measdgt=10 -.lib '@model_file' @model_corner -.include '@netlist' +.lib '${model_file}' ${model_corner} +.include '${netlist_path}' -.param temp_var = @temp +.param temp_var = ${temp} .param vvdd = 1.8 .param sim_end = '800m/exp(0.04*temp_var)' @@ -31,7 +31,7 @@ xi1 CLK_REF DONE DOUT[0] DOUT[10] DOUT[11] + DOUT[19] DOUT[1] DOUT[20] DOUT[21] DOUT[22] DOUT[23] DOUT[2] + DOUT[3] DOUT[4] DOUT[5] DOUT[6] DOUT[7] DOUT[8] DOUT[9] RESET_COUNTERn + SEL_CONV_TIME[0] SEL_CONV_TIME[1] SEL_CONV_TIME[2] SEL_CONV_TIME[3] -+ en lc_out out outb VDD VSS @design_nickname ++ en lc_out out outb VDD VSS ${design_name} vCLK_REF CLK_REF 0 pulse 0 'vvdd' 12u 1n 1n '4/32768' '8/32768' vRESET_COUNTERn RESET_COUNTERn 0 pwl 0 0 5u 0 '5u+1n' 'vvdd' diff --git a/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_xyce.sp b/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_xyce.sp index 96aae91bd..e9151e0a1 100644 --- a/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_xyce.sp +++ b/openfasoc/generators/temp-sense-gen/simulations/templates/tempsenseInst_xyce.sp @@ -1,7 +1,7 @@ *.OPTION sim_la .OPTION autostop -.OPTION temp = @temp +.OPTION temp = ${temp} *.OPTION *+ artist=2 *+ ingold=2 @@ -19,21 +19,21 @@ *+ method=gear *+ measdgt=10 -.lib '@model_file' @model_corner -.include '@netlist' +.lib '${model_file}' ${model_corner} +.include '${netlist_path}' -.param temp_var = @temp +.param temp_var = ${temp} .param vvdd = 1.8 * sim_end = 800m/exp(0.04*temp_var) -* .param sim_end = @sim_end +* .param sim_end = ${sim_end} xi1 CLK_REF DONE DOUT[0] DOUT[10] DOUT[11] + DOUT[12] DOUT[13] DOUT[14] DOUT[15] DOUT[16] DOUT[17] DOUT[18] + DOUT[19] DOUT[1] DOUT[20] DOUT[21] DOUT[22] DOUT[23] DOUT[2] + DOUT[3] DOUT[4] DOUT[5] DOUT[6] DOUT[7] DOUT[8] DOUT[9] RESET_COUNTERn + SEL_CONV_TIME[0] SEL_CONV_TIME[1] SEL_CONV_TIME[2] SEL_CONV_TIME[3] -+ en lc_out out outb VDD VSS @design_nickname ++ en lc_out out outb VDD VSS ${design_name} vCLK_REF CLK_REF 0 pulse 0 'vvdd' 12u 1n 1n '4/32768' '8/32768' vRESET_COUNTERn RESET_COUNTERn 0 pwl 0 0 5u 0 '5u+1n' 'vvdd' @@ -48,7 +48,7 @@ vVSS VSS 0 dc c0 lc_out 0 1f -.TRAN 10n @sim_end +.TRAN 10n ${sim_end} .meas tran period TRIG when v(lc_out)=1.0 td=10p rise=2 + TARG when v(lc_out)=1.0 td=10p rise=3 diff --git a/openfasoc/generators/temp-sense-gen/test.json b/openfasoc/generators/temp-sense-gen/test.json index 28215c0f0..f05174c2d 100644 --- a/openfasoc/generators/temp-sense-gen/test.json +++ b/openfasoc/generators/temp-sense-gen/test.json @@ -2,11 +2,11 @@ "module_name": "tempsenseInst_error", "generator": "temp-sense-gen", "specifications": { - "temperature": { "min": -20, "max": 100 }, + "temperature": { "min": -20, "max": 100, "step": 20 }, "power": "", "error": "", "area": "", "optimization":"error", "model" :"modelfile.csv" } -} +} diff --git a/openfasoc/generators/temp-sense-gen/tools/TEMP_netlist.py b/openfasoc/generators/temp-sense-gen/tools/TEMP_netlist.py deleted file mode 100644 index 5a10c82e2..000000000 --- a/openfasoc/generators/temp-sense-gen/tools/TEMP_netlist.py +++ /dev/null @@ -1,75 +0,0 @@ -##for HSPICE netlist -import function - -# import os - - -def gen_modeling_netlist(srcNetlist, dstNetlist, ninv, nhead) -> None: - r_netlist = open(srcNetlist, "r") - lines = list(r_netlist.readlines()) - w_netlist = open(dstNetlist, "w") - - netmap1 = function.netmap() # modify here - netmap1.get_net("x1", None, 1, 1, 1) - netmap1.get_net("n0", None, ninv + 1, ninv + 1, 1) - netmap1.get_net("n1", None, 1, 1, 1) - netmap1.get_net("x2", None, 1, ninv, 1) - netmap1.get_net("n2", None, 1, ninv, 1) - netmap1.get_net("n3", None, 2, ninv + 1, 1) - netmap1.get_net("x3", None, 1, 1, 1) - netmap1.get_net("n4", None, ninv + 1, ninv + 1, 1) - netmap1.get_net("x4", None, 1, nhead, 1) - for line in lines: - netmap1.printline(line, w_netlist) - - -def gen_temp_netlist(ninv, nhead, aux1, aux2, aux3, aux4, aux5, srcDir) -> None: - r_netlist = open(srcDir + "/TEMP_ANALOG_lv.v", "r") - lines = list(r_netlist.readlines()) - w_netlist = open(srcDir + "/TEMP_ANALOG_lv.nl.v", "w") - port = "X" - slc_cell = "SLC a_lc_0(.IN(out), .INB(outb), .VOUT(lc_0));" - - netmap1 = function.netmap() # modify here - netmap1.get_net("nn", None, 1, int(ninv), 1) - netmap1.get_net("n0", None, int(ninv), int(ninv), 1) - netmap1.get_net("na", aux1, 1, 1, 1) - netmap1.get_net("nb", aux2, 0, int(ninv) - 2, 1) - netmap1.get_net("ni", None, 0, int(ninv) - 2, 1) - netmap1.get_net("n1", None, 1, int(ninv) - 1, 1) - netmap1.get_net("n2", None, 2, int(ninv), 1) - netmap1.get_net("ng", aux2, 1, 1, 1) - netmap1.get_net("n3", None, int(ninv), int(ninv), 1) - netmap1.get_net("nk", aux2, 1, 1, 1) - netmap1.get_net("n4", None, int(ninv), int(ninv), 1) - netmap1.get_net("nm", aux2, 1, 1, 1) - netmap1.get_net("np", aux3, 1, 1, 1) - netmap1.get_net("nc", aux3, 1, 1, 1) - netmap1.get_net("nd", aux4, 1, 1, 1) - netmap1.get_net("ne", aux4, 1, 1, 1) - for line in lines: - line = line.replace("nbout", port) - netmap1.printline(line, w_netlist) - - r_netlist = open(srcDir + "/TEMP_ANALOG_hv.v", "r") - lines = list(r_netlist.readlines()) - w_netlist = open(srcDir + "/TEMP_ANALOG_hv.nl.v", "w") - - netmap1.get_net("nf", aux5, 0, int(nhead) - 1, 1) - netmap1.get_net("nh", None, 0, int(nhead) - 1, 1) - netmap1.get_net("no", aux3, 1, 1, 1) - for line in lines: - line = line.replace("SLC", slc_cell) - line = line.replace("nbout", port) - netmap1.printline(line, w_netlist) - - r_netlist = open(srcDir + "/counter_generic.v", "r") - lines = list(r_netlist.readlines()) - w_netlist = open(srcDir + "/counter.v", "w") - - netmap1.get_net("np", aux3, 1, 1, 1) - for line in lines: - line = line.replace("nbout", port) - netmap1.printline(line, w_netlist) - - return diff --git a/openfasoc/generators/temp-sense-gen/tools/TEMP_sensor_template.sp b/openfasoc/generators/temp-sense-gen/tools/TEMP_sensor_template.sp deleted file mode 100644 index e5c34e179..000000000 --- a/openfasoc/generators/temp-sense-gen/tools/TEMP_sensor_template.sp +++ /dev/null @@ -1,7 +0,0 @@ - -.SUBCKT TEMP_sensor EN OUT VPWR VIN VGND -@@ xin@x1 EN n@n0 VGND VGND VIN VIN n@n1 sky130_fd_sc_hd__nand2_1 -@@ xii@x2 n@n2 VGND VGND VIN VIN n@n3 sky130_fd_sc_hd__inv_1 -@@ xib@x3 n@n4 VGND VGND VIN VIN OUT sky130_fd_sc_hd__inv_1 -@@ xih@x4 VGND VIN VGND VPWR HEADER -.ends TEMP_sensor diff --git a/openfasoc/generators/temp-sense-gen/tools/function.py b/openfasoc/generators/temp-sense-gen/tools/function.py deleted file mode 100644 index ef7e50a70..000000000 --- a/openfasoc/generators/temp-sense-gen/tools/function.py +++ /dev/null @@ -1,372 +0,0 @@ -################ modules for HSPICE sim ###################### -############################################################## -######### varmap definition #################### -############################################################## -### This class is to make combinations of given variables #### -### mostly used for testbench generation ################# -### EX: varmap1=HSPICE_varmap.varmap(4) %%num of var=4 ####### -### varmap1.get_var('vdd',1.5,1.8,0.2) %%vdd=1.5:0.2:1.8## -### varmap1.get_var('abc', ........ %%do this for 4 var ## -### varmap1.cal_nbigcy() %%end of var input### -### varmap1.combinate %%returns variable comb 1 by 1 #### -############################################################## - - -class varmap: - # def __init__(self,num_var): - # self.n_smlcycle=1 - # self.last=0 - # self.smlcy=1 - # self.bigcy=0 - # self.vv=0 - # self.vf=1 - # self.size=num_var - # self.map=[None]*self.size - # self.comblist=[None]*self.size - # self.nvar=0 - def __init__(self) -> None: - self.n_smlcycle = 1 - self.last = 0 - self.smlcy = 1 - self.bigcy = 0 - self.vv = 0 - self.vf = 1 - # self.map=[None] - # self.comblist=[None] - self.nvar = 0 - - def get_var(self, name, start, end, step) -> None: - if self.nvar == 0: - self.map = [None] - self.comblist = [None] - else: - self.map.append(None) - self.comblist.append(None) - self.map[self.nvar] = list([name]) - self.comblist[self.nvar] = list([name]) - self.nswp = (end - start) // step + 1 - for i in range(1, self.nswp + 1): - self.map[self.nvar].append(start + step * (i - 1)) - self.nvar += 1 - - def cal_nbigcy(self) -> None: - self.bias = [1] * (len(self.map)) - for j in range(1, len(self.map) + 1): - self.n_smlcycle = self.n_smlcycle * (len(self.map[j - 1]) - 1) - self.n_smlcycle = self.n_smlcycle * len(self.map) - - def increm(self, inc) -> None: # increment bias - self.bias[inc] += 1 - if self.bias[inc] > len(self.map[inc]) - 1: - self.bias[inc] % len(self.map[inc]) - 1 - - def check_end( - self, vf - ) -> int: # When this is called, it's already last stage of self.map[vf] - self.bias[vf] = 1 - # if vf==0 and self.bias[0]==len(self.map[0])-1: - # return 0 - if ( - self.bias[vf - 1] == len(self.map[vf - 1]) - 1 - ): # if previous column is last element - self.check_end(vf - 1) - else: - self.bias[vf - 1] += 1 - return 1 - - def combinate(self) -> None: - # print self.map[self.vv][self.bias[self.vv]] - self.smlcy += 1 - if self.vv == len(self.map) - 1: # last variable - self.bigcy += 1 - for vprint in range(0, len(self.map)): - self.comblist[vprint].append(self.map[vprint][self.bias[vprint]]) - # print self.map[vprint][self.bias[vprint]] - if self.bias[self.vv] == len(self.map[self.vv]) - 1: # last element - if self.smlcy < self.n_smlcycle: - self.check_end(self.vv) - self.vv = (self.vv + 1) % len(self.map) - self.combinate() - else: - pass - else: - self.bias[self.vv] += 1 - self.vv = (self.vv + 1) % len(self.map) - self.combinate() - else: - self.vv = (self.vv + 1) % len(self.map) - self.combinate() - - -############################################################## -######### netmap ######################## -############################################################## -### This class is used for replacing lines ################ -### detects @@ for line and @ for nets ####################### -############################################################## -# -------- EXAMPLE ---------------------------------------# -### netmap1=netmap(2) %input num_var ######################### -### netmap1.get_var('ab','NN',1,4,1) %flag MUST be 2 char #### -## netmap2.get_var('bc','DD',2,5,1) %length of var must match# -# !!caution: do get_var in order, except for lateral prints ## -### which is using @W => varibales here, do get_var at last ## -### for line in r_file.readlines():########################### -### netmap1.printline(line,w_file) ####################### -############################################################## - - -class netmap: - # def __init__(self,num_var): - # self.size=num_var - # self.map=[None]*self.size - # self.flag=[None]*self.size - # self.name=[None]*self.size - # self.nnet=[None]*self.size - # self.nn=0 - # self.pvar=1 - # self.cnta=0 - # self.line_nvar=0 # index of last variable for this line - # self.nxtl_var=0 # index of variable of next line - # self.ci_at=100 - def __init__(self) -> None: - self.nn = 0 - self.pvar = 1 - self.cnta = 0 - self.line_nvar = 0 # index of last variable for this line - self.nxtl_var = 0 # index of variable of next line - self.ci_at = -5 - - def get_net( - self, flag, netname, start, end, step - ) -> None: # if start==None: want to repeat without incrementation(usually for tab) (end)x(step) is the num of repetition - if self.nn == 0: - self.map = [None] - self.flag = [None] - self.name = [None] - self.nnet = [None] - else: - self.map.append(None) - self.name.append(None) - self.flag.append(None) - self.nnet.append(None) - if netname == None: - self.name[self.nn] = 0 - else: - self.name[self.nn] = 1 - self.map[self.nn] = list([netname]) - self.flag[self.nn] = flag - if start != None and start != "d2o": - self.nnet[self.nn] = int((end - start + step / 10) // step + 1) - if self.name[self.nn] == 1: - for i in range(1, self.nnet[self.nn] + 1): - self.map[self.nn].append("") - else: - for i in range(1, self.nnet[self.nn] + 1): - self.map[self.nn].append(start + step * (i - 1)) - elif start == "d2o": - for i in range(0, end): - if step - i > 0: - self.map[self.nn].append(1) - else: - self.map[self.nn].append(0) - i += 1 - else: - self.map[self.nn] = list([netname]) - for i in range(1, step + 1): - self.map[self.nn].append(end) - # self.map[self.nn]=[None]*step - # for i in range(1,self.nnet[self.nn]+1): - # self.map[self.nn][i]=None - self.nn += 1 - # print self.map - - def add_val(self, flag, netname, start, end, step) -> None: - varidx = self.flag.index(flag) - if start != None: - nval = int((end - start + step / 10) // step + 1) - for i in range(1, nval + 1): - self.map[varidx].append(start + step * (i - 1)) - else: - for i in range(1, step + 1): - self.map[varidx].append(end) - - def printline(self, line, wrfile) -> None: - if line[0:2] == "@@": - # print('self.ci_at=%d'%(self.ci_at)) - self.nline = line[3 : len(line)] - self.clist = list(self.nline) # character list - # print(self.clist,self.nxtl_var) - for iv in range(1, len(self.map[self.nxtl_var])): - for ci in range(0, len(self.clist)): - if (ci == self.ci_at + 1 or ci == self.ci_at + 2) and ci != len( - self.clist - ) - 1: - pass - elif self.clist[ci] == "@": - # print self.cnta - self.cnta += 1 - self.line_nvar += 1 - varidx = self.flag.index( - self.clist[ci + 1] + self.clist[ci + 2] - ) - if self.name[varidx]: - wrfile.write(self.map[varidx][0]) - # print(self.map[varidx]) - if type(self.map[varidx][self.pvar]) == float: - wrfile.write( - "%e" % (self.map[varidx][self.pvar]) - ) # modify here!!!! - elif type(self.map[varidx][self.pvar]) == int: - wrfile.write("%d" % (self.map[varidx][self.pvar])) - self.ci_at = ci - elif ci == len(self.clist) - 1: # end of the line - if ( - self.pvar - == len(self.map[self.nxtl_var + self.line_nvar - 1]) - 1 - ): # last element - self.pvar = 1 - self.nxtl_var = self.nxtl_var + self.line_nvar - self.line_nvar = 0 - self.cnta = 0 - self.ci_at = -6 - # print('printed all var for this line, %d'%(ci)) - else: - self.pvar += 1 - # self.line_nvar=self.cnta - self.line_nvar = 0 - # print ('line_nvar= %d'%(self.line_nvar)) - self.cnta = 0 - wrfile.write(self.clist[ci]) - else: - wrfile.write(self.clist[ci]) - elif line[0:2] == "@W": - # print('found word line') - self.nline = line[3 : len(line)] - self.clist = list(self.nline) - for ci in range(0, len(self.clist)): - if ci == self.ci_at + 1 or ci == self.ci_at + 2: - pass - elif self.clist[ci] == "@": - varidx = self.flag.index(self.clist[ci + 1] + self.clist[ci + 2]) - for iv in range(1, len(self.map[varidx])): - if self.name[varidx]: - wrfile.write(self.map[varidx][0]) - wrfile.write("%d " % (self.map[varidx][iv])) - print( - "n is %d, varidx=%d, iv=%d" - % (self.map[varidx][iv], varidx, iv) - ) - self.ci_at = ci - else: - wrfile.write(self.clist[ci]) - self.ci_at = -5 - else: - wrfile.write(line) - - -############################################################## -######### resmap ######################## -############################################################## -### This class is used to deal with results ################ -### detects @@ for line and @ for nets ####################### -############################################################## -# -------- EXAMPLE ---------------------------------------# -### netmap1=netmap(2) %input num_var ######################### -### netmap1.get_var('ab','NN',1,4,1) %flag MUST be 2 char #### -## netmap2.get_var('bc','DD',2,5,1) %length of var must match# -### for line in r_file.readlines():########################### -### netmap1.printline(line,w_file) ####################### -###### self.tb[x][y][env[]]############################### -############################################################## - - -class resmap: - def __init__(self, num_tb, num_words, index) -> None: # num_words includes index - self.tb = [None] * num_tb - self.tbi = [None] * num_tb - self.vl = [None] * num_tb - self.vlinit = [None] * num_tb - self.svar = [None] * num_tb - self.index = index - self.nenv = 0 - self.num_words = num_words - self.vr = [None] * (num_words + index) # one set of variables per plot - self.vidx = [None] * (num_words + index) - self.env = [None] * (num_words + index) - # self.vl=[None]*(num_words+index) #one set of variables per plot - for itb in range(0, len(self.tb)): - # self.tb[itb].vr=[None]*(num_words+index) - self.tbi[itb] = 0 # index for counting vars within tb - self.vl[itb] = [None] * (num_words + index) - self.vlinit[itb] = [0] * (num_words + index) - - def get_var(self, ntb, var) -> None: - self.vr[self.tbi[ntb]] = var - # self.vl[ntb][self.tbi[ntb]]=list([None]) - self.tbi[ntb] += 1 - if self.tbi[ntb] == len(self.vr): # ???????? - self.tbi[ntb] = 0 - - def add(self, ntb, value) -> None: - if self.vlinit[ntb][self.tbi[ntb]] == 0: # initialization - self.vl[ntb][self.tbi[ntb]] = [value] - self.vlinit[ntb][self.tbi[ntb]] += 1 - else: - self.vl[ntb][self.tbi[ntb]].append(value) - self.tbi[ntb] = (self.tbi[ntb] + 1) % len(self.vr) - - def plot_env( - self, ntb, start, step, xvar, xval - ) -> None: # setting plot environment: if ntb=='all': x axis is in terms of testbench - if ntb == "all": - self.nenv += 1 - self.xaxis = [None] * len(self.tb) - for i in range(0, len(self.tb)): - self.xaxis[i] = start + i * step - self.vidx[self.nenv] = self.vr.index(xvar) - # print self.vl[0][self.vidx[self.nenv]] - print("", self.vl[0][self.vidx[self.nenv]]) - self.env[self.nenv] = [ - i - for (i, x) in enumerate(self.vl[0][self.vidx[self.nenv]]) - if x == "%s" % (xval) - ] - else: - self.nenv += 1 - self.xaxis = [None] # one output - self.xaxis = [start] - self.vidx[self.nenv] = self.vr.index(xvar) - self.env[self.nenv] = [ - i - for (i, x) in enumerate(self.vl[0][self.vidx[self.nenv]]) - if x == "%s" % (xval) - ] - - def rst_env(self) -> None: - self.vidx[self.nenv] = None - self.env[self.nenv] = 0 - self.nenv = 0 - # print self.vl[0][self.vidx[self.nenv]] - - def plot_y(self, yvar) -> None: - self.yidx = self.vr.index(yvar) - print("yidx=%d" % (self.yidx)) - # print self.vl[0][self.yidx][self.env[self.nenv][0]] - print("", self.vl[0][self.yidx][self.env[self.nenv][0]]) - self.yaxis = [None] * len(self.xaxis) - for xx in range(0, len(self.xaxis)): - self.yaxis[xx] = self.vl[xx][self.yidx][self.env[self.nenv][0]] - # plt.plot(self.xaxis,self.yaxis) - # plt.ylabel(self.vr[self.yidx]) - - def sort(self, var) -> None: - varidx = self.vr.index(var) - for k in range(len(self.vl)): # all testbenches - self.svar[k] = {} # define dict - for i in range(len(self.vl[0][0])): # all values - self.svar[k][self.vl[k][varidx][i]] = [] - for j in range(len(self.vr)): # all variables - if j != varidx: - self.svar[k][self.vl[k][varidx][i]].append(self.vl[k][j][i]) - # if k==0: - # print self.svar[k] diff --git a/openfasoc/generators/temp-sense-gen/tools/readparamgen.py b/openfasoc/generators/temp-sense-gen/tools/readparamgen.py index d5f878625..f33b88300 100644 --- a/openfasoc/generators/temp-sense-gen/tools/readparamgen.py +++ b/openfasoc/generators/temp-sense-gen/tools/readparamgen.py @@ -5,7 +5,6 @@ import os import re import sys -import time import matplotlib.pyplot as plt import pandas as pd @@ -81,6 +80,7 @@ try: Tempmin = jsonSpec["specifications"]["temperature"]["min"] Tempmax = jsonSpec["specifications"]["temperature"]["max"] + Tempstep = jsonSpec["specifications"]["temperature"]["step"] except KeyError as e: print( "Error: Bad Input Specfile. 'range o' value is missing under " @@ -175,6 +175,7 @@ Tmin = float(jsonSpec["specifications"]["temperature"]["min"]) Tmax = float(jsonSpec["specifications"]["temperature"]["max"]) +Tstep = float(jsonSpec["specifications"]["temperature"]["step"]) if (Tmax > 100) or (Tmin < -20): print( "Error: Supported temperature sensing must be inside the following range [-20 to 100] Celcius" diff --git a/openfasoc/generators/temp-sense-gen/tools/result.py b/openfasoc/generators/temp-sense-gen/tools/result.py deleted file mode 100644 index c02e14b5c..000000000 --- a/openfasoc/generators/temp-sense-gen/tools/result.py +++ /dev/null @@ -1,53 +0,0 @@ -import argparse -import re -import sys - -parser = argparse.ArgumentParser(description="parse simulators' output file") -parser.add_argument("--tool", "-t", required=True, help="simulator type") -parser.add_argument( - "--inputFile", "-i", required=True, help="simulator's output for processing" -) -args = parser.parse_args() - -file_name = args.inputFile -tool_name = args.tool - -with open(file_name, "r") as rf: - log_file_text = rf.read() - - temp_patten = "TEMP\s*=\s*([0-9\-\.]+)" - temp_value_re = re.search(temp_patten, log_file_text) - temp_value = "failed" - if temp_value_re: - temp_value = temp_value_re.group(1) - else: - temp_value = file_name.split("_")[-1].split(".")[0] - - period_pattern = "PERIOD\s*=\s*([0-9\.e-]+)" - period_pattern_re = re.search(period_pattern, log_file_text) - period_value = "failed" - if period_pattern_re: - period_value = period_pattern_re.group(1) - else: - period_pattern = "period\s*=\s*([0-9\.e-]+)" - period_pattern_re = re.search(period_pattern, log_file_text) - period_value = "failed" - if period_pattern_re: - period_value = period_pattern_re.group(1) - - power_pattern = "POWER\s*=\s*([0-9\.e-]+)" - power_pattern_re = re.search(power_pattern, log_file_text) - power_value = "failed" - if power_pattern_re: - power_value = power_pattern_re.group(1) - else: - power_pattern = "power\s*=\s*([0-9\.e-]+)" - power_pattern_re = re.search(power_pattern, log_file_text) - power_value = "failed" - if power_pattern_re: - power_value = power_pattern_re.group(1) - - print( - "%s %s %s" % (temp_value, period_value, power_value), - file=open("sim_output", "a"), - ) diff --git a/openfasoc/generators/temp-sense-gen/tools/result_error.py b/openfasoc/generators/temp-sense-gen/tools/result_error.py deleted file mode 100644 index 857c8edf9..000000000 --- a/openfasoc/generators/temp-sense-gen/tools/result_error.py +++ /dev/null @@ -1,96 +0,0 @@ -import argparse -import math -import os -import sys - -parser = argparse.ArgumentParser(description="calculate error values") -parser.add_argument( - "--mode", - "-m", - required=True, - help="simulation mode, full: skip power, partial: extract power", -) -args = parser.parse_args() - - -simDir = os.path.dirname(os.path.relpath(__file__)) -sim_output_file = open(simDir + "sim_output", "r") - -data0 = [] -temp = [] -power_list = [] -sim_output_lines = sim_output_file.readlines() -for line in sim_output_lines: - data = line.split() - temp.append(data[0]) - data0.append(data[1]) - if args.mode == "partial": - power_list.append(data[2]) - -frequency_list = [] -data1 = list() -for i, val in enumerate(data0): - if val == "failed": - val_cal = "failed" - frequency = "failed" - else: - frequency = 1 / float(val) - print("temp: %s, \tfrequency: %f" % (temp[i], frequency)) - val_cal = math.log(1 / float(val)) * ((-20 + i * 20) + 273.15) * 0.01 - frequency_list.append(frequency) - data1.append(val_cal) - -data2 = [] - -try: - slope_f = 80 / (data1[len(data1) - 2] - data1[1]) -except: - print("Error calculation failed") - sys.exit(0) - -for k in data1: - if k == "failed": - val = "failed" - else: - val = k * slope_f - data2.append(val) - -data3 = [] -offset = data2[1] -for val in data2: - if val == "failed": - val = "failed" - else: - val = val - offset - data3.append(val) - -data4 = [] -for i, val in enumerate(data3): - if val == "failed": - val = "failed" - else: - val = (-20 + i * 20) - val - data4.append(val) - - -print(os.getcwd(), file=open("all_result", "a")) -if args.mode == "partial": - print("Temp Frequency Power Error", file=open("all_result", "a")) -elif args.mode == "full": - print("Temp Frequency Error", file=open("all_result", "a")) -else: - print("simulation mode - " + args.mode + " is not supported") - sys.exit(1) -for idx, line in enumerate(sim_output_lines): - result_list = line.split() - if args.mode == "partial": - print( - "%s %s %s %s" - % (result_list[0], frequency_list[idx], power_list[idx], data4[idx]), - file=open("all_result", "a"), - ) - elif args.mode == "full": - print( - "%s %s %s" % (result_list[0], frequency_list[idx], data4[idx]), - file=open("all_result", "a"), - ) diff --git a/openfasoc/generators/temp-sense-gen/tools/simulation.py b/openfasoc/generators/temp-sense-gen/tools/simulation.py index a92e1686d..d20339360 100644 --- a/openfasoc/generators/temp-sense-gen/tools/simulation.py +++ b/openfasoc/generators/temp-sense-gen/tools/simulation.py @@ -1,32 +1,33 @@ -import glob import os -import re, math +import re import shutil -import subprocess as sp import sys from itertools import product -import TEMP_netlist +from simulation_result import get_sim_results, calculate_sim_error + +# TODO: Find a better way to import modules from parent directory +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +from common.simulation import run_simulations # note netlist type is either prePEX or postPEX # function returns the location of simulations def generate_runs( - genDir, - designName, - headerList, - invList, - tempList, - jsonConfig, - platform, - mode, - pdk, - modeling=False, - spiceDir=None, - prePEX=True, -): + genDir: str, + designName: str, + headerList: list[int], + invList: list[int], + tempStart: float, + tempStop: float, + tempStep: float, + jsonConfig: dict, + platform: str, + pdk: str, + spiceDir: str | None = None, + prePEX: bool = True, +) -> str: """creates and executes simulations (through run_simulations call)""" simDir = genDir + "simulations/" - flowDir = genDir + "flow/" platformConfig = jsonConfig["platforms"][platform] simTool = jsonConfig["simTool"] model_file = pdk + "/libs.tech/ngspice/sky130.lib.spice" @@ -34,121 +35,58 @@ def generate_runs( if not spiceDir: spiceDir = genDir + "/work" - platformSpice = glob.glob( - genDir + "../../common/platforms/%s/cdl/*.spice" % (platform) - ) - designList = list(product(headerList, invList)) if not os.path.isdir(simDir + "run/"): os.mkdir(simDir + "run/") - # Update simulation testbench, depending on platform config and if running modeling - with open(simDir + "templates/tempsenseInst_%s.sp" % (simTool), "r") as rf: - simTestbench = rf.read() - simTestbench = re.sub("@model_file", model_file, simTestbench) - simTestbench = re.sub( - "@model_corner", platformConfig["model_corner"], simTestbench - ) - simTestbench = re.sub( - "@voltage", str(platformConfig["nominal_voltage"]), simTestbench - ) - - if not modeling: - simTestbench = re.sub("\*@verification", "", simTestbench) - if jsonConfig["simMode"] == "full": - simTestbench = re.sub("\*@full", "", simTestbench) - elif jsonConfig["simMode"] == "partial": - simTestbench = re.sub("\*@partial", "", simTestbench) - else: - print("simulation mode - " + jsonConfig["simMode"] + "is not supported") - sys.exit(1) - else: - simTestbench = re.sub("\*@modeling", "", simTestbench) - simTestbench = re.sub("\*@partial", "", simTestbench) - # Iterate over runs for design in designList: header = design[0] inv = design[1] - if prePEX: - runDir = simDir + "run/prePEX_inv{:d}_header{:d}/".format(inv, header) - else: - runDir = simDir + "run/PEX_inv{:d}_header{:d}/".format(inv, header) - - if os.path.isdir(runDir): - shutil.rmtree(runDir, ignore_errors=True) - os.mkdir(runDir) - shutil.copyfile(genDir + "tools/result.py", runDir + "result.py") - shutil.copyfile(genDir + "tools/result_error.py", runDir + "result_error.py") - - if modeling: - srcNetlist = genDir + "tools/TEMP_sensor_template.sp" - dstNetlist = runDir + "TEMP_sensor_inv%d_header%d.spice" % (inv, header) - simTestbench = re.sub( - "@netlist", - os.path.abspath( - runDir + "TEMP_sensor_inv%d_header%d.spice" % (inv, header) - ), - simTestbench, - ) - TEMP_netlist.gen_modeling_netlist(srcNetlist, dstNetlist, inv, header) - with open(dstNetlist, "r") as rf: - filedata = rf.read() - for spice in platformSpice: - filedata = ".INCLUDE '%s'\n" % os.path.abspath(spice) + filedata - with open(dstNetlist, "w") as wf: - filedata = wf.write(filedata) - else: - if prePEX: - srcNetlist = spiceDir + "/" + designName + ".spice" - else: - srcNetlist = spiceDir + "/" + designName + "_pex.spice" - dstNetlist = runDir + designName + ".spice" - simTestbench = re.sub( - "@netlist", - os.path.abspath(runDir + designName + ".spice"), - simTestbench, - ) - update_netlist(srcNetlist, dstNetlist, jsonConfig["simMode"]) - - for temp in tempList: - w_file = open(runDir + "/%s_sim_%d.sp" % (designName, temp), "w") - wfdata = re.sub("@temp", str(temp), simTestbench) - wfdata = re.sub("@design_nickname", designName, wfdata) - if jsonConfig["simTool"] == "xyce": - sim_end = round(math.pow(10, -3) * 800 / math.exp(0.04 * temp), 4) - wfdata = re.sub("@sim_end", str(sim_end), wfdata) - w_file.write(wfdata) - w_file.close() + runDir = "run/{:s}_inv{:d}_header{:d}/".format("prePEX" if prePEX else "PEX", inv, header) + runDirPath = os.path.join(simDir, runDir) + + if os.path.isdir(runDirPath): + shutil.rmtree(runDirPath, ignore_errors=True) + os.mkdir(runDirPath) + + srcNetlist = spiceDir + "/" + designName + (".spice" if prePEX else "_pex.spice") + dstNetlist = os.path.join(runDirPath, designName + ".spice") + + update_netlist(srcNetlist, dstNetlist, jsonConfig["simMode"]) + + num_simulations = run_simulations( + parameters={ + 'temp': {'start': tempStart, 'end': tempStop, 'step': tempStep}, + 'model_file': model_file, + 'model_corner': platformConfig['model_corner'], + 'nominal_voltage': platformConfig['nominal_voltage'], + 'design_name': designName + }, + platform=platform, + simulation_dir=simDir, + template_path=os.path.join("templates", f"tempsenseInst_{simTool}.sp"), + runs_dir=runDir, + sim_tool=simTool, + netlist_path=dstNetlist + ) - # runs simulation only if mode is set to "full" - if mode == "full": - print( - "#----------------------------------------------------------------------" - ) - print("# Running {0} Simulations".format("prePEX" if prePEX else "PEX")) - print( - "#----------------------------------------------------------------------" - ) + # Calculating simulation results and error + with open(os.path.join(runDirPath, 'sim_output'), 'w') as sim_output_file: + for i in range(num_simulations): + log_file_path = os.path.join(runDirPath, f"{i + 1}", f"sim_{i + 1}.log") + sim_results = get_sim_results(open(log_file_path, "r").read()) - run_simulations( - runDir, - designName, - tempList, - jsonConfig["simTool"], - jsonConfig["simMode"], - ) - else: - print( - "spice netlists created for different temperatures to run the {0} simulations".format( - "prePEX" if prePEX else "PEX" - ) - ) + sim_output_file.write(f"{sim_results['temp']} {sim_results['period']} {sim_results['power']}\n") - return runDir + with open(os.path.join(runDirPath, 'sim_output'), 'r') as sim_output_file: + with open(os.path.join(runDirPath, 'all_result'), 'w') as all_result_file: + error_data = calculate_sim_error(sim_output_lines=sim_output_file.readlines()) + all_result_file.write("\n".join(error_data)) + return runDirPath def matchNetlistCell(cell_instantiation): """returns true if the input contains as a pin (as a substring) one of the identified cells to remove for partial simulations""" @@ -231,175 +169,4 @@ def update_netlist(srcNetlist, dstNetlist, simMode): """ netlist = netlist.replace(toplevel_pinout.split(" ", 2)[2], standardized_pinout) with open(dstNetlist, "w") as wf: - wf.write(netlist) - - -def run_simulations(runDir, designName, temp_list, simTool, simMode) -> None: - if simTool == "finesim": - with open(runDir + "run_sim", "w") as wf: - for temp in temp_list: - wf.write( - "finesim -spice %s_sim_%d.sp -o %s_sim_%d &\n" - % (designName, temp, designName, temp) - ) - - with open(runDir + "cal_result", "w") as wf: - for temp in temp_list: - wf.write( - "python result.py --tool finesim --inputFile %s_sim_%d.log\n" - % (designName, temp) - ) - wf.write("python result_error.py --mode %s\n" % (simMode)) - - processes = [] - for temp in temp_list: - p = sp.Popen( - [ - "finesim", - "-spice", - "%s_sim_%d.sp" % (designName, temp), - "-o", - "%s_sim_%d" % (designName, temp), - ], - cwd=runDir, - ) - processes.append(p) - - for p in processes: - p.wait() - - for temp in temp_list: - if not os.path.isfile(runDir + "%s_sim_%d.log" % (designName, temp)): - print( - "simulation output: %s_sim_%d.log is not generated" - % (designName, temp) - ) - sys.exit(1) - p = sp.Popen( - [ - "python", - "result.py", - "--tool", - "finesim", - "--inputFile", - "%s_sim_%d.log" % (designName, temp), - ], - cwd=runDir, - ) - p.wait() - - p = sp.Popen(["python", "result_error.py", "--mode", simMode], cwd=runDir) - p.wait() - - elif simTool == "ngspice": - with open(runDir + "run_sim", "w") as wf: - for temp in temp_list: - wf.write( - "ngspice -b -o %s_sim_%d.log %s_sim_%d.sp\n" - % (designName, temp, designName, temp) - ) - - with open(runDir + "cal_result", "w") as wf: - for temp in temp_list: - wf.write( - "python result.py --tool ngspice --inputFile %s_sim_%d.log\n" - % (designName, temp) - ) - wf.write("python result_error.py --mode %s\n" % (simMode)) - - processes = [] - for temp in temp_list: - p = sp.Popen( - [ - "ngspice", - "-b", - "-o", - "%s_sim_%d.log" % (designName, temp), - "%s_sim_%d.sp" % (designName, temp), - ], - cwd=runDir, - ) - processes.append(p) - - for p in processes: - p.wait() - - for temp in temp_list: - if not os.path.isfile(runDir + "%s_sim_%d.log" % (designName, temp)): - print( - "simulation output: %s_sim_%d.log is not generated" - % (designName, temp) - ) - sys.exit(1) - p = sp.Popen( - [ - "python", - "result.py", - "--tool", - "ngspice", - "--inputFile", - "%s_sim_%d.log" % (designName, temp), - ], - cwd=runDir, - ) - p.wait() - - p = sp.Popen(["python", "result_error.py", "--mode", simMode], cwd=runDir) - p.wait() - - elif simTool == "xyce": - with open(runDir + "run_sim", "w") as wf: - for temp in temp_list: - wf.write( - "xyce -l %s_sim_%d.log -o %s_sim_%d %s_sim_%d.sp\n" - % (designName, temp, designName, temp, designName, temp) - ) - - with open(runDir + "cal_result", "w") as wf: - for temp in temp_list: - wf.write( - "python result.py --tool xyce --inputFile %s_sim_%d.mt0\n" - % (designName, temp) - ) - wf.write("python result_error.py --mode %s\n" % (simMode)) - - processes = [] - for temp in temp_list: - p = sp.Popen( - [ - "/opt/xyce/xyce_serial/bin/Xyce", - "-l", - "%s_sim_%d.log" % (designName, temp), - "-o", - "%s_sim_%d" % (designName, temp), - "%s_sim_%d.sp" % (designName, temp), - ], - cwd=runDir, - ) - processes.append(p) - - for p in processes: - p.wait() - - for temp in temp_list: - if not os.path.isfile(runDir + "%s_sim_%d.mt0" % (designName, temp)): - print( - "simulation output: %s_sim_%d.mt0 is not generated" - % (designName, temp) - ) - sys.exit(1) - p = sp.Popen( - [ - "python", - "result.py", - "--tool", - "xyce", - "--inputFile", - "%s_sim_%d.mt0" % (designName, temp), - ], - cwd=runDir, - ) - p.wait() - - p = sp.Popen(["python", "result_error.py", "--mode", simMode], cwd=runDir) - p.wait() + wf.write(netlist) \ No newline at end of file diff --git a/openfasoc/generators/temp-sense-gen/tools/simulation_result.py b/openfasoc/generators/temp-sense-gen/tools/simulation_result.py new file mode 100644 index 000000000..cc169a62e --- /dev/null +++ b/openfasoc/generators/temp-sense-gen/tools/simulation_result.py @@ -0,0 +1,93 @@ +import re +import sys +import math + +def get_value(log_file_text, key): + """Finds a value in the simulation log file. + + Finds and returns the value from a statement of the format key = value. + """ + pattern = f"{key}\s*=\s*([0-9\.e-]+)" + pattern_re = re.search(pattern, log_file_text, flags=re.IGNORECASE) + value = "failed" + + if pattern_re: + value = pattern_re.group(1) + + return value + +def get_sim_results(log_file_text): + temp_value = get_value(log_file_text=log_file_text, key="temp") + period_value = get_value(log_file_text=log_file_text, key="period") + power_value = get_value(log_file_text=log_file_text, key="power") + + return { + 'temp': temp_value, + 'period': period_value, + 'power': power_value + } + +def calculate_sim_error(sim_output_lines: list[str]): + temp_list = [] + period_list = [] + power_list = [] + + for line in sim_output_lines: + data = line.split() + temp_list.append(float(data[0])) + period_list.append(float(data[1])) + power_list.append(float(data[2])) + + frequency_list = [] + data1 = [] + + for i, x in enumerate(period_list): + if x == "failed": + x = "failed" + frequency = "failed" + else: + frequency = 1 / float(x) + print("temp: %f, \tfrequency: %f" % (temp_list[i], frequency)) + + x = math.log(1 / float(x)) * (temp_list[i] + 273.15) * 0.01 + + frequency_list.append(frequency) + data1.append(x) + + try: + slope_f = (temp_list[-2] - temp_list[1]) / (data1[-2] - data1[1]) + except: + print("Error calculation failed") + sys.exit(1) + + data2 = [] + for data in data1: + if data == "failed": + x = "failed" + else: + x = data * slope_f + data2.append(x) + + data3 = [] + offset = data2[0] - temp_list[0] + for val in data2: + if val == "failed": + val = "failed" + else: + val = val - offset + data3.append(val) + + error_list = [] + for i, x in enumerate(data3): + if x == "failed": + error = "failed" + else: + error = temp_list[i] - x + error_list.append(error) + + error_data: list[str] = [] + error_data.append("Temp Frequency Power Error") + for idx, temp in enumerate(temp_list): + error_data.append(f"{temp} {frequency_list[idx]} {power_list[idx]} {error_list[idx]}") + + return error_data diff --git a/openfasoc/generators/temp-sense-gen/tools/temp-sense-gen.py b/openfasoc/generators/temp-sense-gen/tools/temp-sense-gen.py index ab31b61e0..7d5d48aa8 100755 --- a/openfasoc/generators/temp-sense-gen/tools/temp-sense-gen.py +++ b/openfasoc/generators/temp-sense-gen/tools/temp-sense-gen.py @@ -7,7 +7,7 @@ import sys import re -from readparamgen import args, check_search_done, designName +from readparamgen import args, check_search_done, designName, Tmin, Tmax, Tstep from simulation import generate_runs # TODO: Find a better way to import modules from parent directory @@ -192,12 +192,6 @@ os.mkdir(args.outputDir) outputDir = args.outputDir -# print("genDir + args.outputDir: {}".format(genDir + args.outputDir)) -# print("flowDir: {}".format(flowDir)) -# print("args.platform: {}".format(args.platform)) -# print("designName: {}".format(designName)) -# subprocess.run(["ls", "-l", flowDir, "results/", args.platform, "/tempsense"]) - shutil.copyfile( flowDir + "results/" + args.platform + "/tempsense/6_final.gds", outputDir + "/" + designName + ".gds", @@ -231,83 +225,41 @@ outputDir + "/6_final_lvs.rpt", ) - print("#----------------------------------------------------------------------") print("# Macro Generated") print("#----------------------------------------------------------------------") print() - -print("#----------------------------------------------------------------------") -print("# Generating spice netlists for the macro") -print("#----------------------------------------------------------------------") - -stage_var = [int(ninv)] -header_var = [int(nhead)] - -# make a temp list, TODO: get from JSON config -temp_start = -20 -temp_stop = 100 -temp_step = 20 -temp_points = int((temp_stop - temp_start) / temp_step) -temp_list = [] -for i in range(0, temp_points + 1): - temp_list.append(temp_start + i * temp_step) - -# run PEX and/or prePEX simulations based on the command line flags -if args.prepex: - prepexDir = generate_runs( - genDir, - designName, - header_var, - stage_var, - temp_list, - jsonConfig, - args.platform, - args.mode, - pdk, - spiceDir=args.outputDir, - prePEX=True, - ) - if args.mode == "full": - if os.path.isfile(prepexDir + "all_result"): - shutil.copyfile( - prepexDir + "all_result", genDir + args.outputDir + "/prePEX_sim_result" - ) - else: - print(prepexDir + "prePEX all_result file is not generated successfully") - sys.exit(1) - -if args.pex: - pexDir = generate_runs( - genDir, - designName, - header_var, - stage_var, - temp_list, - jsonConfig, - args.platform, - args.mode, - pdk, - spiceDir=args.outputDir, - prePEX=False, - ) - - if args.mode == "full": - if os.path.isfile(pexDir + "all_result"): - shutil.copyfile( - pexDir + "all_result", genDir + args.outputDir + "/PEX_sim_result" - ) - else: - print(pexDir + "PEX all_result file is not generated successfully") - sys.exit(1) - - if args.mode == "full": print("#----------------------------------------------------------------------") - print("# Simulation output Generated") + print("# Running simulations.") print("#----------------------------------------------------------------------") + stage_var = [int(ninv)] + header_var = [int(nhead)] + + # run PEX and/or prePEX simulations based on the command line flags + sim_output_dir = generate_runs( + genDir=genDir, + designName=designName, + headerList=header_var, + invList=stage_var, + tempStart=Tmin, + tempStop=Tmax, + tempStep=Tstep, + jsonConfig=jsonConfig, + platform=args.platform, + pdk=pdk, + spiceDir=args.outputDir, + prePEX=args.prepex, + ) + if os.path.isfile(sim_output_dir + "all_result"): + shutil.copyfile( + sim_output_dir + "all_result", genDir + args.outputDir + f"/{'prePEX' if args.prepex else 'PEX'}_sim_result" + ) + else: + print(sim_output_dir + f"{'prePEX' if args.prepex else 'PEX'} all_result file is not generated successfully") + sys.exit(1) print("Exiting tool....") exit() diff --git a/tests/common_api/test-simulation/expected/1/sim_1.sp b/tests/common_api/test-simulation/expected/1/sim_1.sp new file mode 100644 index 000000000..b82cb6986 --- /dev/null +++ b/tests/common_api/test-simulation/expected/1/sim_1.sp @@ -0,0 +1,19 @@ + + +.OPTION temp = -5 + +.subckt TEST_NETLIST V1 V2 +C1 V1 V0 C=5 +R1 V0 V2 R=5k +.ends + +X0 V1 0 TEST_NETLIST + +VPOWER V1 0 AC SIN(0 1 1k 0 0 0) + +.TRAN 0.01 1 + +.MEAS tran vrms RMS par('V(V1)') +.MEAS tran irms RMS par('I(VPOWER)') + +.END diff --git a/tests/common_api/test-simulation/expected/2/sim_2.sp b/tests/common_api/test-simulation/expected/2/sim_2.sp new file mode 100644 index 000000000..4c0802904 --- /dev/null +++ b/tests/common_api/test-simulation/expected/2/sim_2.sp @@ -0,0 +1,19 @@ + + +.OPTION temp = 5 + +.subckt TEST_NETLIST V1 V2 +C1 V1 V0 C=5 +R1 V0 V2 R=5k +.ends + +X0 V1 0 TEST_NETLIST + +VPOWER V1 0 AC SIN(0 1 1k 0 0 0) + +.TRAN 0.01 1 + +.MEAS tran vrms RMS par('V(V1)') +.MEAS tran irms RMS par('I(VPOWER)') + +.END diff --git a/tests/common_api/test-simulation/expected/3/sim_3.sp b/tests/common_api/test-simulation/expected/3/sim_3.sp new file mode 100644 index 000000000..9e4d6f067 --- /dev/null +++ b/tests/common_api/test-simulation/expected/3/sim_3.sp @@ -0,0 +1,19 @@ + + +.OPTION temp = -5 + +.subckt TEST_NETLIST V1 V2 +C1 V1 V0 C=5 +R1 V0 V2 R=5k +.ends + +X0 V1 0 TEST_NETLIST + +VPOWER V1 0 AC SIN(0 10 1k 0 0 0) + +.TRAN 0.01 1 + +.MEAS tran vrms RMS par('V(V1)') +.MEAS tran irms RMS par('I(VPOWER)') + +.END diff --git a/tests/common_api/test-simulation/expected/4/sim_4.sp b/tests/common_api/test-simulation/expected/4/sim_4.sp new file mode 100644 index 000000000..4280cc6f0 --- /dev/null +++ b/tests/common_api/test-simulation/expected/4/sim_4.sp @@ -0,0 +1,19 @@ + + +.OPTION temp = 5 + +.subckt TEST_NETLIST V1 V2 +C1 V1 V0 C=5 +R1 V0 V2 R=5k +.ends + +X0 V1 0 TEST_NETLIST + +VPOWER V1 0 AC SIN(0 10 1k 0 0 0) + +.TRAN 0.01 1 + +.MEAS tran vrms RMS par('V(V1)') +.MEAS tran irms RMS par('I(VPOWER)') + +.END diff --git a/tests/common_api/test-simulation/expected/5/sim_5.sp b/tests/common_api/test-simulation/expected/5/sim_5.sp new file mode 100644 index 000000000..1fe61045f --- /dev/null +++ b/tests/common_api/test-simulation/expected/5/sim_5.sp @@ -0,0 +1,19 @@ + + +.OPTION temp = -5 + +.subckt TEST_NETLIST V1 V2 +C1 V1 V0 C=5 +R1 V0 V2 R=5k +.ends + +X0 V1 0 TEST_NETLIST + +VPOWER V1 0 AC SIN(0 1 10k 0 0 0) + +.TRAN 0.01 1 + +.MEAS tran vrms RMS par('V(V1)') +.MEAS tran irms RMS par('I(VPOWER)') + +.END diff --git a/tests/common_api/test-simulation/expected/6/sim_6.sp b/tests/common_api/test-simulation/expected/6/sim_6.sp new file mode 100644 index 000000000..2507e0dda --- /dev/null +++ b/tests/common_api/test-simulation/expected/6/sim_6.sp @@ -0,0 +1,19 @@ + + +.OPTION temp = 5 + +.subckt TEST_NETLIST V1 V2 +C1 V1 V0 C=5 +R1 V0 V2 R=5k +.ends + +X0 V1 0 TEST_NETLIST + +VPOWER V1 0 AC SIN(0 1 10k 0 0 0) + +.TRAN 0.01 1 + +.MEAS tran vrms RMS par('V(V1)') +.MEAS tran irms RMS par('I(VPOWER)') + +.END diff --git a/tests/common_api/test-simulation/expected/7/sim_7.sp b/tests/common_api/test-simulation/expected/7/sim_7.sp new file mode 100644 index 000000000..bc9e2c3c6 --- /dev/null +++ b/tests/common_api/test-simulation/expected/7/sim_7.sp @@ -0,0 +1,19 @@ + + +.OPTION temp = -5 + +.subckt TEST_NETLIST V1 V2 +C1 V1 V0 C=5 +R1 V0 V2 R=5k +.ends + +X0 V1 0 TEST_NETLIST + +VPOWER V1 0 AC SIN(0 10 10k 0 0 0) + +.TRAN 0.01 1 + +.MEAS tran vrms RMS par('V(V1)') +.MEAS tran irms RMS par('I(VPOWER)') + +.END diff --git a/tests/common_api/test-simulation/expected/8/sim_8.sp b/tests/common_api/test-simulation/expected/8/sim_8.sp new file mode 100644 index 000000000..0f7bbc084 --- /dev/null +++ b/tests/common_api/test-simulation/expected/8/sim_8.sp @@ -0,0 +1,19 @@ + + +.OPTION temp = 5 + +.subckt TEST_NETLIST V1 V2 +C1 V1 V0 C=5 +R1 V0 V2 R=5k +.ends + +X0 V1 0 TEST_NETLIST + +VPOWER V1 0 AC SIN(0 10 10k 0 0 0) + +.TRAN 0.01 1 + +.MEAS tran vrms RMS par('V(V1)') +.MEAS tran irms RMS par('I(VPOWER)') + +.END diff --git a/tests/common_api/test-simulation/src/template.sp b/tests/common_api/test-simulation/src/template.sp new file mode 100644 index 000000000..5b1d5b006 --- /dev/null +++ b/tests/common_api/test-simulation/src/template.sp @@ -0,0 +1,17 @@ +.OPTION temp = ${temp} + +.subckt TEST_NETLIST V1 V2 +C1 V1 V0 C=5 +R1 V0 V2 R=5k +.ends + +X0 V1 0 ${netlist_subckt_name} + +VPOWER V1 0 AC SIN(0 ${Vhigh} ${freq} 0 0 0) + +.TRAN 0.01 ${end_time} + +.MEAS tran vrms RMS par('V(V1)') +.MEAS tran irms RMS par('I(VPOWER)') + +.END diff --git a/tests/common_api/test_simulation.py b/tests/common_api/test_simulation.py new file mode 100644 index 000000000..b5947f5d2 --- /dev/null +++ b/tests/common_api/test_simulation.py @@ -0,0 +1,102 @@ +import sys +import os +import re +import pytest +from shutil import rmtree + +# Add the common API to the path +# TODO: Find a better way to import the modules +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'openfasoc', 'generators')) + +from common.simulation import run_simulations + +TEST_SIMULATION_DIR = os.path.join(os.path.dirname(__file__), 'test-simulation') +EXPECTED_CONFIGS_DIR = os.path.join(TEST_SIMULATION_DIR, 'expected') +RUNS_DIR = os.path.join(TEST_SIMULATION_DIR, 'runs') + +PARAMS = { + 'temp': {'start': -5, 'end': 5, 'step': 10}, + 'netlist_subckt_name': 'TEST_NETLIST', + 'Vhigh': [1, 10], + 'end_time': 1, + 'freq': ['1k', '10k'] +} + +EXPECTED_NUM_CONFIGS = 8 + +# From the temperature senssor generator. +def get_value(log_file_text, key): + """Finds a value in the simulation log file. + + Finds and returns the value from a statement of the format key = value. + """ + pattern = f"{key}\s*=\s*([0-9\.e-]+)" + pattern_re = re.search(pattern, log_file_text, flags=re.IGNORECASE) + value = "NOT_FOUND" + + if pattern_re: + value = pattern_re.group(1) + + return value + +def isfloat(num: str): + """Checks if a string represents a float.""" + try: + float(num) + return True + + except ValueError: + return False + +@pytest.fixture(autouse=True) +def run_before_and_after_tests(): + """Cleans the runs directory before and after tests.""" + if os.path.exists(RUNS_DIR): + rmtree(RUNS_DIR) + + yield + + if os.path.exists(RUNS_DIR): + rmtree(RUNS_DIR) + +def test_simulations(): + num_runs = run_simulations( + parameters=PARAMS, + platform = "", + simulation_dir = TEST_SIMULATION_DIR, + template_path = os.path.join(TEST_SIMULATION_DIR, 'src', 'template.sp'), + num_concurrent_sims = 8 + ) + + # Check if the correct number of configurations are generated + assert num_runs == EXPECTED_NUM_CONFIGS, "The number of runs does not match the expected number." + assert len(os.listdir(RUNS_DIR)) == EXPECTED_NUM_CONFIGS, "The number of generated configurations does not match the expected number." + + + for i in range(1, EXPECTED_NUM_CONFIGS + 1): + expected_spice_path = os.path.join(EXPECTED_CONFIGS_DIR, str(i), f"sim_{i}.sp") + + run_dir = os.path.join(RUNS_DIR, str(i)) + generated_spice_path = os.path.join(run_dir, f"sim_{i}.sp") + generated_log_file_path = os.path.join(run_dir, f"sim_{i}.log") + parameters_file_path = os.path.join(run_dir, f"parameters.txt") + + # Check if all the correct files are generated + assert os.path.exists(generated_spice_path), f"SPICE file is not generated for config #{i}" + assert os.path.exists(generated_log_file_path), f"Simulation log file is not generated for config #{i}" + assert os.path.exists(parameters_file_path), f"parameters.txt file is not generated for config #{i}" + + # Check if the generated spice matches + with open(expected_spice_path) as expected_spice: + with open(generated_spice_path) as generated_spice: + assert generated_spice.read() == expected_spice.read(), f"Generated SPICE does not match the expected SPICE for config #{i}" + + # Check if the simulations are run correctly + with open(generated_log_file_path) as log_file: + log_file_text = log_file.read() + # 1. Search for vrms and irms values + vrms_value = get_value(log_file_text, 'vrms') + irms_value = get_value(log_file_text, 'irms') + + assert vrms_value != "NOT_FOUND", f"`vrms` value not found in the simulation output for config #{i}" + assert irms_value != "NOT_FOUND", f"`irms` value not found in the simulation output for config #{i}" \ No newline at end of file