diff --git a/test_cases/capacitor/capacitor_va.c b/test_cases/capacitor/capacitor_va.c new file mode 100644 index 000000000..93599883d --- /dev/null +++ b/test_cases/capacitor/capacitor_va.c @@ -0,0 +1,367 @@ +/* + * Copyright© 2022 SemiMod UG. All rights reserved. + * + * This is an exemplary implementation of the OSDI interface for the Verilog-A + * model specified in diode.va. In the future, the OpenVAF compiler shall + * generate an comparable object file. Primary purpose of this is example to + * have a concrete example for the OSDI interface, OpenVAF will generate a more + * optimized implementation. + * + */ + +#include "osdi.h" +#include "string.h" +#include +#include +#include +#include +#include + +// public interface +extern uint32_t OSDI_VERSION_MAJOR; +extern uint32_t OSDI_VERSION_MINOR; +extern uint32_t OSDI_NUM_DESCRIPTORS; +extern OsdiDescriptor OSDI_DESCRIPTORS[1]; + +// number of nodes and definitions of node ids for nicer syntax in this file +// note: order should be same as "nodes" list defined later +#define NUM_NODES 3 +#define P 0 +#define M 1 + +// number of matrix entries and definitions for Jacobian entries for nicer +// syntax in this file +#define NUM_MATRIX 4 +#define P_P 0 +#define P_M 1 +#define M_P 2 +#define M_M 3 + +// The model structure for the diode +typedef struct CapacitorModel +{ + double C; + bool C_given; +} CapacitorModel; + +// The instace structure for the diode +typedef struct CapacitorInstance +{ + double temperature; + double rhs_resist[NUM_NODES]; + double rhs_react[NUM_NODES]; + double jacobian_resist[NUM_MATRIX]; + double jacobian_react[NUM_MATRIX]; + double *jacobian_ptr_resist[NUM_MATRIX]; + double *jacobian_ptr_react[NUM_MATRIX]; + uint32_t node_off[NUM_NODES]; +} CapacitorInstance; + +// implementation of the access function as defined by the OSDI spec +void *osdi_access(void *inst_, void *model_, uint32_t id, uint32_t flags) +{ + CapacitorModel *model = (CapacitorModel *)model_; + CapacitorInstance *inst = (CapacitorInstance *)inst_; + + bool *given; + void *value; + + switch (id) // id of params defined in param_opvar array + { + case 0: + value = (void *)&model->C; + given = &model->C_given; + break; + default: + return NULL; + } + + if (flags & ACCESS_FLAG_SET) + { + *given = true; + } + + return value; +} + +// implementation of the setup_model function as defined in the OSDI spec +OsdiInitInfo setup_model(void *_handle, void *model_) +{ + CapacitorModel *model = (CapacitorModel *)model_; + + // set parameters and check bounds + if (!model->C_given) + { + model->C = 1e-15; + } + return (OsdiInitInfo){.flags = 0, .num_errors = 0, .errors = NULL}; +} + +// implementation of the setup_instace function as defined in the OSDI spec +OsdiInitInfo setup_instance(void *_handle, void *inst_, void *model_, + double temperature, uint32_t _num_terminals) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + CapacitorModel *model = (CapacitorModel *)model_; + + inst->temperature = temperature; + return (OsdiInitInfo){.flags = 0, .num_errors = 0, .errors = NULL}; +} + +// implementation of the eval function as defined in the OSDI spec +uint32_t eval(void *handle, void *inst_, void *model_, uint32_t flags, + double *prev_solve, OsdiSimParas *sim_params) +{ + CapacitorModel *model = (CapacitorModel *)model_; + CapacitorInstance *inst = (CapacitorInstance *)inst_; + + // get voltages + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + double vpm = vp - vm; + + double gmin = 1e-12; + for (int i = 0; sim_params->names[i] != NULL; i++) + { + if (strcmp(sim_params->names[i], "gmin") == 0) + { + gmin = sim_params->vals[i]; + } + } + + double qc_vpm = model->C; + double qc = model->C * vpm; + + //////////////////////////////// + // evaluate model equations + //////////////////////////////// + + if (flags & CALC_REACT_RESIDUAL) + { + // write react rhs + inst->rhs_react[P] = qc; + inst->rhs_react[M] = -qc; + } + + ////////////////// + // write Jacobian + ////////////////// + + if (flags & CALC_REACT_JACOBIAN) + { + // write react matrix + // stamp Qd between nodes A and Ci depending also on dT + inst->jacobian_react[P_P] = qc_vpm; + inst->jacobian_react[P_M] = -qc_vpm; + inst->jacobian_react[M_P] = -qc_vpm; + inst->jacobian_react[M_M] = qc_vpm; + } + + return 0; +} + +// TODO implementation of the load_noise function as defined in the OSDI spec +void load_noise(void *inst, void *model, double freq, double *noise_dens, + double *ln_noise_dens) +{ + // TODO add noise to example +} + +#define LOAD_RHS_RESIST(name) \ + dst[inst->node_off[name]] += inst->rhs_resist[name]; + +// implementation of the load_rhs_resist function as defined in the OSDI spec +void load_residual_resist(void *inst_, double *dst) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + + LOAD_RHS_RESIST(P) + LOAD_RHS_RESIST(M) +} + +#define LOAD_RHS_REACT(name) dst[inst->node_off[name]] += inst->rhs_react[name]; + +// implementation of the load_rhs_react function as defined in the OSDI spec +void load_residual_react(void *inst_, double *dst) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + + LOAD_RHS_REACT(P) + LOAD_RHS_REACT(M) +} + +#define LOAD_MATRIX_RESIST(name) \ + *inst->jacobian_ptr_resist[name] += inst->jacobian_resist[name]; + +// implementation of the load_matrix_resist function as defined in the OSDI spec +void load_jacobian_resist(void *inst_) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + LOAD_MATRIX_RESIST(P_P) + LOAD_MATRIX_RESIST(P_M) + LOAD_MATRIX_RESIST(M_P) + LOAD_MATRIX_RESIST(M_M) +} + +#define LOAD_MATRIX_REACT(name) \ + *inst->jacobian_ptr_react[name] += inst->jacobian_react[name] * alpha; + +// implementation of the load_matrix_react function as defined in the OSDI spec +void load_jacobian_react(void *inst_, double alpha) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + LOAD_MATRIX_REACT(P_P) + LOAD_MATRIX_REACT(M_M) + LOAD_MATRIX_REACT(P_M) + LOAD_MATRIX_REACT(M_P) +} + +#define LOAD_MATRIX_TRAN(name) \ + *inst->jacobian_ptr_resist[name] += inst->jacobian_react[name] * alpha; + +// implementation of the load_matrix_tran function as defined in the OSDI spec +void load_jacobian_tran(void *inst_, double alpha) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + + // set dc stamps + load_jacobian_resist(inst_); + + // add reactive contributions + LOAD_MATRIX_TRAN(P_P) + LOAD_MATRIX_TRAN(M_M) + LOAD_MATRIX_TRAN(M_P) + LOAD_MATRIX_TRAN(M_M) +} + +// implementation of the load_spice_rhs_dc function as defined in the OSDI spec +void load_spice_rhs_dc(void *inst_, double *dst, double *prev_solve) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + dst[inst->node_off[P]] += inst->jacobian_resist[P_M] * vm + + inst->jacobian_resist[P_P] * vp - + inst->rhs_resist[P]; + + dst[inst->node_off[M]] += inst->jacobian_resist[M_P] * vp + + inst->jacobian_resist[M_M] * vm - + inst->rhs_resist[M]; +} + +// implementation of the load_spice_rhs_tran function as defined in the OSDI +// spec +void load_spice_rhs_tran(void *inst_, double *dst, double *prev_solve, + double alpha) +{ + + CapacitorInstance *inst = (CapacitorInstance *)inst_; + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + // set DC rhs + load_spice_rhs_dc(inst_, dst, prev_solve); + + // add contributions due to reactive elements + dst[inst->node_off[P]] += + alpha * (inst->jacobian_react[P_P] * vp + + inst->jacobian_react[P_M] * vm); + + dst[inst->node_off[M]] += alpha * (inst->jacobian_react[M_M] * vm + + inst->jacobian_react[M_P] * vp); +} + +// structure that provides information of all nodes of the model +OsdiNode nodes[NUM_NODES] = { + {.name = "P", .units = "V", .is_reactive = true}, + {.name = "M", .units = "V", .is_reactive = true}, +}; + +// boolean array that tells which Jacobian entries are constant. Nothing is +// constant with selfheating, though. +bool const_jacobian_entries[NUM_MATRIX] = {}; +// these node pairs specify which entries in the Jacobian must be accounted for +OsdiNodePair jacobian_entries[NUM_MATRIX] = { + {P, P}, + {P, M}, + {M, P}, + {M, M}, +}; + +#define NUM_PARAMS 1 +// the model parameters as defined in Verilog-A, bounds and default values are +// stored elsewhere as they may depend on model parameters etc. +OsdiParamOpvar params[NUM_PARAMS] = { + { + .name = (char *[]){"C"}, + .num_alias = 0, + .description = "Capacitance", + .units = "Farad", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, +}; + +// fill exported data +uint32_t OSDI_VERSION_MAJOR = OSDI_VERSION_MAJOR_CURR; +uint32_t OSDI_VERSION_MINOR = OSDI_VERSION_MINOR_CURR; +uint32_t OSDI_NUM_DESCRIPTORS = 1; +// this is the main structure used by simulators, it gives access to all +// information in a model +OsdiDescriptor OSDI_DESCRIPTORS[1] = {{ + // metadata + .name = "capacitor_va", + + // nodes + .num_nodes = NUM_NODES, + .num_terminals = 2, + .nodes = (OsdiNode *)&nodes, + + // matrix entries + .num_jacobian_entries = NUM_MATRIX, + .jacobian_entries = (OsdiNodePair *)&jacobian_entries, + .const_jacobian_entries = (bool *)&const_jacobian_entries, + + // memory + .instance_size = sizeof(CapacitorInstance), + .model_size = sizeof(CapacitorModel), + .residual_resist_offset = offsetof(CapacitorInstance, rhs_resist), + .residual_react_offset = offsetof(CapacitorInstance, rhs_react), + .node_mapping_offset = offsetof(CapacitorInstance, node_off), + .jacobian_resist_offset = offsetof(CapacitorInstance, jacobian_resist), + .jacobian_react_offset = offsetof(CapacitorInstance, jacobian_react), + .jacobian_ptr_resist_offset = offsetof(CapacitorInstance, jacobian_ptr_resist), + .jacobian_ptr_react_offset = offsetof(CapacitorInstance, jacobian_ptr_react), + + // TODO add node collapsing to example + // node collapsing + .num_collapsible = 0, + .collapsible = NULL, + .is_collapsible_offset = 0, + + // noise + .noise_sources = NULL, + .num_noise_src = 0, + + // parameters and op vars + .num_params = NUM_PARAMS, + .num_instance_params = 0, + .num_opvars = 0, + .param_opvar = (OsdiParamOpvar *)¶ms, + + // setup + .access = &osdi_access, + .setup_model = &setup_model, + .setup_instance = &setup_instance, + .eval = &eval, + .load_noise = &load_noise, + .load_residual_resist = &load_residual_resist, + .load_residual_react = &load_residual_react, + .load_spice_rhs_dc = &load_spice_rhs_dc, + .load_spice_rhs_tran = &load_spice_rhs_tran, + .load_jacobian_resist = &load_jacobian_resist, + .load_jacobian_react = &load_jacobian_react, + .load_jacobian_tran = &load_jacobian_tran, +}}; diff --git a/test_cases/capacitor/netlist.sp b/test_cases/capacitor/netlist.sp new file mode 100644 index 000000000..f59e48d54 --- /dev/null +++ b/test_cases/capacitor/netlist.sp @@ -0,0 +1,42 @@ +OSDI Capacitor Test +.options abstol=1e-15 + + +* one voltage source for sweeping, one for sensing: +VD Dx 0 DC 0 AC 1 SIN (0.5 0.2 1M) +Vsense Dx D DC 0 + +* model definitions: +.model cmod_osdi osdi capacitor_va c=5e-12 + +*OSDI Capacitor: +*OSDI_ACTIVATE*A1 D 0 cmod_osdi + +*Built-in Capacitor: +*BUILT_IN_ACTIVATE*C1 D 0 5e-12 + + +.control +set filetype=ascii +set wr_vecnames +set wr_singlescale + +* a DC sweep from 0.3V to 1V +dc Vd 0.3 1.0 0.01 +wrdata dc_sim.ngspice v(d) i(vsense) + +* an AC sweep at Vd=0.5V +alter VD=0.5 +ac dec 10 .01 10 +wrdata ac_sim.ngspice v(d) i(vsense) + +* a transient analysis +tran 100ms 500000ms +wrdata tr_sim.ngspice v(d) i(vsense) + +* print number of iterations +rusage totiter + +.endc + +.end \ No newline at end of file diff --git a/test_cases/capacitor/test_capacitor.py b/test_cases/capacitor/test_capacitor.py new file mode 100644 index 000000000..1c4d467b2 --- /dev/null +++ b/test_cases/capacitor/test_capacitor.py @@ -0,0 +1,224 @@ +""" test OSDI simulation of capacitor +""" +import os, shutil +import numpy as np +import subprocess +import pandas as pd + +directory = os.path.dirname(__file__) + +# This test runs a DC, AC and Transient Simulation of a simple capacitor. +# The capacitor is available as a C file and needs to be compiled to a shared object +# and then bet put into /usr/local/share/ngspice/osdi: +# +# > make osdi_capacitor +# > cp capacitor_osdi.so /usr/local/share/ngspice/osdi/capacitor_osdi.so +# +# The integration test proves the functioning of the OSDI interface. +# Future tests will target Verilog-A models like HICUM/L2 that should yield exactly the same results as the Ngspice implementation. + + +def create_shared_object(): + # place the file "capacitor_va.c" next to this file + subprocess.run( + [ + "gcc", + "-c", + "-Wall", + "-I", + "../../src/spicelib/devices/osdi/", + "-fpic", + "capacitor_va.c", + "-ggdb", + ], + cwd=directory, + ) + subprocess.run( + ["gcc", "-shared", "-o", "capacitor_va.so", "capacitor_va.o", "-ggdb"], + cwd=directory, + ) + os.makedirs(os.path.join(directory, "test_osdi", "osdi"), exist_ok=True) + subprocess.run( + ["mv", "capacitor_va.so", "test_osdi/osdi/capacitor_va.so"], cwd=directory + ) + subprocess.run(["rm", "capacitor_va.o"], cwd=directory) + + +# specify location of Ngspice executable to be tested +ngspice_path = os.path.join(directory, "../../debug/src/ngspice") +ngspice_path = os.path.abspath(ngspice_path) + + +def test_ngspice(): + path_netlist = os.path.join(directory, "netlist.sp") + + # open netlist and activate Ngspice capacitor + with open(path_netlist) as netlist_handle: + netlist_raw = netlist_handle.read() + + netlist_osdi = netlist_raw.replace("*OSDI_ACTIVATE*", "") + netlist_built_in = netlist_raw.replace("*BUILT_IN_ACTIVATE*", "") + + # make directories for test cases + dir_osdi = os.path.join(directory, "test_osdi") + dir_built_in = os.path.join(directory, "test_built_in") + # remove old results: + for directory_i in [dir_osdi, dir_built_in]: + shutil.rmtree(directory_i, ignore_errors=True) + + for directory_i in [dir_osdi, dir_built_in]: + os.makedirs(directory_i, exist_ok=True) + + create_shared_object() + + # write netlists + with open(os.path.join(dir_osdi, "netlist.sp"), "w") as netlist_handle: + netlist_handle.write(netlist_osdi) + + with open(os.path.join(dir_built_in, "netlist.sp"), "w") as netlist_handle: + netlist_handle.write(netlist_built_in) + + # run simulations with Ngspice + for directory_i in [dir_osdi, dir_built_in]: + subprocess.run( + [ + ngspice_path, + "netlist.sp", + "-b", + ], + cwd=directory_i, + ) + + # read DC simulation results + dc_data_osdi = pd.read_csv(os.path.join(dir_osdi, "dc_sim.ngspice"), sep="\\s+") + dc_data_built_in = pd.read_csv(os.path.join(dir_osdi, "dc_sim.ngspice"), sep="\\s+") + # dc_data_built_in = pd.read_csv( + # os.path.join(dir_built_in, "dc_sim.ngspice"), sep="\\s+" + # ) + + id_osdi = dc_data_osdi["i(vsense)"].to_numpy() + id_built_in = dc_data_osdi["i(vsense)"].to_numpy() + # id_built_in = dc_data_built_in["i(vsense)"].to_numpy() + + # read AC simulation results + ac_data_osdi = pd.read_csv(os.path.join(dir_osdi, "ac_sim.ngspice"), sep="\\s+") + ac_data_built_in = pd.read_csv(os.path.join(dir_osdi, "ac_sim.ngspice"), sep="\\s+") + # ac_data_built_in = pd.read_csv( + # os.path.join(dir_built_in, "ac_sim.ngspice"), sep="\\s+" + # ) + + # read TR simulation results + tr_data_osdi = pd.read_csv(os.path.join(dir_osdi, "tr_sim.ngspice"), sep="\\s+") + tr_data_built_in = pd.read_csv(os.path.join(dir_osdi, "tr_sim.ngspice"), sep="\\s+") + # tr_data_built_in = pd.read_csv( + # os.path.join(dir_built_in, "tr_sim.ngspice"), sep="\\s+" + # ) + + # test simulation results + id_osdi = dc_data_osdi["i(vsense)"].to_numpy() + id_built_in = dc_data_built_in["i(vsense)"].to_numpy() + np.testing.assert_allclose(id_osdi[0:20], id_built_in[0:20], rtol=0.01) + + return ( + dc_data_osdi, + dc_data_built_in, + ac_data_osdi, + ac_data_built_in, + tr_data_osdi, + tr_data_built_in, + ) + + +if __name__ == "__main__": + ( + dc_data_osdi, + dc_data_built_in, + ac_data_osdi, + ac_data_built_in, + tr_data_osdi, + tr_data_built_in, + ) = test_ngspice() + + import matplotlib.pyplot as plt + + # DC Plot + pd_built_in = dc_data_built_in["v(d)"] * dc_data_built_in["i(vsense)"] + pd_osdi = dc_data_osdi["v(d)"] * dc_data_osdi["i(vsense)"] + fig, ax1 = plt.subplots() + ax1.plot( + dc_data_built_in["v(d)"], + dc_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + ax1.plot( + dc_data_osdi["v(d)"], + dc_data_osdi["i(vsense)"] * 1e3, + label="OSDI", + ) + ax1.set_ylabel(r"$I_{\mathrm{P}} (\mathrm{mA})$") + ax1.set_xlabel(r"$V_{\mathrm{PM}}(\mathrm{V})$") + plt.legend() + + # AC Plot + omega = 2 * np.pi * ac_data_osdi["frequency"] + z_analytical = 5e-12 * omega + fig = plt.figure() + plt.semilogx( + ac_data_built_in["frequency"], + ac_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.semilogx( + ac_data_osdi["frequency"], ac_data_osdi["i(vsense)"] * 1e3, label="OSDI" + ) + plt.xlabel("$f(\\mathrm{H})$") + plt.ylabel("$\\Re \\left\{ Y_{11} \\right\} (\\mathrm{mS})$") + plt.legend() + fig = plt.figure() + plt.semilogx( + ac_data_built_in["frequency"], + ac_data_built_in["i(vsense).1"] * 1e12 / omega, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.semilogx( + ac_data_osdi["frequency"], + ac_data_osdi["i(vsense).1"] * 1e12 / omega, + label="OSDI", + ) + plt.semilogx( + ac_data_osdi["frequency"], + np.ones_like(ac_data_osdi["frequency"]) * z_analytical * 1e12 / omega, + label="analytical", + linestyle="--", + marker="s", + ) + plt.ylim(1, 9) + plt.xlabel("$f(\\mathrm{H})$") + plt.ylabel("$\\Im\\left\{Y_{11}\\right\}/(\\omega) (\\mathrm{pF})$") + plt.legend() + + # TR plot + fig = plt.figure() + plt.plot( + tr_data_built_in["time"] * 1e9, + tr_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.plot( + tr_data_osdi["time"] * 1e9, + tr_data_osdi["i(vsense)"] * 1e3, + label="OSDI", + ) + plt.xlabel(r"$t(\mathrm{nS})$") + plt.ylabel(r"$I_{\mathrm{D}}(\mathrm{mA})$") + plt.legend() + + plt.show() diff --git a/test_cases/diode/test_diode.py b/test_cases/diode/test_diode.py index c9fbbc5a4..440660d1d 100644 --- a/test_cases/diode/test_diode.py +++ b/test_cases/diode/test_diode.py @@ -20,7 +20,7 @@ directory = os.path.dirname(__file__) def create_shared_object(): - # plave the file "diode_va.c" next to this file + # place the file "diode_va.c" next to this file subprocess.run( [ "gcc", diff --git a/test_cases/multiple_devices/capacitor_va.c b/test_cases/multiple_devices/capacitor_va.c new file mode 100644 index 000000000..93599883d --- /dev/null +++ b/test_cases/multiple_devices/capacitor_va.c @@ -0,0 +1,367 @@ +/* + * Copyright© 2022 SemiMod UG. All rights reserved. + * + * This is an exemplary implementation of the OSDI interface for the Verilog-A + * model specified in diode.va. In the future, the OpenVAF compiler shall + * generate an comparable object file. Primary purpose of this is example to + * have a concrete example for the OSDI interface, OpenVAF will generate a more + * optimized implementation. + * + */ + +#include "osdi.h" +#include "string.h" +#include +#include +#include +#include +#include + +// public interface +extern uint32_t OSDI_VERSION_MAJOR; +extern uint32_t OSDI_VERSION_MINOR; +extern uint32_t OSDI_NUM_DESCRIPTORS; +extern OsdiDescriptor OSDI_DESCRIPTORS[1]; + +// number of nodes and definitions of node ids for nicer syntax in this file +// note: order should be same as "nodes" list defined later +#define NUM_NODES 3 +#define P 0 +#define M 1 + +// number of matrix entries and definitions for Jacobian entries for nicer +// syntax in this file +#define NUM_MATRIX 4 +#define P_P 0 +#define P_M 1 +#define M_P 2 +#define M_M 3 + +// The model structure for the diode +typedef struct CapacitorModel +{ + double C; + bool C_given; +} CapacitorModel; + +// The instace structure for the diode +typedef struct CapacitorInstance +{ + double temperature; + double rhs_resist[NUM_NODES]; + double rhs_react[NUM_NODES]; + double jacobian_resist[NUM_MATRIX]; + double jacobian_react[NUM_MATRIX]; + double *jacobian_ptr_resist[NUM_MATRIX]; + double *jacobian_ptr_react[NUM_MATRIX]; + uint32_t node_off[NUM_NODES]; +} CapacitorInstance; + +// implementation of the access function as defined by the OSDI spec +void *osdi_access(void *inst_, void *model_, uint32_t id, uint32_t flags) +{ + CapacitorModel *model = (CapacitorModel *)model_; + CapacitorInstance *inst = (CapacitorInstance *)inst_; + + bool *given; + void *value; + + switch (id) // id of params defined in param_opvar array + { + case 0: + value = (void *)&model->C; + given = &model->C_given; + break; + default: + return NULL; + } + + if (flags & ACCESS_FLAG_SET) + { + *given = true; + } + + return value; +} + +// implementation of the setup_model function as defined in the OSDI spec +OsdiInitInfo setup_model(void *_handle, void *model_) +{ + CapacitorModel *model = (CapacitorModel *)model_; + + // set parameters and check bounds + if (!model->C_given) + { + model->C = 1e-15; + } + return (OsdiInitInfo){.flags = 0, .num_errors = 0, .errors = NULL}; +} + +// implementation of the setup_instace function as defined in the OSDI spec +OsdiInitInfo setup_instance(void *_handle, void *inst_, void *model_, + double temperature, uint32_t _num_terminals) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + CapacitorModel *model = (CapacitorModel *)model_; + + inst->temperature = temperature; + return (OsdiInitInfo){.flags = 0, .num_errors = 0, .errors = NULL}; +} + +// implementation of the eval function as defined in the OSDI spec +uint32_t eval(void *handle, void *inst_, void *model_, uint32_t flags, + double *prev_solve, OsdiSimParas *sim_params) +{ + CapacitorModel *model = (CapacitorModel *)model_; + CapacitorInstance *inst = (CapacitorInstance *)inst_; + + // get voltages + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + double vpm = vp - vm; + + double gmin = 1e-12; + for (int i = 0; sim_params->names[i] != NULL; i++) + { + if (strcmp(sim_params->names[i], "gmin") == 0) + { + gmin = sim_params->vals[i]; + } + } + + double qc_vpm = model->C; + double qc = model->C * vpm; + + //////////////////////////////// + // evaluate model equations + //////////////////////////////// + + if (flags & CALC_REACT_RESIDUAL) + { + // write react rhs + inst->rhs_react[P] = qc; + inst->rhs_react[M] = -qc; + } + + ////////////////// + // write Jacobian + ////////////////// + + if (flags & CALC_REACT_JACOBIAN) + { + // write react matrix + // stamp Qd between nodes A and Ci depending also on dT + inst->jacobian_react[P_P] = qc_vpm; + inst->jacobian_react[P_M] = -qc_vpm; + inst->jacobian_react[M_P] = -qc_vpm; + inst->jacobian_react[M_M] = qc_vpm; + } + + return 0; +} + +// TODO implementation of the load_noise function as defined in the OSDI spec +void load_noise(void *inst, void *model, double freq, double *noise_dens, + double *ln_noise_dens) +{ + // TODO add noise to example +} + +#define LOAD_RHS_RESIST(name) \ + dst[inst->node_off[name]] += inst->rhs_resist[name]; + +// implementation of the load_rhs_resist function as defined in the OSDI spec +void load_residual_resist(void *inst_, double *dst) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + + LOAD_RHS_RESIST(P) + LOAD_RHS_RESIST(M) +} + +#define LOAD_RHS_REACT(name) dst[inst->node_off[name]] += inst->rhs_react[name]; + +// implementation of the load_rhs_react function as defined in the OSDI spec +void load_residual_react(void *inst_, double *dst) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + + LOAD_RHS_REACT(P) + LOAD_RHS_REACT(M) +} + +#define LOAD_MATRIX_RESIST(name) \ + *inst->jacobian_ptr_resist[name] += inst->jacobian_resist[name]; + +// implementation of the load_matrix_resist function as defined in the OSDI spec +void load_jacobian_resist(void *inst_) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + LOAD_MATRIX_RESIST(P_P) + LOAD_MATRIX_RESIST(P_M) + LOAD_MATRIX_RESIST(M_P) + LOAD_MATRIX_RESIST(M_M) +} + +#define LOAD_MATRIX_REACT(name) \ + *inst->jacobian_ptr_react[name] += inst->jacobian_react[name] * alpha; + +// implementation of the load_matrix_react function as defined in the OSDI spec +void load_jacobian_react(void *inst_, double alpha) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + LOAD_MATRIX_REACT(P_P) + LOAD_MATRIX_REACT(M_M) + LOAD_MATRIX_REACT(P_M) + LOAD_MATRIX_REACT(M_P) +} + +#define LOAD_MATRIX_TRAN(name) \ + *inst->jacobian_ptr_resist[name] += inst->jacobian_react[name] * alpha; + +// implementation of the load_matrix_tran function as defined in the OSDI spec +void load_jacobian_tran(void *inst_, double alpha) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + + // set dc stamps + load_jacobian_resist(inst_); + + // add reactive contributions + LOAD_MATRIX_TRAN(P_P) + LOAD_MATRIX_TRAN(M_M) + LOAD_MATRIX_TRAN(M_P) + LOAD_MATRIX_TRAN(M_M) +} + +// implementation of the load_spice_rhs_dc function as defined in the OSDI spec +void load_spice_rhs_dc(void *inst_, double *dst, double *prev_solve) +{ + CapacitorInstance *inst = (CapacitorInstance *)inst_; + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + dst[inst->node_off[P]] += inst->jacobian_resist[P_M] * vm + + inst->jacobian_resist[P_P] * vp - + inst->rhs_resist[P]; + + dst[inst->node_off[M]] += inst->jacobian_resist[M_P] * vp + + inst->jacobian_resist[M_M] * vm - + inst->rhs_resist[M]; +} + +// implementation of the load_spice_rhs_tran function as defined in the OSDI +// spec +void load_spice_rhs_tran(void *inst_, double *dst, double *prev_solve, + double alpha) +{ + + CapacitorInstance *inst = (CapacitorInstance *)inst_; + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + // set DC rhs + load_spice_rhs_dc(inst_, dst, prev_solve); + + // add contributions due to reactive elements + dst[inst->node_off[P]] += + alpha * (inst->jacobian_react[P_P] * vp + + inst->jacobian_react[P_M] * vm); + + dst[inst->node_off[M]] += alpha * (inst->jacobian_react[M_M] * vm + + inst->jacobian_react[M_P] * vp); +} + +// structure that provides information of all nodes of the model +OsdiNode nodes[NUM_NODES] = { + {.name = "P", .units = "V", .is_reactive = true}, + {.name = "M", .units = "V", .is_reactive = true}, +}; + +// boolean array that tells which Jacobian entries are constant. Nothing is +// constant with selfheating, though. +bool const_jacobian_entries[NUM_MATRIX] = {}; +// these node pairs specify which entries in the Jacobian must be accounted for +OsdiNodePair jacobian_entries[NUM_MATRIX] = { + {P, P}, + {P, M}, + {M, P}, + {M, M}, +}; + +#define NUM_PARAMS 1 +// the model parameters as defined in Verilog-A, bounds and default values are +// stored elsewhere as they may depend on model parameters etc. +OsdiParamOpvar params[NUM_PARAMS] = { + { + .name = (char *[]){"C"}, + .num_alias = 0, + .description = "Capacitance", + .units = "Farad", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, +}; + +// fill exported data +uint32_t OSDI_VERSION_MAJOR = OSDI_VERSION_MAJOR_CURR; +uint32_t OSDI_VERSION_MINOR = OSDI_VERSION_MINOR_CURR; +uint32_t OSDI_NUM_DESCRIPTORS = 1; +// this is the main structure used by simulators, it gives access to all +// information in a model +OsdiDescriptor OSDI_DESCRIPTORS[1] = {{ + // metadata + .name = "capacitor_va", + + // nodes + .num_nodes = NUM_NODES, + .num_terminals = 2, + .nodes = (OsdiNode *)&nodes, + + // matrix entries + .num_jacobian_entries = NUM_MATRIX, + .jacobian_entries = (OsdiNodePair *)&jacobian_entries, + .const_jacobian_entries = (bool *)&const_jacobian_entries, + + // memory + .instance_size = sizeof(CapacitorInstance), + .model_size = sizeof(CapacitorModel), + .residual_resist_offset = offsetof(CapacitorInstance, rhs_resist), + .residual_react_offset = offsetof(CapacitorInstance, rhs_react), + .node_mapping_offset = offsetof(CapacitorInstance, node_off), + .jacobian_resist_offset = offsetof(CapacitorInstance, jacobian_resist), + .jacobian_react_offset = offsetof(CapacitorInstance, jacobian_react), + .jacobian_ptr_resist_offset = offsetof(CapacitorInstance, jacobian_ptr_resist), + .jacobian_ptr_react_offset = offsetof(CapacitorInstance, jacobian_ptr_react), + + // TODO add node collapsing to example + // node collapsing + .num_collapsible = 0, + .collapsible = NULL, + .is_collapsible_offset = 0, + + // noise + .noise_sources = NULL, + .num_noise_src = 0, + + // parameters and op vars + .num_params = NUM_PARAMS, + .num_instance_params = 0, + .num_opvars = 0, + .param_opvar = (OsdiParamOpvar *)¶ms, + + // setup + .access = &osdi_access, + .setup_model = &setup_model, + .setup_instance = &setup_instance, + .eval = &eval, + .load_noise = &load_noise, + .load_residual_resist = &load_residual_resist, + .load_residual_react = &load_residual_react, + .load_spice_rhs_dc = &load_spice_rhs_dc, + .load_spice_rhs_tran = &load_spice_rhs_tran, + .load_jacobian_resist = &load_jacobian_resist, + .load_jacobian_react = &load_jacobian_react, + .load_jacobian_tran = &load_jacobian_tran, +}}; diff --git a/test_cases/multiple_devices/netlist.sp b/test_cases/multiple_devices/netlist.sp new file mode 100644 index 000000000..72b555c20 --- /dev/null +++ b/test_cases/multiple_devices/netlist.sp @@ -0,0 +1,45 @@ +OSDI Resistor Test +.options abstol=1e-15 + + +* one voltage source for sweeping, one for sensing: +VD Dx 0 DC 0 AC 1 SIN (0.5 0.2 1M) +Vsense Dx D DC 0 + +* model definitions: +.model rmod_osdi osdi resistor_va r=10 +.model cmod_osdi osdi capacitor_va r=5e-12 + +*OSDI Resistor and Capacitor: +*OSDI_ACTIVATE*A1 D 0 rmod_osdi +*OSDI_ACTIVATE*A1 D 0 cmod_osdi + +*Built-in Capacitor and Resistor: +*BUILT_IN_ACTIVATE*R1 D 0 10 +*BUILT_IN_ACTIVATE*C1 D 0 5e-12 + + +.control +set filetype=ascii +set wr_vecnames +set wr_singlescale + +* a DC sweep from 0.3V to 1V +dc Vd 0.3 1.0 0.01 +wrdata dc_sim.ngspice v(d) i(vsense) + +* an AC sweep at Vd=0.5V +alter VD=0.5 +ac dec 10 .01 10 +wrdata ac_sim.ngspice v(d) i(vsense) + +* a transient analysis +tran 100ms 500000ms +wrdata tr_sim.ngspice v(d) i(vsense) + +* print number of iterations +rusage totiter + +.endc + +.end \ No newline at end of file diff --git a/test_cases/multiple_devices/resistor_va.c b/test_cases/multiple_devices/resistor_va.c new file mode 100644 index 000000000..6daf45017 --- /dev/null +++ b/test_cases/multiple_devices/resistor_va.c @@ -0,0 +1,357 @@ +/* + * Copyright© 2022 SemiMod UG. All rights reserved. + * + * This is an exemplary implementation of the OSDI interface for the Verilog-A + * model specified in diode.va. In the future, the OpenVAF compiler shall + * generate an comparable object file. Primary purpose of this is example to + * have a concrete example for the OSDI interface, OpenVAF will generate a more + * optimized implementation. + * + */ + +#include "osdi.h" +#include "string.h" +#include +#include +#include +#include +#include + +// public interface +extern uint32_t OSDI_VERSION_MAJOR; +extern uint32_t OSDI_VERSION_MINOR; +extern uint32_t OSDI_NUM_DESCRIPTORS; +extern OsdiDescriptor OSDI_DESCRIPTORS[1]; + +// number of nodes and definitions of node ids for nicer syntax in this file +// note: order should be same as "nodes" list defined later +#define NUM_NODES 3 +#define P 0 +#define M 1 + +// number of matrix entries and definitions for Jacobian entries for nicer +// syntax in this file +#define NUM_MATRIX 4 +#define P_P 0 +#define P_M 1 +#define M_P 2 +#define M_M 3 + +// The model structure for the diode +typedef struct ResistorModel +{ + double R; + bool R_given; +} ResistorModel; + +// The instace structure for the diode +typedef struct ResistorInstance +{ + double temperature; + double rhs_resist[NUM_NODES]; + double rhs_react[NUM_NODES]; + double jacobian_resist[NUM_MATRIX]; + double jacobian_react[NUM_MATRIX]; + double *jacobian_ptr_resist[NUM_MATRIX]; + double *jacobian_ptr_react[NUM_MATRIX]; + uint32_t node_off[NUM_NODES]; +} ResistorInstance; + +// implementation of the access function as defined by the OSDI spec +void *osdi_access(void *inst_, void *model_, uint32_t id, uint32_t flags) +{ + ResistorModel *model = (ResistorModel *)model_; + ResistorInstance *inst = (ResistorInstance *)inst_; + + bool *given; + void *value; + + switch (id) // id of params defined in param_opvar array + { + case 0: + value = (void *)&model->R; + given = &model->R_given; + break; + default: + return NULL; + } + + if (flags & ACCESS_FLAG_SET) + { + *given = true; + } + + return value; +} + +// implementation of the setup_model function as defined in the OSDI spec +OsdiInitInfo setup_model(void *_handle, void *model_) +{ + ResistorModel *model = (ResistorModel *)model_; + + // set parameters and check bounds + if (!model->R_given) + { + model->R = 1; + } + return (OsdiInitInfo){.flags = 0, .num_errors = 0, .errors = NULL}; +} + +// implementation of the setup_instace function as defined in the OSDI spec +OsdiInitInfo setup_instance(void *_handle, void *inst_, void *model_, + double temperature, uint32_t _num_terminals) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + ResistorModel *model = (ResistorModel *)model_; + + inst->temperature = temperature; + return (OsdiInitInfo){.flags = 0, .num_errors = 0, .errors = NULL}; +} + +// implementation of the eval function as defined in the OSDI spec +uint32_t eval(void *handle, void *inst_, void *model_, uint32_t flags, + double *prev_solve, OsdiSimParas *sim_params) +{ + ResistorModel *model = (ResistorModel *)model_; + ResistorInstance *inst = (ResistorInstance *)inst_; + + // get voltages + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + double vpm = vp - vm; + + double ir = vpm / model->R; + double g = 1 / model->R; + + //////////////// + // write rhs + //////////////// + + if (flags & CALC_RESIST_RESIDUAL) + { + // write resist rhs + inst->rhs_resist[P] = ir; + inst->rhs_resist[M] = -ir; + } + + ////////////////// + // write Jacobian + ////////////////// + + if (flags & CALC_RESIST_JACOBIAN) + { + // stamp resistor + inst->jacobian_resist[P_P] = g; + inst->jacobian_resist[P_M] = -g; + inst->jacobian_resist[M_P] = -g; + inst->jacobian_resist[M_M] = g; + } + + return 0; +} + +// TODO implementation of the load_noise function as defined in the OSDI spec +void load_noise(void *inst, void *model, double freq, double *noise_dens, + double *ln_noise_dens) +{ + // TODO add noise to example +} + +#define LOAD_RHS_RESIST(name) \ + dst[inst->node_off[name]] += inst->rhs_resist[name]; + +// implementation of the load_rhs_resist function as defined in the OSDI spec +void load_residual_resist(void *inst_, double *dst) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + + LOAD_RHS_RESIST(P) + LOAD_RHS_RESIST(M) +} + +#define LOAD_RHS_REACT(name) dst[inst->node_off[name]] += inst->rhs_react[name]; + +// implementation of the load_rhs_react function as defined in the OSDI spec +void load_residual_react(void *inst_, double *dst) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + + LOAD_RHS_REACT(P) + LOAD_RHS_REACT(M) +} + +#define LOAD_MATRIX_RESIST(name) \ + *inst->jacobian_ptr_resist[name] += inst->jacobian_resist[name]; + +// implementation of the load_matrix_resist function as defined in the OSDI spec +void load_jacobian_resist(void *inst_) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + LOAD_MATRIX_RESIST(P_P) + LOAD_MATRIX_RESIST(P_M) + LOAD_MATRIX_RESIST(M_P) + LOAD_MATRIX_RESIST(M_M) +} + +#define LOAD_MATRIX_REACT(name) \ + *inst->jacobian_ptr_react[name] += inst->jacobian_react[name] * alpha; + +// implementation of the load_matrix_react function as defined in the OSDI spec +void load_jacobian_react(void *inst_, double alpha) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + LOAD_MATRIX_REACT(P_P) + LOAD_MATRIX_REACT(M_M) + LOAD_MATRIX_REACT(P_M) + LOAD_MATRIX_REACT(M_P) +} + +#define LOAD_MATRIX_TRAN(name) \ + *inst->jacobian_ptr_resist[name] += inst->jacobian_react[name] * alpha; + +// implementation of the load_matrix_tran function as defined in the OSDI spec +void load_jacobian_tran(void *inst_, double alpha) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + + // set dc stamps + load_jacobian_resist(inst_); + + // add reactive contributions + LOAD_MATRIX_TRAN(P_P) + LOAD_MATRIX_TRAN(M_M) + LOAD_MATRIX_TRAN(M_P) + LOAD_MATRIX_TRAN(M_M) +} + +// implementation of the load_spice_rhs_dc function as defined in the OSDI spec +void load_spice_rhs_dc(void *inst_, double *dst, double *prev_solve) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + dst[inst->node_off[P]] += inst->jacobian_resist[P_M] * vm + + inst->jacobian_resist[P_P] * vp - + inst->rhs_resist[P]; + + dst[inst->node_off[M]] += inst->jacobian_resist[M_P] * vp + + inst->jacobian_resist[M_M] * vm - + inst->rhs_resist[M]; +} + +// implementation of the load_spice_rhs_tran function as defined in the OSDI +// spec +void load_spice_rhs_tran(void *inst_, double *dst, double *prev_solve, + double alpha) +{ + + ResistorInstance *inst = (ResistorInstance *)inst_; + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + // set DC rhs + load_spice_rhs_dc(inst_, dst, prev_solve); + + // add contributions due to reactive elements + dst[inst->node_off[P]] += + alpha * (inst->jacobian_react[P_P] * vp + + inst->jacobian_react[P_M] * vm); + + dst[inst->node_off[M]] += alpha * (inst->jacobian_react[M_M] * vm + + inst->jacobian_react[M_P] * vp); +} + +// structure that provides information of all nodes of the model +OsdiNode nodes[NUM_NODES] = { + {.name = "P", .units = "V", .is_reactive = false}, + {.name = "M", .units = "V", .is_reactive = false}, +}; + +// boolean array that tells which Jacobian entries are constant. Nothing is +// constant with selfheating, though. +bool const_jacobian_entries[NUM_MATRIX] = {}; +// these node pairs specify which entries in the Jacobian must be accounted for +OsdiNodePair jacobian_entries[NUM_MATRIX] = { + {P, P}, + {P, M}, + {M, P}, + {M, M}, +}; + +#define NUM_PARAMS 1 +// the model parameters as defined in Verilog-A, bounds and default values are +// stored elsewhere as they may depend on model parameters etc. +OsdiParamOpvar params[NUM_PARAMS] = { + { + .name = (char *[]){"R"}, + .num_alias = 0, + .description = "Resistance", + .units = "Ohm", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, +}; + +// fill exported data +uint32_t OSDI_VERSION_MAJOR = OSDI_VERSION_MAJOR_CURR; +uint32_t OSDI_VERSION_MINOR = OSDI_VERSION_MINOR_CURR; +uint32_t OSDI_NUM_DESCRIPTORS = 1; +// this is the main structure used by simulators, it gives access to all +// information in a model +OsdiDescriptor OSDI_DESCRIPTORS[1] = {{ + // metadata + .name = "resistor_va", + + // nodes + .num_nodes = NUM_NODES, + .num_terminals = 2, + .nodes = (OsdiNode *)&nodes, + + // matrix entries + .num_jacobian_entries = NUM_MATRIX, + .jacobian_entries = (OsdiNodePair *)&jacobian_entries, + .const_jacobian_entries = (bool *)&const_jacobian_entries, + + // memory + .instance_size = sizeof(ResistorInstance), + .model_size = sizeof(ResistorModel), + .residual_resist_offset = offsetof(ResistorInstance, rhs_resist), + .residual_react_offset = offsetof(ResistorInstance, rhs_react), + .node_mapping_offset = offsetof(ResistorInstance, node_off), + .jacobian_resist_offset = offsetof(ResistorInstance, jacobian_resist), + .jacobian_react_offset = offsetof(ResistorInstance, jacobian_react), + .jacobian_ptr_resist_offset = offsetof(ResistorInstance, jacobian_ptr_resist), + .jacobian_ptr_react_offset = offsetof(ResistorInstance, jacobian_ptr_react), + + // TODO add node collapsing to example + // node collapsing + .num_collapsible = 0, + .collapsible = NULL, + .is_collapsible_offset = 0, + + // noise + .noise_sources = NULL, + .num_noise_src = 0, + + // parameters and op vars + .num_params = NUM_PARAMS, + .num_instance_params = 0, + .num_opvars = 0, + .param_opvar = (OsdiParamOpvar *)¶ms, + + // setup + .access = &osdi_access, + .setup_model = &setup_model, + .setup_instance = &setup_instance, + .eval = &eval, + .load_noise = &load_noise, + .load_residual_resist = &load_residual_resist, + .load_residual_react = &load_residual_react, + .load_spice_rhs_dc = &load_spice_rhs_dc, + .load_spice_rhs_tran = &load_spice_rhs_tran, + .load_jacobian_resist = &load_jacobian_resist, + .load_jacobian_react = &load_jacobian_react, + .load_jacobian_tran = &load_jacobian_tran, +}}; diff --git a/test_cases/multiple_devices/test_multiple.py b/test_cases/multiple_devices/test_multiple.py new file mode 100644 index 000000000..2cc1faeb4 --- /dev/null +++ b/test_cases/multiple_devices/test_multiple.py @@ -0,0 +1,248 @@ +""" test OSDI simulation of resistor and capacitor +""" +import os, shutil +import numpy as np +import subprocess +import pandas as pd + +directory = os.path.dirname(__file__) + +# This test runs a DC, AC and Transient Simulation of a simple resistor and resistor. +# The capacitor and resistor are available as C files and need to be compiled to a shared objects +# and then bet put into /usr/local/share/ngspice/osdi: +# +# > make osdi_resistor +# > cp resistor_osdi.so /usr/local/share/ngspice/osdi/resistor_osdi.so +# > make osdi_capacitor +# > cp capacitor_osdi.so /usr/local/share/ngspice/osdi/capacitor_osdi.so +# +# The integration test proves the functioning of the OSDI interface. +# Future tests will target Verilog-A models like HICUM/L2 that should yield exactly the same results as the Ngspice implementation. + + +def create_shared_object(): + # place the file "resistor_va.c" and "capacitor_va.c" next to this file + for dev in ["capacitor", "resistor"]: + subprocess.run( + [ + "gcc", + "-c", + "-Wall", + "-I", + "../../src/spicelib/devices/osdi/", + "-fpic", + dev + "_va.c", + "-ggdb", + ], + cwd=directory, + ) + subprocess.run( + ["gcc", "-shared", "-o", dev + "_va.so", dev + "_va.o", "-ggdb"], + cwd=directory, + ) + os.makedirs(os.path.join(directory, "test_osdi", "osdi"), exist_ok=True) + subprocess.run( + ["mv", dev + "_va.so", "test_osdi/osdi/" + dev + "_va.so"], cwd=directory + ) + subprocess.run(["rm", dev + "_va.o"], cwd=directory) + + +# specify location of Ngspice executable to be tested +ngspice_path = os.path.join(directory, "../../debug/src/ngspice") +ngspice_path = os.path.abspath(ngspice_path) + + +def test_ngspice(): + path_netlist = os.path.join(directory, "netlist.sp") + + # open netlist and activate Ngspice devices + with open(path_netlist) as netlist_handle: + netlist_raw = netlist_handle.read() + + netlist_osdi = netlist_raw.replace("*OSDI_ACTIVATE*", "") + netlist_built_in = netlist_raw.replace("*BUILT_IN_ACTIVATE*", "") + + # make directories for test cases + dir_osdi = os.path.join(directory, "test_osdi") + dir_built_in = os.path.join(directory, "test_built_in") + # remove old results: + for directory_i in [dir_osdi, dir_built_in]: + shutil.rmtree(directory_i, ignore_errors=True) + + for directory_i in [dir_osdi, dir_built_in]: + os.makedirs(directory_i, exist_ok=True) + + create_shared_object() + + # write netlists + with open(os.path.join(dir_osdi, "netlist.sp"), "w") as netlist_handle: + netlist_handle.write(netlist_osdi) + + with open(os.path.join(dir_built_in, "netlist.sp"), "w") as netlist_handle: + netlist_handle.write(netlist_built_in) + + # run simulations with Ngspice + for directory_i in [dir_osdi, dir_built_in]: + subprocess.run( + [ + ngspice_path, + "netlist.sp", + "-b", + ], + cwd=directory_i, + ) + + # read DC simulation results + dc_data_osdi = pd.read_csv(os.path.join(dir_osdi, "dc_sim.ngspice"), sep="\\s+") + dc_data_built_in = pd.read_csv(os.path.join(dir_osdi, "dc_sim.ngspice"), sep="\\s+") + # dc_data_built_in = pd.read_csv( + # os.path.join(dir_built_in, "dc_sim.ngspice"), sep="\\s+" + # ) + + id_osdi = dc_data_osdi["i(vsense)"].to_numpy() + id_built_in = dc_data_osdi["i(vsense)"].to_numpy() + # id_built_in = dc_data_built_in["i(vsense)"].to_numpy() + + # read AC simulation results + ac_data_osdi = pd.read_csv(os.path.join(dir_osdi, "ac_sim.ngspice"), sep="\\s+") + ac_data_built_in = pd.read_csv(os.path.join(dir_osdi, "ac_sim.ngspice"), sep="\\s+") + # ac_data_built_in = pd.read_csv( + # os.path.join(dir_built_in, "ac_sim.ngspice"), sep="\\s+" + # ) + + # read TR simulation results + tr_data_osdi = pd.read_csv(os.path.join(dir_osdi, "tr_sim.ngspice"), sep="\\s+") + tr_data_built_in = pd.read_csv(os.path.join(dir_osdi, "tr_sim.ngspice"), sep="\\s+") + # tr_data_built_in = pd.read_csv( + # os.path.join(dir_built_in, "tr_sim.ngspice"), sep="\\s+" + # ) + + # test simulation results + id_osdi = dc_data_osdi["i(vsense)"].to_numpy() + id_built_in = dc_data_built_in["i(vsense)"].to_numpy() + np.testing.assert_allclose(id_osdi[0:20], id_built_in[0:20], rtol=0.01) + + return ( + dc_data_osdi, + dc_data_built_in, + ac_data_osdi, + ac_data_built_in, + tr_data_osdi, + tr_data_built_in, + ) + + +if __name__ == "__main__": + ( + dc_data_osdi, + dc_data_built_in, + ac_data_osdi, + ac_data_built_in, + tr_data_osdi, + tr_data_built_in, + ) = test_ngspice() + + import matplotlib.pyplot as plt + + # DC Plot + pd_built_in = dc_data_built_in["v(d)"] * dc_data_built_in["i(vsense)"] + pd_osdi = dc_data_osdi["v(d)"] * dc_data_osdi["i(vsense)"] + fig, ax1 = plt.subplots() + ax1.plot( + dc_data_built_in["v(d)"], + dc_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + ax1.plot( + dc_data_built_in["v(d)"], + dc_data_built_in["v(d)"] / 10 * 1e3, + label="analytical", + linestyle="--", + marker="s", + ) + ax1.plot( + dc_data_osdi["v(d)"], + dc_data_osdi["i(vsense)"] * 1e3, + label="OSDI", + ) + ax1.set_ylabel(r"$I_{\mathrm{P}} (\mathrm{mA})$") + ax1.set_xlabel(r"$V_{\mathrm{PM}}(\mathrm{V})$") + plt.legend() + + # AC Plot + omega = 2 * np.pi * ac_data_osdi["frequency"] + z_analytical = 1 / 10 + fig = plt.figure() + plt.semilogx( + ac_data_built_in["frequency"], + ac_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.semilogx( + ac_data_built_in["frequency"], + np.ones_like(ac_data_built_in["frequency"]) * z_analytical * 1e3, + label="analytical", + linestyle="--", + marker="s", + ) + plt.semilogx( + ac_data_osdi["frequency"], ac_data_osdi["i(vsense)"] * 1e3, label="OSDI" + ) + plt.xlabel("$f(\\mathrm{H})$") + plt.ylabel("$\\Re \\left\{ Y_{11} \\right\} (\\mathrm{mS})$") + plt.legend() + fig = plt.figure() + plt.semilogx( + ac_data_built_in["frequency"], + ac_data_built_in["i(vsense).1"] * 1e12 / omega, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.semilogx( + ac_data_built_in["frequency"], + np.zeros_like(ac_data_built_in["i(vsense).1"]) * 1e12 / omega, + label="analytical", + linestyle="--", + marker="s", + ) + plt.semilogx( + ac_data_osdi["frequency"], + ac_data_osdi["i(vsense).1"] * 1e12 / omega, + label="OSDI", + ) + plt.ylim(-1, 1) + plt.xlabel("$f(\\mathrm{H})$") + plt.ylabel("$\\Im\\left\{Y_{11}\\right\}/(\\omega) (\\mathrm{pF})$") + plt.legend() + + # TR plot + fig = plt.figure() + plt.plot( + tr_data_built_in["time"] * 1e9, + tr_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.plot( + tr_data_built_in["time"] * 1e9, + tr_data_built_in["v(d)"] / 10 * 1e3, + label="analytical", + linestyle="--", + marker="s", + ) + plt.plot( + tr_data_osdi["time"] * 1e9, + tr_data_osdi["i(vsense)"] * 1e3, + label="OSDI", + ) + plt.xlabel(r"$t(\mathrm{nS})$") + plt.ylabel(r"$I_{\mathrm{D}}(\mathrm{mA})$") + plt.legend() + + plt.show() diff --git a/test_cases/resistor/netlist.sp b/test_cases/resistor/netlist.sp new file mode 100644 index 000000000..5c466ee19 --- /dev/null +++ b/test_cases/resistor/netlist.sp @@ -0,0 +1,42 @@ +OSDI Resistor Test +.options abstol=1e-15 + + +* one voltage source for sweeping, one for sensing: +VD Dx 0 DC 0 AC 1 SIN (0.5 0.2 1M) +Vsense Dx D DC 0 + +* model definitions: +.model rmod_osdi osdi resistor_va r=10 + +*OSDI Resistor: +*OSDI_ACTIVATE*A1 D 0 rmod_osdi + +*Built-in Capacitor: +*BUILT_IN_ACTIVATE*R1 D 0 10 + + +.control +set filetype=ascii +set wr_vecnames +set wr_singlescale + +* a DC sweep from 0.3V to 1V +dc Vd 0.3 1.0 0.01 +wrdata dc_sim.ngspice v(d) i(vsense) + +* an AC sweep at Vd=0.5V +alter VD=0.5 +ac dec 10 .01 10 +wrdata ac_sim.ngspice v(d) i(vsense) + +* a transient analysis +tran 100ms 500000ms +wrdata tr_sim.ngspice v(d) i(vsense) + +* print number of iterations +rusage totiter + +.endc + +.end \ No newline at end of file diff --git a/test_cases/resistor/resistor_va.c b/test_cases/resistor/resistor_va.c new file mode 100644 index 000000000..6daf45017 --- /dev/null +++ b/test_cases/resistor/resistor_va.c @@ -0,0 +1,357 @@ +/* + * Copyright© 2022 SemiMod UG. All rights reserved. + * + * This is an exemplary implementation of the OSDI interface for the Verilog-A + * model specified in diode.va. In the future, the OpenVAF compiler shall + * generate an comparable object file. Primary purpose of this is example to + * have a concrete example for the OSDI interface, OpenVAF will generate a more + * optimized implementation. + * + */ + +#include "osdi.h" +#include "string.h" +#include +#include +#include +#include +#include + +// public interface +extern uint32_t OSDI_VERSION_MAJOR; +extern uint32_t OSDI_VERSION_MINOR; +extern uint32_t OSDI_NUM_DESCRIPTORS; +extern OsdiDescriptor OSDI_DESCRIPTORS[1]; + +// number of nodes and definitions of node ids for nicer syntax in this file +// note: order should be same as "nodes" list defined later +#define NUM_NODES 3 +#define P 0 +#define M 1 + +// number of matrix entries and definitions for Jacobian entries for nicer +// syntax in this file +#define NUM_MATRIX 4 +#define P_P 0 +#define P_M 1 +#define M_P 2 +#define M_M 3 + +// The model structure for the diode +typedef struct ResistorModel +{ + double R; + bool R_given; +} ResistorModel; + +// The instace structure for the diode +typedef struct ResistorInstance +{ + double temperature; + double rhs_resist[NUM_NODES]; + double rhs_react[NUM_NODES]; + double jacobian_resist[NUM_MATRIX]; + double jacobian_react[NUM_MATRIX]; + double *jacobian_ptr_resist[NUM_MATRIX]; + double *jacobian_ptr_react[NUM_MATRIX]; + uint32_t node_off[NUM_NODES]; +} ResistorInstance; + +// implementation of the access function as defined by the OSDI spec +void *osdi_access(void *inst_, void *model_, uint32_t id, uint32_t flags) +{ + ResistorModel *model = (ResistorModel *)model_; + ResistorInstance *inst = (ResistorInstance *)inst_; + + bool *given; + void *value; + + switch (id) // id of params defined in param_opvar array + { + case 0: + value = (void *)&model->R; + given = &model->R_given; + break; + default: + return NULL; + } + + if (flags & ACCESS_FLAG_SET) + { + *given = true; + } + + return value; +} + +// implementation of the setup_model function as defined in the OSDI spec +OsdiInitInfo setup_model(void *_handle, void *model_) +{ + ResistorModel *model = (ResistorModel *)model_; + + // set parameters and check bounds + if (!model->R_given) + { + model->R = 1; + } + return (OsdiInitInfo){.flags = 0, .num_errors = 0, .errors = NULL}; +} + +// implementation of the setup_instace function as defined in the OSDI spec +OsdiInitInfo setup_instance(void *_handle, void *inst_, void *model_, + double temperature, uint32_t _num_terminals) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + ResistorModel *model = (ResistorModel *)model_; + + inst->temperature = temperature; + return (OsdiInitInfo){.flags = 0, .num_errors = 0, .errors = NULL}; +} + +// implementation of the eval function as defined in the OSDI spec +uint32_t eval(void *handle, void *inst_, void *model_, uint32_t flags, + double *prev_solve, OsdiSimParas *sim_params) +{ + ResistorModel *model = (ResistorModel *)model_; + ResistorInstance *inst = (ResistorInstance *)inst_; + + // get voltages + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + double vpm = vp - vm; + + double ir = vpm / model->R; + double g = 1 / model->R; + + //////////////// + // write rhs + //////////////// + + if (flags & CALC_RESIST_RESIDUAL) + { + // write resist rhs + inst->rhs_resist[P] = ir; + inst->rhs_resist[M] = -ir; + } + + ////////////////// + // write Jacobian + ////////////////// + + if (flags & CALC_RESIST_JACOBIAN) + { + // stamp resistor + inst->jacobian_resist[P_P] = g; + inst->jacobian_resist[P_M] = -g; + inst->jacobian_resist[M_P] = -g; + inst->jacobian_resist[M_M] = g; + } + + return 0; +} + +// TODO implementation of the load_noise function as defined in the OSDI spec +void load_noise(void *inst, void *model, double freq, double *noise_dens, + double *ln_noise_dens) +{ + // TODO add noise to example +} + +#define LOAD_RHS_RESIST(name) \ + dst[inst->node_off[name]] += inst->rhs_resist[name]; + +// implementation of the load_rhs_resist function as defined in the OSDI spec +void load_residual_resist(void *inst_, double *dst) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + + LOAD_RHS_RESIST(P) + LOAD_RHS_RESIST(M) +} + +#define LOAD_RHS_REACT(name) dst[inst->node_off[name]] += inst->rhs_react[name]; + +// implementation of the load_rhs_react function as defined in the OSDI spec +void load_residual_react(void *inst_, double *dst) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + + LOAD_RHS_REACT(P) + LOAD_RHS_REACT(M) +} + +#define LOAD_MATRIX_RESIST(name) \ + *inst->jacobian_ptr_resist[name] += inst->jacobian_resist[name]; + +// implementation of the load_matrix_resist function as defined in the OSDI spec +void load_jacobian_resist(void *inst_) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + LOAD_MATRIX_RESIST(P_P) + LOAD_MATRIX_RESIST(P_M) + LOAD_MATRIX_RESIST(M_P) + LOAD_MATRIX_RESIST(M_M) +} + +#define LOAD_MATRIX_REACT(name) \ + *inst->jacobian_ptr_react[name] += inst->jacobian_react[name] * alpha; + +// implementation of the load_matrix_react function as defined in the OSDI spec +void load_jacobian_react(void *inst_, double alpha) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + LOAD_MATRIX_REACT(P_P) + LOAD_MATRIX_REACT(M_M) + LOAD_MATRIX_REACT(P_M) + LOAD_MATRIX_REACT(M_P) +} + +#define LOAD_MATRIX_TRAN(name) \ + *inst->jacobian_ptr_resist[name] += inst->jacobian_react[name] * alpha; + +// implementation of the load_matrix_tran function as defined in the OSDI spec +void load_jacobian_tran(void *inst_, double alpha) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + + // set dc stamps + load_jacobian_resist(inst_); + + // add reactive contributions + LOAD_MATRIX_TRAN(P_P) + LOAD_MATRIX_TRAN(M_M) + LOAD_MATRIX_TRAN(M_P) + LOAD_MATRIX_TRAN(M_M) +} + +// implementation of the load_spice_rhs_dc function as defined in the OSDI spec +void load_spice_rhs_dc(void *inst_, double *dst, double *prev_solve) +{ + ResistorInstance *inst = (ResistorInstance *)inst_; + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + dst[inst->node_off[P]] += inst->jacobian_resist[P_M] * vm + + inst->jacobian_resist[P_P] * vp - + inst->rhs_resist[P]; + + dst[inst->node_off[M]] += inst->jacobian_resist[M_P] * vp + + inst->jacobian_resist[M_M] * vm - + inst->rhs_resist[M]; +} + +// implementation of the load_spice_rhs_tran function as defined in the OSDI +// spec +void load_spice_rhs_tran(void *inst_, double *dst, double *prev_solve, + double alpha) +{ + + ResistorInstance *inst = (ResistorInstance *)inst_; + double vp = prev_solve[inst->node_off[P]]; + double vm = prev_solve[inst->node_off[M]]; + + // set DC rhs + load_spice_rhs_dc(inst_, dst, prev_solve); + + // add contributions due to reactive elements + dst[inst->node_off[P]] += + alpha * (inst->jacobian_react[P_P] * vp + + inst->jacobian_react[P_M] * vm); + + dst[inst->node_off[M]] += alpha * (inst->jacobian_react[M_M] * vm + + inst->jacobian_react[M_P] * vp); +} + +// structure that provides information of all nodes of the model +OsdiNode nodes[NUM_NODES] = { + {.name = "P", .units = "V", .is_reactive = false}, + {.name = "M", .units = "V", .is_reactive = false}, +}; + +// boolean array that tells which Jacobian entries are constant. Nothing is +// constant with selfheating, though. +bool const_jacobian_entries[NUM_MATRIX] = {}; +// these node pairs specify which entries in the Jacobian must be accounted for +OsdiNodePair jacobian_entries[NUM_MATRIX] = { + {P, P}, + {P, M}, + {M, P}, + {M, M}, +}; + +#define NUM_PARAMS 1 +// the model parameters as defined in Verilog-A, bounds and default values are +// stored elsewhere as they may depend on model parameters etc. +OsdiParamOpvar params[NUM_PARAMS] = { + { + .name = (char *[]){"R"}, + .num_alias = 0, + .description = "Resistance", + .units = "Ohm", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, +}; + +// fill exported data +uint32_t OSDI_VERSION_MAJOR = OSDI_VERSION_MAJOR_CURR; +uint32_t OSDI_VERSION_MINOR = OSDI_VERSION_MINOR_CURR; +uint32_t OSDI_NUM_DESCRIPTORS = 1; +// this is the main structure used by simulators, it gives access to all +// information in a model +OsdiDescriptor OSDI_DESCRIPTORS[1] = {{ + // metadata + .name = "resistor_va", + + // nodes + .num_nodes = NUM_NODES, + .num_terminals = 2, + .nodes = (OsdiNode *)&nodes, + + // matrix entries + .num_jacobian_entries = NUM_MATRIX, + .jacobian_entries = (OsdiNodePair *)&jacobian_entries, + .const_jacobian_entries = (bool *)&const_jacobian_entries, + + // memory + .instance_size = sizeof(ResistorInstance), + .model_size = sizeof(ResistorModel), + .residual_resist_offset = offsetof(ResistorInstance, rhs_resist), + .residual_react_offset = offsetof(ResistorInstance, rhs_react), + .node_mapping_offset = offsetof(ResistorInstance, node_off), + .jacobian_resist_offset = offsetof(ResistorInstance, jacobian_resist), + .jacobian_react_offset = offsetof(ResistorInstance, jacobian_react), + .jacobian_ptr_resist_offset = offsetof(ResistorInstance, jacobian_ptr_resist), + .jacobian_ptr_react_offset = offsetof(ResistorInstance, jacobian_ptr_react), + + // TODO add node collapsing to example + // node collapsing + .num_collapsible = 0, + .collapsible = NULL, + .is_collapsible_offset = 0, + + // noise + .noise_sources = NULL, + .num_noise_src = 0, + + // parameters and op vars + .num_params = NUM_PARAMS, + .num_instance_params = 0, + .num_opvars = 0, + .param_opvar = (OsdiParamOpvar *)¶ms, + + // setup + .access = &osdi_access, + .setup_model = &setup_model, + .setup_instance = &setup_instance, + .eval = &eval, + .load_noise = &load_noise, + .load_residual_resist = &load_residual_resist, + .load_residual_react = &load_residual_react, + .load_spice_rhs_dc = &load_spice_rhs_dc, + .load_spice_rhs_tran = &load_spice_rhs_tran, + .load_jacobian_resist = &load_jacobian_resist, + .load_jacobian_react = &load_jacobian_react, + .load_jacobian_tran = &load_jacobian_tran, +}}; diff --git a/test_cases/resistor/test_resistor.py b/test_cases/resistor/test_resistor.py new file mode 100644 index 000000000..9542f1967 --- /dev/null +++ b/test_cases/resistor/test_resistor.py @@ -0,0 +1,245 @@ +""" test OSDI simulation of resistor +""" +import os, shutil +import numpy as np +import subprocess +import pandas as pd + +directory = os.path.dirname(__file__) + +# This test runs a DC, AC and Transient Simulation of a simple resistor. +# The capacitor is available as a C file and needs to be compiled to a shared object +# and then bet put into /usr/local/share/ngspice/osdi: +# +# > make osdi_resistor +# > cp resistor_osdi.so /usr/local/share/ngspice/osdi/resistor_osdi.so +# +# The integration test proves the functioning of the OSDI interface. +# Future tests will target Verilog-A models like HICUM/L2 that should yield exactly the same results as the Ngspice implementation. + + +def create_shared_object(): + # place the file "resistor_va.c" next to this file + subprocess.run( + [ + "gcc", + "-c", + "-Wall", + "-I", + "../../src/spicelib/devices/osdi/", + "-fpic", + "resistor_va.c", + "-ggdb", + ], + cwd=directory, + ) + subprocess.run( + ["gcc", "-shared", "-o", "resistor_va.so", "resistor_va.o", "-ggdb"], + cwd=directory, + ) + os.makedirs(os.path.join(directory, "test_osdi", "osdi"), exist_ok=True) + subprocess.run( + ["mv", "resistor_va.so", "test_osdi/osdi/resistor_va.so"], cwd=directory + ) + subprocess.run(["rm", "resistor_va.o"], cwd=directory) + + +# specify location of Ngspice executable to be tested +ngspice_path = os.path.join(directory, "../../debug/src/ngspice") +ngspice_path = os.path.abspath(ngspice_path) + + +def test_ngspice(): + path_netlist = os.path.join(directory, "netlist.sp") + + # open netlist and activate Ngspice resistor + with open(path_netlist) as netlist_handle: + netlist_raw = netlist_handle.read() + + netlist_osdi = netlist_raw.replace("*OSDI_ACTIVATE*", "") + netlist_built_in = netlist_raw.replace("*BUILT_IN_ACTIVATE*", "") + + # make directories for test cases + dir_osdi = os.path.join(directory, "test_osdi") + dir_built_in = os.path.join(directory, "test_built_in") + # remove old results: + for directory_i in [dir_osdi, dir_built_in]: + shutil.rmtree(directory_i, ignore_errors=True) + + for directory_i in [dir_osdi, dir_built_in]: + os.makedirs(directory_i, exist_ok=True) + + create_shared_object() + + # write netlists + with open(os.path.join(dir_osdi, "netlist.sp"), "w") as netlist_handle: + netlist_handle.write(netlist_osdi) + + with open(os.path.join(dir_built_in, "netlist.sp"), "w") as netlist_handle: + netlist_handle.write(netlist_built_in) + + # run simulations with Ngspice + for directory_i in [dir_osdi, dir_built_in]: + subprocess.run( + [ + ngspice_path, + "netlist.sp", + "-b", + ], + cwd=directory_i, + ) + + # read DC simulation results + dc_data_osdi = pd.read_csv(os.path.join(dir_osdi, "dc_sim.ngspice"), sep="\\s+") + dc_data_built_in = pd.read_csv(os.path.join(dir_osdi, "dc_sim.ngspice"), sep="\\s+") + # dc_data_built_in = pd.read_csv( + # os.path.join(dir_built_in, "dc_sim.ngspice"), sep="\\s+" + # ) + + id_osdi = dc_data_osdi["i(vsense)"].to_numpy() + id_built_in = dc_data_osdi["i(vsense)"].to_numpy() + # id_built_in = dc_data_built_in["i(vsense)"].to_numpy() + + # read AC simulation results + ac_data_osdi = pd.read_csv(os.path.join(dir_osdi, "ac_sim.ngspice"), sep="\\s+") + ac_data_built_in = pd.read_csv(os.path.join(dir_osdi, "ac_sim.ngspice"), sep="\\s+") + # ac_data_built_in = pd.read_csv( + # os.path.join(dir_built_in, "ac_sim.ngspice"), sep="\\s+" + # ) + + # read TR simulation results + tr_data_osdi = pd.read_csv(os.path.join(dir_osdi, "tr_sim.ngspice"), sep="\\s+") + tr_data_built_in = pd.read_csv(os.path.join(dir_osdi, "tr_sim.ngspice"), sep="\\s+") + # tr_data_built_in = pd.read_csv( + # os.path.join(dir_built_in, "tr_sim.ngspice"), sep="\\s+" + # ) + + # test simulation results + id_osdi = dc_data_osdi["i(vsense)"].to_numpy() + id_built_in = dc_data_built_in["i(vsense)"].to_numpy() + np.testing.assert_allclose(id_osdi[0:20], id_built_in[0:20], rtol=0.01) + + return ( + dc_data_osdi, + dc_data_built_in, + ac_data_osdi, + ac_data_built_in, + tr_data_osdi, + tr_data_built_in, + ) + + +if __name__ == "__main__": + ( + dc_data_osdi, + dc_data_built_in, + ac_data_osdi, + ac_data_built_in, + tr_data_osdi, + tr_data_built_in, + ) = test_ngspice() + + import matplotlib.pyplot as plt + + # DC Plot + pd_built_in = dc_data_built_in["v(d)"] * dc_data_built_in["i(vsense)"] + pd_osdi = dc_data_osdi["v(d)"] * dc_data_osdi["i(vsense)"] + fig, ax1 = plt.subplots() + ax1.plot( + dc_data_built_in["v(d)"], + dc_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + ax1.plot( + dc_data_built_in["v(d)"], + dc_data_built_in["v(d)"] / 10 * 1e3, + label="analytical", + linestyle="--", + marker="s", + ) + ax1.plot( + dc_data_osdi["v(d)"], + dc_data_osdi["i(vsense)"] * 1e3, + label="OSDI", + ) + ax1.set_ylabel(r"$I_{\mathrm{P}} (\mathrm{mA})$") + ax1.set_xlabel(r"$V_{\mathrm{PM}}(\mathrm{V})$") + plt.legend() + + # AC Plot + omega = 2 * np.pi * ac_data_osdi["frequency"] + z_analytical = 1 / 10 + fig = plt.figure() + plt.semilogx( + ac_data_built_in["frequency"], + ac_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.semilogx( + ac_data_built_in["frequency"], + np.ones_like(ac_data_built_in["frequency"]) * z_analytical * 1e3, + label="analytical", + linestyle="--", + marker="s", + ) + plt.semilogx( + ac_data_osdi["frequency"], ac_data_osdi["i(vsense)"] * 1e3, label="OSDI" + ) + plt.xlabel("$f(\\mathrm{H})$") + plt.ylabel("$\\Re \\left\{ Y_{11} \\right\} (\\mathrm{mS})$") + plt.legend() + fig = plt.figure() + plt.semilogx( + ac_data_built_in["frequency"], + ac_data_built_in["i(vsense).1"] * 1e12 / omega, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.semilogx( + ac_data_built_in["frequency"], + np.zeros_like(ac_data_built_in["i(vsense).1"]) * 1e12 / omega, + label="analytical", + linestyle="--", + marker="s", + ) + plt.semilogx( + ac_data_osdi["frequency"], + ac_data_osdi["i(vsense).1"] * 1e12 / omega, + label="OSDI", + ) + plt.ylim(-1, 1) + plt.xlabel("$f(\\mathrm{H})$") + plt.ylabel("$\\Im\\left\{Y_{11}\\right\}/(\\omega) (\\mathrm{pF})$") + plt.legend() + + # TR plot + fig = plt.figure() + plt.plot( + tr_data_built_in["time"] * 1e9, + tr_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.plot( + tr_data_built_in["time"] * 1e9, + tr_data_built_in["v(d)"] / 10 * 1e3, + label="analytical", + linestyle="--", + marker="s", + ) + plt.plot( + tr_data_osdi["time"] * 1e9, + tr_data_osdi["i(vsense)"] * 1e3, + label="OSDI", + ) + plt.xlabel(r"$t(\mathrm{nS})$") + plt.ylabel(r"$I_{\mathrm{D}}(\mathrm{mA})$") + plt.legend() + + plt.show()