diff --git a/.gitignore b/.gitignore index 3af70316b..62177c06b 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,7 @@ src/spicelib/parser/inpptree-parser.c src/spicelib/parser/inpptree-parser.h # Visual Studio Code user options files .vscode/ + +test_cases/diode/__pycache__/* +test_cases/diode/test_osdi/* +test_cases/diode/test_built_in/* diff --git a/AUTHORS b/AUTHORS index 85c939ed7..a7aeec540 100644 --- a/AUTHORS +++ b/AUTHORS @@ -74,6 +74,7 @@ Dietmar Warning, Michael Widlok, Charles D.H. Williams, Antony Wilson, +Pascal Kuthe, and many others... If someone helped in the development and has not been inserted in this list diff --git a/COPYING b/COPYING index da4b0aa1c..fc558c85b 100644 --- a/COPYING +++ b/COPYING @@ -28,6 +28,9 @@ unnamed MIT license, compatible to New BSD ngspice/src/spicelib/devices/adms/admst LGPLv2.1 +* all files in ngspice/src/osdi +MPLv2.0 + ngspice/src/spicelib/devices/ndev public domain @@ -565,3 +568,380 @@ LICENSE permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty. + + +-----------------------------MPLv2.0 - OSDI-------------------------------- + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..0d6c3e9e9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.10.4-bullseye + +# python installation +RUN apt-get update && apt-get -y install bc bison flex libxaw7 libxaw7-dev libx11-6 libx11-dev libreadline8 libxmu6 +RUN apt-get update && apt-get -y install build-essential libtool gperf libxml2 libxml2-dev libxml-libxml-perl libgd-perl +RUN apt-get update && apt-get -y install g++ gfortran make cmake libfl-dev libfftw3-dev + +RUN pip install pytest numpy pandas diff --git a/README_OSDI.md b/README_OSDI.md new file mode 100644 index 000000000..8c529b37a --- /dev/null +++ b/README_OSDI.md @@ -0,0 +1,33 @@ +# OSDI implementation for NGSPICE + +OSDI (Open Source Device Interface) is a simulator independent device interface, that is used by the OpenVAF compiler. +Implementing this interface in NGSPICE allows loading Verilog-A models compiled by OpenVAF. +The interface is fixed and does not require the compiler to know about NGSPICE during compilation. +NGSPICE also doesn't need to know anything about the compiled models at compilation. +Therefore, these models can be loaded dynamically at runtime. + +To that end the `osdi` command is provided. +It allows loading a dynamic library conforming to OSDI. +Example usage: `osdi diode.osdi`. + +If used within a netlist the command requires the `pre_` prefix. +This ensures that the devices are loaded before the netlist is parsed. + +Example usage: `pre_osdi diode.osdi` + +If a relative path is provided to the `osdi` command in a netlist, it will resolve that path **relative to the netlist**, not relative to current working directory. +This ensures that netlists can be simulated from any directory + +## Build Instructions + +To compile NGSPICE with OSDI support ensure that the `--enable-predictor` and `--enable-osdi` flags are used. +The `compile_linus.sh` file enables these flags by default. + + +## Example/Test Case + +A simple handwritten diode can be found in `test_cases/diode/diode.c`. +In the same directory a script named `test_diode.py` is provided that will compile this model and run some example simulations. +After the script has finished the compilation result `diode.osdi` and the netlist can then be found in `test_cases/diode/test_osdi`. + + diff --git a/compile_linux.sh b/compile_linux.sh index 9c8cb9c54..06c0d5b6d 100755 --- a/compile_linux.sh +++ b/compile_linux.sh @@ -52,14 +52,14 @@ if test "$1" = "d"; then echo "configuring for 64 bit debug" echo # You may add --enable-adms to the following command for adding adms generated devices - ../configure --with-x --enable-xspice --enable-cider --with-readline=yes --enable-openmp CFLAGS="-g -m64 -O0 -Wall -Wno-unused-but-set-variable" LDFLAGS="-m64 -g" + ../configure --with-x --enable-xspice --enable-cider --enable-predictor --enable-osdi --with-readline=yes --enable-openmp CFLAGS="-g -m64 -O0 -Wall -Wno-unused-but-set-variable" LDFLAGS="-m64 -g" else cd release if [ $? -ne 0 ]; then echo "cd release failed"; exit 1 ; fi echo "configuring for 64 bit release" echo # You may add --enable-adms to the following command for adding adms generated devices - ../configure --with-x --enable-xspice --enable-cider --with-readline=yes --enable-openmp --disable-debug CFLAGS="-m64 -O2" LDFLAGS="-m64 -s" + ../configure --with-x --enable-xspice --enable-cider --enable-predictor --enable-osdi --with-readline=yes --enable-openmp --disable-debug CFLAGS="-m64 -O2" LDFLAGS="-m64 -s" fi if [ $? -ne 0 ]; then echo "../configure failed"; exit 1 ; fi diff --git a/configure.ac b/configure.ac index a4e3dca72..46bc3d26f 100644 --- a/configure.ac +++ b/configure.ac @@ -146,6 +146,10 @@ AC_ARG_ENABLE([oldapps], AC_ARG_ENABLE([xspice], [AS_HELP_STRING([--enable-xspice], [Enable XSPICE enhancements])]) +# --enable-osdi: define OSDI in the code. This is for osdi support +AC_ARG_ENABLE([osdi], + [AS_HELP_STRING([--enable-osdi], [Enable OSDI integration])]) + # --enable-cider: define CIDER in the code. This is for CIDER support AC_ARG_ENABLE([cider], [AS_HELP_STRING([--enable-cider], [Enable CIDER enhancements])]) @@ -1156,7 +1160,18 @@ if test "x$enable_xspice" = xyes; then AC_SUBST([VIS_CFLAGS]) else XSPICEINIT="*" + if test "x$enable_osdi" = xyes; then\ + case $host_os in + *mingw* | *msys* | *cygwin* | *solaris* ) + XSPICEDLLIBS="" + ;; + * ) + XSPICEDLLIBS="-ldl" + ;; + esac + fi fi + AC_SUBST([XSPICEINIT]) AC_SUBST([XSPICEDLLIBS]) @@ -1165,6 +1180,36 @@ AM_CONDITIONAL([XSPICE_WANTED], [test "x$enable_xspice" = xyes]) AM_CONDITIONAL([SHORT_CHECK_WANTED], [test "x$enable_shortcheck" = xyes]) +if test "x$enable_osdi" = xyes; then + AC_DEFUN([AX_CHECK_COMPILE_FLAG], + [AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF + AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_[]_AC_LANG_ABBREV[]flags_$4_$1])dnl + AC_CACHE_CHECK([whether _AC_LANG compiler accepts $1], CACHEVAR, [ + ax_check_save_flags=$[]_AC_LANG_PREFIX[]FLAGS + _AC_LANG_PREFIX[]FLAGS="$[]_AC_LANG_PREFIX[]FLAGS $4 $1" + AC_COMPILE_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])], + [AS_VAR_SET(CACHEVAR,[yes])], + [AS_VAR_SET(CACHEVAR,[no])]) + _AC_LANG_PREFIX[]FLAGS=$ax_check_save_flags]) + AS_VAR_IF(CACHEVAR,yes, + [m4_default([$2], :)], + [m4_default([$3], :)]) + AS_VAR_POPDEF([CACHEVAR])dnl + ])dnl AX_CHECK_COMPILE_FLAGS + + AX_CHECK_COMPILE_FLAG([-std=c11], [ + CFLAGS="$CFLAGS -std=c11" + LDLAGS="$LDLAGS -std=c11" + ], [ + echo "C compiler cannot compile C11 code" + exit -1 + ]) + AC_MSG_RESULT([OSDI features included]) + AC_DEFINE([OSDI], [1], [The OSDI enhancements]) +fi + +AM_CONDITIONAL([OSDI_WANTED], [test "x$enable_osdi" = xyes]) + # Add CIDER enhancements to ngspice. if test "x$enable_cider" = xyes; then AC_MSG_RESULT([CIDER features enabled]) @@ -1444,6 +1489,7 @@ AC_CONFIG_FILES([Makefile src/xspice/enh/Makefile src/xspice/ipc/Makefile src/xspice/idn/Makefile + src/osdi/Makefile tests/Makefile tests/bsim1/Makefile tests/bsim2/Makefile diff --git a/src/Makefile.am b/src/Makefile.am index 1f0e53588..c25b7b3db 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -7,6 +7,10 @@ if XSPICE_WANTED SUBDIRS += xspice endif +if OSDI_WANTED +SUBDIRS += osdi +endif + if CIDER_WANTED SUBDIRS += ciderlib endif @@ -97,6 +101,8 @@ DYNAMIC_DEVICELIBS = \ @VLADEV@ + + ## Build ngspice first: ## compile the icon: @@ -116,6 +122,8 @@ ngspice_CPPFLAGS = $(AM_CPPFLAGS) -DSIMULATOR if WINGUI ngspice_LDFLAGS = -municode $(AM_LDFLAGS) ngspice_SOURCES += winmain.c hist_info.c +else +ngspice_LDFLAGS = endif ngspice_LDADD = \ @@ -157,9 +165,9 @@ ngspice_LDADD += \ xspice/evt/libevtxsp.la \ xspice/enh/libenhxsp.la \ xspice/ipc/libipcxsp.la \ - xspice/idn/libidnxsp.la \ - @XSPICEDLLIBS@ + xspice/idn/libidnxsp.la endif +ngspice_LDADD += @XSPICEDLLIBS@ ngspice_LDADD += \ frontend/parser/libparser.la \ @@ -175,6 +183,10 @@ ngspice_LDADD += \ ciderlib/support/libcidersuprt.la endif +if OSDI_WANTED +ngspice_LDADD += osdi/libosdi.la +endif + ngspice_LDADD += \ maths/deriv/libderiv.la \ maths/cmaths/libcmaths.la \ @@ -439,9 +451,9 @@ libspice_la_LIBADD += \ xspice/evt/libevtxsp.la \ xspice/enh/libenhxsp.la \ xspice/ipc/libipcxsp.la \ - xspice/idn/libidnxsp.la \ - @XSPICEDLLIBS@ + xspice/idn/libidnxsp.la endif +libspice_la_LIBADD += @XSPICEDLLIBS@ libspice_la_LIBADD += \ frontend/parser/libparser.la \ @@ -457,6 +469,11 @@ libspice_la_LIBADD += \ ciderlib/support/libcidersuprt.la endif + +if OSDI_WANTED +libspice_la_LIBADD += osdi/libosdi.la +endif + libspice_la_LIBADD += \ maths/deriv/libderiv.la \ maths/cmaths/libcmaths.la \ @@ -558,9 +575,9 @@ libngspice_la_LIBADD += \ xspice/evt/libevtxsp.la \ xspice/enh/libenhxsp.la \ xspice/ipc/libipcxsp.la \ - xspice/idn/libidnxsp.la \ - @XSPICEDLLIBS@ + xspice/idn/libidnxsp.la endif +libngspice_la_LIBADD += @XSPICEDLLIBS@ libngspice_la_LIBADD += \ frontend/parser/libparser.la \ @@ -576,6 +593,9 @@ libngspice_la_LIBADD += \ ciderlib/support/libcidersuprt.la endif + +libngspice_la_LIBADD += osdi/libosdi.la + libngspice_la_LIBADD += \ maths/deriv/libderiv.la \ maths/cmaths/libcmaths.la \ diff --git a/src/frontend/com_dl.c b/src/frontend/com_dl.c index 00ef5b6e3..2cdca00a7 100644 --- a/src/frontend/com_dl.c +++ b/src/frontend/com_dl.c @@ -18,6 +18,21 @@ void com_codemodel(wordlist *wl) } #endif +#ifdef OSDI +void com_osdi(wordlist *wl) +{ + wordlist *ww; + for (ww = wl; ww; ww = ww->wl_next) + if (load_osdi(ww->wl_word)) { + fprintf(cp_err, "Error: Library %s couldn't be loaded!\n", ww->wl_word); + if (ft_stricterror) + controlled_exit(EXIT_BAD); + } +} +#endif + + + #ifdef DEVLIB void com_use(wordlist *wl) diff --git a/src/frontend/com_dl.h b/src/frontend/com_dl.h index fcb9b1ce8..91028de3c 100644 --- a/src/frontend/com_dl.h +++ b/src/frontend/com_dl.h @@ -5,6 +5,10 @@ void com_codemodel(wordlist *wl); #endif +#ifdef OSDI +void com_osdi(wordlist *wl); +#endif + #ifdef DEVLIB void com_use(wordlist *wl); #endif diff --git a/src/frontend/commands.c b/src/frontend/commands.c index c6754ef08..028f24071 100644 --- a/src/frontend/commands.c +++ b/src/frontend/commands.c @@ -268,6 +268,12 @@ struct comm spcp_coms[] = { NULL, "library library ... : Loads the code model libraries." } , #endif +#ifdef OSDI + { "osdi", com_osdi, FALSE, TRUE, + { 040000, 040000, 040000, 040000 }, E_BEGINNING, 1, LOTS, + NULL, + "library library ... : Loads a osdi library." } , +#endif #ifdef DEVLIB { "use", com_use, FALSE, TRUE, { 040000, 040000, 040000, 040000 }, E_BEGINNING, 1, LOTS, diff --git a/src/frontend/inp.c b/src/frontend/inp.c index af7dc24e9..86652c08e 100644 --- a/src/frontend/inp.c +++ b/src/frontend/inp.c @@ -18,6 +18,7 @@ Author: 1985 Wayne A. Christopher #include "ngspice/fteinp.h" #include "inp.h" +#include "ngspice/osdiitf.h" #include "runcoms.h" #include "inpcom.h" #include "circuits.h" @@ -565,7 +566,6 @@ inp_spsource(FILE *fp, bool comfile, char *filename, bool intfile) if (fp) { cp_vset("inputdir", CP_STRING, dir_name); } - tfree(dir_name); /* if nothing came back from inp_readall, e.g. after calling ngspice without parameters, just close fp and return to caller */ @@ -740,8 +740,16 @@ inp_spsource(FILE *fp, bool comfile, char *filename, bool intfile) before the circuit structure is set up */ if (pre_controls) { pre_controls = wl_reverse(pre_controls); - for (wl = pre_controls; wl; wl = wl->wl_next) + for (wl = pre_controls; wl; wl = wl->wl_next){ +#ifdef OSDI + inputdir = dir_name; +#endif cp_evloop(wl->wl_word); + } + +#ifdef OSDI + inputdir = NULL; +#endif wl_free(pre_controls); } @@ -1108,18 +1116,29 @@ inp_spsource(FILE *fp, bool comfile, char *filename, bool intfile) of commands. Thus this is delegated to a function using a third thread, that only starts when the background thread has finished (sharedspice.c).*/ #ifdef SHARED_MODULE - for (wl = controls; wl; wl = wl->wl_next) + for (wl = controls; wl; wl = wl->wl_next){ +#ifdef OSDI + inputdir = dir_name; +#endif if (cp_getvar("controlswait", CP_BOOL, NULL, 0)) { exec_controls(wl_copy(wl)); break; } else cp_evloop(wl->wl_word); + } #else - for (wl = controls; wl; wl = wl->wl_next) + for (wl = controls; wl; wl = wl->wl_next){ +#ifdef OSDI + inputdir = dir_name; +#endif cp_evloop(wl->wl_word); + } #endif wl_free(controls); +#ifdef OSDI + inputdir = NULL; +#endif } /* Now reset everything. Pop the control stack, and fix up the IO @@ -1131,6 +1150,9 @@ inp_spsource(FILE *fp, bool comfile, char *filename, bool intfile) cp_curerr = lasterr; tfree(tt); + tfree(dir_name); + + return 0; } diff --git a/src/frontend/inpcom.c b/src/frontend/inpcom.c index 8929897d9..5ad40e6aa 100644 --- a/src/frontend/inpcom.c +++ b/src/frontend/inpcom.c @@ -11,6 +11,7 @@ Author: 1985 Wayne A. Christopher /* Note: Must include shlwapi.h before ngspice header defining BOOL due * to conflict */ +#include #ifdef _WIN32 #include /* for definition of PathIsRelativeA() */ #pragma comment(lib, "Shlwapi.lib") @@ -1563,6 +1564,8 @@ struct inp_read_t inp_read( FILE *fp, int call_depth, const char *dir_name, !ciprefix("wrdata", buffer) && !ciprefix(".lib", buffer) && !ciprefix(".inc", buffer) && !ciprefix("codemodel", buffer) && + !ciprefix("osdi", buffer) && + !ciprefix("pre_osdi", buffer) && !ciprefix("echo", buffer) && !ciprefix("shell", buffer) && !ciprefix("source", buffer) && !ciprefix("cd ", buffer) && !ciprefix("load", buffer) && !ciprefix("setcs", buffer)) { @@ -2352,6 +2355,8 @@ static char *get_subckt_model_name(char *line) name = skip_non_ws(line); // eat .subckt|.model name = skip_ws(name); + + end_ptr = skip_non_ws(name); return copy_substring(name, end_ptr); @@ -2401,14 +2406,29 @@ static char *get_model_type(char *line) } -static char *get_adevice_model_name(char *line) +static char *get_adevice_model_name(char *line, struct nscope *scope) { - char *ptr_end, *ptr_beg; + char *beg_ptr, *end_ptr, *name; + int i = 0; - ptr_end = skip_back_ws(strchr(line, '\0'), line); - ptr_beg = skip_back_non_ws(ptr_end, line); + beg_ptr = skip_non_ws(line); /* eat device name */ + beg_ptr = skip_ws(beg_ptr); - return copy_substring(ptr_beg, ptr_end); + for (i = 0; i < 30; i++) { /* skip the terminals */ + end_ptr = skip_non_ws(beg_ptr); + name = copy_substring(beg_ptr, end_ptr); + if (inp_find_model(scope, name)){ + return name; + }else if (beg_ptr == end_ptr){ + break; + } + end_ptr = skip_ws(end_ptr); + beg_ptr = end_ptr; + + } + + + return NULL; } @@ -2627,7 +2647,7 @@ static void get_subckts_for_subckt(struct card *start_card, char *subckt_name, nlist_adjoin(used_subckts, inst_subckt_name); } else if (*line == 'a') { - char *model_name = get_adevice_model_name(line); + char *model_name = get_adevice_model_name( line, card->level); nlist_adjoin(used_models, model_name); } else if (has_models) { @@ -2713,7 +2733,7 @@ void comment_out_unused_subckt_models(struct card *start_card) nlist_adjoin(used_subckts, subckt_name); } else if (*line == 'a') { - char *model_name = get_adevice_model_name(line); + char *model_name = get_adevice_model_name(line, card->level); nlist_adjoin(used_models, model_name); } else if (has_models) { @@ -10245,9 +10265,12 @@ void inp_rem_unused_models(struct nscope *root, struct card *deck) /* num_terminals may be 0 for a elements */ if ((num_terminals != 0) || (*curr_line == 'a')) { char *elem_model_name; - if (*curr_line == 'a') - elem_model_name = get_adevice_model_name(curr_line); - else + if (*curr_line == 'a'){ + elem_model_name = get_adevice_model_name( curr_line, card->level); + if (!elem_model_name){ + continue; + } + }else elem_model_name = get_model_name(curr_line, num_terminals); /* ignore certain cases, for example @@ -10288,7 +10311,7 @@ void inp_rem_unused_models(struct nscope *root, struct card *deck) * only correct UTF-8. It also spots UTF-8 sequences that could cause * trouble if converted to UTF-16, namely surrogate characters * (U+D800..U+DFFF) and non-Unicode positions (U+FFFE..U+FFFF). - * In addition we check for some ngspice-specific characters like µ etc.*/ + * In addition we check for some ngspice-specific characters like � etc.*/ #ifndef EXT_ASC static unsigned char* utf8_check(unsigned char *s) @@ -10298,12 +10321,12 @@ utf8_check(unsigned char *s) /* 0xxxxxxx */ s++; else if (*s == 0xb5) { - /* translate ansi micro µ to u */ + /* translate ansi micro � to u */ *s = 'u'; s++; } else if (s[0] == 0xc2 && s[1] == 0xb5) { - /* translate utf-8 micro µ to u */ + /* translate utf-8 micro � to u */ s[0] = 'u'; /* remove second byte */ unsigned char *y = s + 1; diff --git a/src/include/ngspice/ifsim.h b/src/include/ngspice/ifsim.h index 4d62605b8..cf019be2e 100644 --- a/src/include/ngspice/ifsim.h +++ b/src/include/ngspice/ifsim.h @@ -299,6 +299,10 @@ struct IFdevice { #endif int flags; /* DEV_ */ + +#ifdef OSDI + const void *registry_entry; +#endif }; diff --git a/src/include/ngspice/osdiitf.h b/src/include/ngspice/osdiitf.h new file mode 100644 index 000000000..5c84ed0ad --- /dev/null +++ b/src/include/ngspice/osdiitf.h @@ -0,0 +1,32 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + +#pragma once + +#include "ngspice/devdefs.h" +#include + +typedef struct OsdiRegistryEntry { + const void *descriptor; + uint32_t inst_offset; + uint32_t dt; + uint32_t temp; +} OsdiRegistryEntry; + +typedef struct OsdiObjectFile { + OsdiRegistryEntry *entrys; + int num_entries; +} OsdiObjectFile; + +extern OsdiObjectFile load_object_file(const char *path); +extern SPICEdev *osdi_create_spicedev(const OsdiRegistryEntry *entry); + +extern char *inputdir; diff --git a/src/osdi/Makefile.am b/src/osdi/Makefile.am new file mode 100644 index 000000000..f23a7b8eb --- /dev/null +++ b/src/osdi/Makefile.am @@ -0,0 +1,25 @@ +## Process this file with automake to produce Makefile.in + +noinst_LTLIBRARIES = libosdi.la + +libosdi_la_SOURCES = \ + osdi.h \ + osdidefs.h \ + osdiext.h \ + osdiinit.c \ + osdiload.c \ + osdiacld.c \ + osdiparam.c \ + osdiregistry.c \ + osdisetup.c \ + osdiitf.h \ + osditrunc.c \ + osdipzld.c \ + osdicallbacks.c + + + + +AM_CPPFLAGS = @AM_CPPFLAGS@ -I$(top_srcdir)/src/include +AM_CFLAGS = $(STATIC) +MAINTAINERCLEANFILES = Makefile.in diff --git a/src/osdi/osdi.h b/src/osdi/osdi.h new file mode 100644 index 000000000..d35d58aef --- /dev/null +++ b/src/osdi/osdi.h @@ -0,0 +1,209 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * This file is automatically generated by header.lua to match + * the OSDI specfication. DO NOT EDIT MANUALLY + * + */ + +#pragma once +#include +#include +#include + + +#define OSDI_VERSION_MAJOR_CURR 0 +#define OSDI_VERSION_MINOR_CURR 3 + +#define PARA_TY_MASK 3 +#define PARA_TY_REAL 0 +#define PARA_TY_INT 1 +#define PARA_TY_STR 2 +#define PARA_KIND_MASK (3 << 30) +#define PARA_KIND_MODEL (0 << 30) +#define PARA_KIND_INST (1 << 30) +#define PARA_KIND_OPVAR (2 << 30) + +#define ACCESS_FLAG_READ 0 +#define ACCESS_FLAG_SET 1 +#define ACCESS_FLAG_INSTANCE 4 + +#define JACOBIAN_ENTRY_RESIST_CONST 1 +#define JACOBIAN_ENTRY_REACT_CONST 2 +#define JACOBIAN_ENTRY_RESIST 4 +#define JACOBIAN_ENTRY_REACT 8 + +#define CALC_RESIST_RESIDUAL 1 +#define CALC_REACT_RESIDUAL 2 +#define CALC_RESIST_JACOBIAN 4 +#define CALC_REACT_JACOBIAN 8 +#define CALC_NOISE 16 +#define CALC_OP 32 +#define CALC_RESIST_LIM_RHS 64 +#define CALC_REACT_LIM_RHS 128 +#define ENABLE_LIM 256 +#define INIT_LIM 512 +#define ANALYSIS_NOISE 1024 +#define ANALYSIS_DC 2048 +#define ANALYSIS_AC 4096 +#define ANALYSIS_TRAN 8192 +#define ANALYSIS_IC 16384 +#define ANALYSIS_STATIC 32768 +#define ANALYSIS_NODESET 65536 + +#define EVAL_RET_FLAG_LIM 1 +#define EVAL_RET_FLAG_FATAL 2 +#define EVAL_RET_FLAG_FINISH 4 +#define EVAL_RET_FLAG_STOP 8 + + +#define LOG_LVL_MASK 8 +#define LOG_LVL_DEBUG 0 +#define LOG_LVL_DISPLAY 1 +#define LOG_LVL_INFO 2 +#define LOG_LVL_WARN 3 +#define LOG_LVL_ERR 4 +#define LOG_LVL_FATAL 5 +#define LOG_FMT_ERR 16 + +#define INIT_ERR_OUT_OF_BOUNDS 1 + + + +typedef struct OsdiLimFunction { + char *name; + uint32_t num_args; + void *func_ptr; +}OsdiLimFunction; + +typedef struct OsdiSimParas { + char **names; + double *vals; + char **names_str; + char **vals_str; +}OsdiSimParas; + +typedef struct OsdiSimInfo { + OsdiSimParas paras; + double abstime; + double *prev_solve; + double *prev_state; + double *next_state; + uint32_t flags; +}OsdiSimInfo; + +typedef union OsdiInitErrorPayload { + uint32_t parameter_id; +}OsdiInitErrorPayload; + +typedef struct OsdiInitError { + uint32_t code; + OsdiInitErrorPayload payload; +}OsdiInitError; + +typedef struct OsdiInitInfo { + uint32_t flags; + uint32_t num_errors; + OsdiInitError *errors; +}OsdiInitInfo; + +typedef struct OsdiNodePair { + uint32_t node_1; + uint32_t node_2; +}OsdiNodePair; + +typedef struct OsdiJacobianEntry { + OsdiNodePair nodes; + uint32_t react_ptr_off; + uint32_t flags; +}OsdiJacobianEntry; + +typedef struct OsdiNode { + char *name; + char *units; + char *residual_units; + uint32_t resist_residual_off; + uint32_t react_residual_off; + uint32_t resist_limit_rhs_off; + uint32_t react_limit_rhs_off; + bool is_flow; +}OsdiNode; + +typedef struct OsdiParamOpvar { + char **name; + uint32_t num_alias; + char *description; + char *units; + uint32_t flags; + uint32_t len; +}OsdiParamOpvar; + +typedef struct OsdiNoiseSource { + char *name; + OsdiNodePair nodes; +}OsdiNoiseSource; + +typedef struct OsdiDescriptor { + char *name; + + uint32_t num_nodes; + uint32_t num_terminals; + OsdiNode *nodes; + + uint32_t num_jacobian_entries; + OsdiJacobianEntry *jacobian_entries; + + uint32_t num_collapsible; + OsdiNodePair *collapsible; + uint32_t collapsed_offset; + + OsdiNoiseSource *noise_sources; + uint32_t num_noise_src; + + uint32_t num_params; + uint32_t num_instance_params; + uint32_t num_opvars; + OsdiParamOpvar *param_opvar; + + uint32_t node_mapping_offset; + uint32_t jacobian_ptr_resist_offset; + + uint32_t num_states; + uint32_t state_idx_off; + + uint32_t bound_step_offset; + + uint32_t instance_size; + uint32_t model_size; + + void *(*access)(void *inst, void *model, uint32_t id, uint32_t flags); + + void (*setup_model)(void *handle, void *model, OsdiSimParas *sim_params, + OsdiInitInfo *res); + void (*setup_instance)(void *handle, void *inst, void *model, + double temperature, uint32_t num_terminals, + OsdiSimParas *sim_params, OsdiInitInfo *res); + + uint32_t (*eval)(void *handle, void *inst, void *model, OsdiSimInfo *info); + void (*load_noise)(void *inst, void *model, double freq, double *noise_dens, + double *ln_noise_dens); + void (*load_residual_resist)(void *inst, void* model, double *dst); + void (*load_residual_react)(void *inst, void* model, double *dst); + void (*load_limit_rhs_resist)(void *inst, void* model, double *dst); + void (*load_limit_rhs_react)(void *inst, void* model, double *dst); + void (*load_spice_rhs_dc)(void *inst, void* model, double *dst, + double* prev_solve); + void (*load_spice_rhs_tran)(void *inst, void* model, double *dst, + double* prev_solve, double alpha); + void (*load_jacobian_resist)(void *inst, void* model); + void (*load_jacobian_react)(void *inst, void* model, double alpha); + void (*load_jacobian_tran)(void *inst, void* model, double alpha); +}OsdiDescriptor; + + + diff --git a/src/osdi/osdiacld.c b/src/osdi/osdiacld.c new file mode 100644 index 000000000..5631dfb25 --- /dev/null +++ b/src/osdi/osdiacld.c @@ -0,0 +1,44 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + +#include "ngspice/iferrmsg.h" +#include "ngspice/memory.h" +#include "ngspice/ngspice.h" +#include "ngspice/typedefs.h" + +#include "osdi.h" +#include "osdidefs.h" + +#include +#include +#include + +int OSDIacLoad(GENmodel *inModel, CKTcircuit *ckt) { + + GENmodel *gen_model; + GENinstance *gen_inst; + + OsdiRegistryEntry *entry = osdi_reg_entry_model(inModel); + const OsdiDescriptor *descr = entry->descriptor; + for (gen_model = inModel; gen_model; gen_model = gen_model->GENnextModel) { + void *model = osdi_model_data(gen_model); + + for (gen_inst = gen_model->GENinstances; gen_inst; + gen_inst = gen_inst->GENnextInstance) { + void *inst = osdi_instance_data(entry, gen_inst); + // nothing to calculate just load the matrix entries calculated during + // operating point iterations + descr->load_jacobian_resist(inst, model); + descr->load_jacobian_react(inst, model, ckt->CKTomega); + } + } + return (OK); +} diff --git a/src/osdi/osdicallbacks.c b/src/osdi/osdicallbacks.c new file mode 100644 index 000000000..de7b1dc97 --- /dev/null +++ b/src/osdi/osdicallbacks.c @@ -0,0 +1,88 @@ +#include "ngspice/devdefs.h" +#include "osdidefs.h" + +void osdi_log(void *handle_, char *msg, uint32_t lvl) { + OsdiNgspiceHandle *handle = handle_; + FILE *dst = stdout; + switch (lvl & LOG_LVL_MASK) { + case LOG_LVL_DEBUG: + printf("OSDI(debug) %s: ", handle->name); + break; + case LOG_LVL_DISPLAY: + printf("OSDI %s: ", handle->name); + break; + case LOG_LVL_INFO: + printf("OSDI(info) %s: ", handle->name); + break; + case LOG_LVL_WARN: + fprintf(stderr, "OSDI(warn) %s: ", handle->name); + dst = stderr; + break; + case LOG_LVL_ERR: + fprintf(stderr, "OSDI(err) %s: ", handle->name); + dst = stderr; + break; + case LOG_LVL_FATAL: + fprintf(stderr, "OSDI(fatal) %s: ", handle->name); + dst = stderr; + break; + default: + fprintf(stderr, "OSDI(unkown) %s", handle->name); + break; + } + + if (lvl & LOG_FMT_ERR) { + fprintf(dst, "failed to format\"%s\"\n", msg); + } else { + fprintf(dst, "%s", msg); + } +} + +double osdi_pnjlim(bool init, bool *check, double vnew, double vold, double vt, + double vcrit) { + if (init) { + *check = true; + return vcrit; + } + int icheck = 0; + double res = DEVpnjlim(vnew, vold, vt, vcrit, &icheck); + *check = icheck != 0; + return res; +} + +double osdi_limvds(bool init, bool *check, double vnew, double vold) { + if (init) { + *check = true; + return 0.1; + } + double res = DEVlimvds(vnew, vold); + if (res != vnew) { + *check = true; + } + return res; +} + +double osdi_fetlim(bool init, bool *check, double vnew, double vold, + double vto) { + if (init) { + *check = true; + return vto + 0.1; + } + double res = DEVfetlim(vnew, vold, vto); + if (res != vnew) { + *check = true; + } + return res; +} + +double osdi_limitlog(bool init, bool *check, double vnew, double vold, + double LIM_TOL) { + if (init) { + *check = true; + return 0.0; + } + int icheck = 0; + double res = DEVlimitlog(vnew, vold, LIM_TOL, &icheck); + *check = icheck != 0; + return res; +} diff --git a/src/osdi/osdidefs.h b/src/osdi/osdidefs.h new file mode 100644 index 000000000..9d61df8d0 --- /dev/null +++ b/src/osdi/osdidefs.h @@ -0,0 +1,74 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + +#pragma once + +#include "ngspice/cktdefs.h" +#include "ngspice/complex.h" +#include "ngspice/fteext.h" +#include "ngspice/gendefs.h" +#include "ngspice/ifsim.h" +#include "ngspice/ngspice.h" +#include "ngspice/noisedef.h" +#include "ngspice/typedefs.h" + +#include "osdi.h" +#include "osdiext.h" + +#include "stddef.h" +#include +#include + +typedef struct OsdiModelData { + GENmodel gen; + max_align_t data; +} OsdiModelData; + +typedef struct OsdiExtraInstData { + double dt; + double temp; + bool temp_given; + bool dt_given; + bool finish; + +} __attribute__((aligned(sizeof(max_align_t)))) OsdiExtraInstData; + +size_t osdi_instance_data_off(const OsdiRegistryEntry *entry); +void *osdi_instance_data(const OsdiRegistryEntry *entry, GENinstance *inst); +OsdiExtraInstData *osdi_extra_instance_data(const OsdiRegistryEntry *entry, + GENinstance *inst); +size_t osdi_model_data_off(void); +void *osdi_model_data(GENmodel *model); +void *osdi_model_data_from_inst(GENinstance *inst); +OsdiRegistryEntry *osdi_reg_entry_model(const GENmodel *model); +OsdiRegistryEntry *osdi_reg_entry_inst(const GENinstance *inst); + +typedef struct OsdiNgspiceHandle { + uint32_t kind; + char *name; +} OsdiNgspiceHandle; + +/* values returned by $simparam*/ +OsdiSimParas get_simparams(const CKTcircuit *ckt); + +typedef void (*osdi_log_ptr)(void *handle, char *msg, uint32_t lvl); +void osdi_log(void *handle_, char *msg, uint32_t lvl); + +typedef void (*osdi_log_ptr)(void *handle, char *msg, uint32_t lvl); + +double osdi_pnjlim(bool init, bool *icheck, double vnew, double vold, double vt, + double vcrit); + +double osdi_limvds(bool init, bool *icheck, double vnew, double vold); +double osdi_limitlog(bool init, bool *icheck, double vnew, double vold, + double LIM_TOL); +double osdi_fetlim(bool init, bool *icheck, double vnew, double vold, + double vto); diff --git a/src/osdi/osdiext.h b/src/osdi/osdiext.h new file mode 100644 index 000000000..d61c8c488 --- /dev/null +++ b/src/osdi/osdiext.h @@ -0,0 +1,36 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + +#pragma once + +#include "ngspice/gendefs.h" +#include "ngspice/smpdefs.h" +#include + +#include "ngspice/osdiitf.h" + +extern int OSDImParam(int, IFvalue *, GENmodel *); +extern int OSDIparam(int, IFvalue *, GENinstance *, IFvalue *); +extern int OSDIsetup(SMPmatrix *, GENmodel *, CKTcircuit *, int *); +extern int OSDIunsetup(GENmodel *, CKTcircuit *); +extern int OSDIask(CKTcircuit *, GENinstance *, int, IFvalue *, IFvalue *); +extern int OSDIload(GENmodel *, CKTcircuit *); +extern int OSDItemp(GENmodel *, CKTcircuit *); +extern int OSDIacLoad(GENmodel *, CKTcircuit *); +extern int OSDItrunc(GENmodel *, CKTcircuit *, double *); +extern int OSDIpzLoad(GENmodel*, CKTcircuit*, SPcomplex*); + +/* extern int OSDIconvTest(GENmodel*,CKTcircuit*); */ +/* extern int OSDImDelete(GENmodel*); */ +/* extern int OSDIgetic(GENmodel*,CKTcircuit*); */ +/* extern int OSDImAsk(CKTcircuit*,GENmodel*,int,IFvalue*); */ +/* extern int OSDInoise(int,int,GENmodel*,CKTcircuit*,Ndata*,double*); */ +/* extern int OSDIsoaCheck(CKTcircuit *, GENmodel *); */ diff --git a/src/osdi/osdiinit.c b/src/osdi/osdiinit.c new file mode 100644 index 000000000..852737abb --- /dev/null +++ b/src/osdi/osdiinit.c @@ -0,0 +1,187 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + + +#include "ngspice/stringutil.h" + +#include "ngspice/config.h" +#include "ngspice/devdefs.h" +#include "ngspice/iferrmsg.h" +#include "ngspice/memory.h" +#include "ngspice/ngspice.h" +#include "ngspice/typedefs.h" + +#include "osdi.h" +#include "osdidefs.h" + +#include +#include +#include +#include + +/* + * This function converts the information in (a list of) OsdiParamOpvar in + * descr->param_opvar to the internal ngspice representation (IFparm). + */ +static int write_param_info(IFparm **dst, const OsdiDescriptor *descr, + uint32_t start, uint32_t end) { + for (uint32_t i = start; i < end; i++) { + OsdiParamOpvar *para = &descr->param_opvar[i]; + uint32_t num_names = para->num_alias + 1; + + int dataType = IF_ASK; + if ((para->flags & (uint32_t)PARA_KIND_OPVAR) == 0) { + dataType |= IF_SET; + } + + switch (para->flags & PARA_TY_MASK) { + case PARA_TY_REAL: + dataType |= IF_REAL; + break; + case PARA_TY_INT: + dataType |= IF_INTEGER; + break; + case PARA_TY_STR: + dataType |= IF_STRING; + break; + default: + errRtn = "get_osdi_info"; + errMsg = tprintf("Unkown OSDI type %d for parameter %s!", + para->flags & PARA_TY_MASK, para->name[0]); + return -1; + } + + if (para->len != 0) { + dataType |= IF_VECTOR; + } + + for (uint32_t j = 0; j < num_names; j++) { + if (j != 0) { + dataType = IF_UNINTERESTING; + } + char *para_name = copy(para->name[j]); + strtolower(para_name); + (*dst)[j] = (IFparm){.keyword = para_name, + .id = (int)i, + .description = para->description, + .dataType = dataType}; + } + *dst += num_names; + } + + return 0; +} +/** + * This function creates a SPICEdev instance for a specific OsdiDescriptor by + * populating the SPICEdev struct with descriptor specific metadata and pointers + * to the descriptor independent functions. + * */ +extern SPICEdev *osdi_create_spicedev(const OsdiRegistryEntry *entry) { + const OsdiDescriptor *descr = entry->descriptor; + + // allocate and fill terminal names array + char **termNames = TMALLOC(char *, descr->num_terminals); + for (uint32_t i = 0; i < descr->num_terminals; i++) { + termNames[i] = descr->nodes[i].name; + } + + // allocate and fill instance params (and opvars) + int *num_instance_para_names = TMALLOC(int, 1); + for (uint32_t i = 0; i < descr->num_instance_params; i++) { + *num_instance_para_names += (int)(1 + descr->param_opvar[i].num_alias); + } + for (uint32_t i = descr->num_params; + i < descr->num_opvars + descr->num_params; i++) { + *num_instance_para_names += (int)(1 + descr->param_opvar[i].num_alias); + } + if (entry->dt != UINT32_MAX) { + *num_instance_para_names += 1; + } + + if (entry->temp != UINT32_MAX) { + *num_instance_para_names += 1; + } + + IFparm *instance_para_names = TMALLOC(IFparm, *num_instance_para_names); + IFparm *dst = instance_para_names; + + if (entry->dt != UINT32_MAX) { + dst[0] = (IFparm){"dt", (int)entry->dt, IF_REAL | IF_SET, + "Instance delta temperature"}; + dst += 1; + } + + if (entry->temp != UINT32_MAX) { + dst[0] = (IFparm){"temp", (int)entry->temp, IF_REAL | IF_SET, + "Instance temperature"}; + dst += 1; + } + write_param_info(&dst, descr, 0, descr->num_instance_params); + write_param_info(&dst, descr, descr->num_params, + descr->num_params + descr->num_opvars); + + // allocate and fill model params + int *num_model_para_names = TMALLOC(int, 1); + for (uint32_t i = descr->num_instance_params; i < descr->num_params; i++) { + *num_model_para_names += (int)(1 + descr->param_opvar[i].num_alias); + } + IFparm *model_para_names = TMALLOC(IFparm, *num_model_para_names); + dst = model_para_names; + write_param_info(&dst, descr, descr->num_instance_params, descr->num_params); + + // Allocate SPICE device + SPICEdev *OSDIinfo = TMALLOC(SPICEdev, 1); + + // fill information + OSDIinfo->DEVpublic = (IFdevice){ + .name = descr->name, + .description = "A simulator independent device loaded with OSDI", + // TODO why extra indirection? Optional ports? + .terms = (int *)&descr->num_terminals, + .numNames = (int *)&descr->num_terminals, + .termNames = termNames, + .numInstanceParms = num_instance_para_names, + .instanceParms = instance_para_names, + .numModelParms = num_model_para_names, + .modelParms = model_para_names, + .flags = DEV_DEFAULT, + .registry_entry = (void *)entry, + }; + + size_t inst_off = entry->inst_offset; + + int *inst_size = TMALLOC(int, 1); + *inst_size = + (int)(inst_off + descr->instance_size + sizeof(OsdiExtraInstData)); + OSDIinfo->DEVinstSize = inst_size; + + size_t model_off = osdi_model_data_off(); + int *model_size = TMALLOC(int, 1); + *model_size = (int)(model_off + descr->model_size); + OSDIinfo->DEVmodSize = model_size; + + // fill generic functions + OSDIinfo->DEVparam = OSDIparam; + OSDIinfo->DEVmodParam = OSDImParam; + OSDIinfo->DEVask = OSDIask; + OSDIinfo->DEVsetup = OSDIsetup; + OSDIinfo->DEVpzSetup = OSDIsetup; + OSDIinfo->DEVtemperature = OSDItemp; + OSDIinfo->DEVunsetup = OSDIunsetup; + OSDIinfo->DEVload = OSDIload; + OSDIinfo->DEVacLoad = OSDIacLoad; + OSDIinfo->DEVpzLoad = OSDIpzLoad; + OSDIinfo->DEVtrunc = OSDItrunc; + + + + return OSDIinfo; +} diff --git a/src/osdi/osdiload.c b/src/osdi/osdiload.c new file mode 100644 index 000000000..09d04f5e3 --- /dev/null +++ b/src/osdi/osdiload.c @@ -0,0 +1,211 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + +#include "ngspice/iferrmsg.h" +#include "ngspice/memory.h" +#include "ngspice/ngspice.h" +#include "ngspice/typedefs.h" + +#include "osdi.h" +#include "osdidefs.h" + +#include +#include +#include +#include +#include + +#define NUM_SIM_PARAMS 5 +char *sim_params[NUM_SIM_PARAMS + 1] = { + "gdev", "gmin", "tnom", "simulatorVersion", "sourceScaleFactor", NULL}; +char *sim_params_str[1] = {NULL}; + +double sim_param_vals[NUM_SIM_PARAMS] = {}; + +/* values returned by $simparam*/ +OsdiSimParas get_simparams(const CKTcircuit *ckt) { + + double simulatorVersion = strtod(PACKAGE_VERSION, NULL); + double gdev = ckt->CKTgmin; + double sourceScaleFactor = ckt->CKTsrcFact; + double gmin = ((ckt->CKTgmin) > (ckt->CKTdiagGmin)) ? (ckt->CKTgmin) + : (ckt->CKTdiagGmin); + double sim_param_vals_[NUM_SIM_PARAMS] = { + gdev, gmin, ckt->CKTnomTemp, simulatorVersion, sourceScaleFactor}; + memcpy(&sim_param_vals, &sim_param_vals_, sizeof(double) * NUM_SIM_PARAMS); + OsdiSimParas sim_params_ = {.names = sim_params, + .vals = (double *)&sim_param_vals, + .names_str = sim_params_str, + .vals_str = NULL}; + return sim_params_; +} + +extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) { + OsdiNgspiceHandle handle; + GENmodel *gen_model; + GENinstance *gen_inst; + double dump; + + bool is_init_smsig = ckt->CKTmode & MODEINITSMSIG; + bool is_sweep = ckt->CKTmode & MODEDCTRANCURVE; + bool is_dc = ckt->CKTmode & (MODEDCOP | MODEDCTRANCURVE); + bool is_ac = ckt->CKTmode & (MODEAC | MODEINITSMSIG); + bool is_tran = ckt->CKTmode & (MODETRAN); + bool is_tran_op = ckt->CKTmode & (MODETRANOP); + bool is_init_tran = ckt->CKTmode & MODEINITTRAN; + bool is_init_junc = ckt->CKTmode & MODEINITJCT; + + OsdiSimInfo sim_info = { + .paras = get_simparams(ckt), + .abstime = is_tran ? ckt->CKTtime : 0.0, + .prev_solve = ckt->CKTrhsOld, + .prev_state = ckt->CKTstates[0], + .next_state = ckt->CKTstates[0], + .flags = CALC_RESIST_JACOBIAN, + }; + + if (is_init_smsig || is_sweep) { + sim_info.flags |= CALC_OP; + } + + if (is_dc) { + sim_info.flags |= ANALYSIS_DC | ANALYSIS_STATIC; + } + + if (!is_init_smsig) { + sim_info.flags |= CALC_RESIST_RESIDUAL | ENABLE_LIM | CALC_RESIST_LIM_RHS; + } + + if (is_tran) { + sim_info.flags |= CALC_REACT_JACOBIAN | CALC_REACT_RESIDUAL | + CALC_REACT_LIM_RHS | ANALYSIS_TRAN; + } + + if (is_tran_op) { + sim_info.flags |= ANALYSIS_TRAN; + } + + if (is_ac) { + sim_info.flags |= CALC_REACT_JACOBIAN | ANALYSIS_AC; + } + + if (is_init_tran) { + sim_info.flags |= ANALYSIS_IC | ANALYSIS_STATIC; + } + + if (is_init_junc) { + sim_info.flags |= INIT_LIM; + } + + if (ckt->CKTmode & MODEACNOISE) { + sim_info.flags |= CALC_NOISE | ANALYSIS_NOISE; + } + + int ret = OK; + + OsdiRegistryEntry *entry = osdi_reg_entry_model(inModel); + const OsdiDescriptor *descr = entry->descriptor; + + for (gen_model = inModel; gen_model; gen_model = gen_model->GENnextModel) { + void *model = osdi_model_data(gen_model); + + for (gen_inst = gen_model->GENinstances; gen_inst; + gen_inst = gen_inst->GENnextInstance) { + void *inst = osdi_instance_data(entry, gen_inst); + + /* hpyothetically this could run in parallel we do not write any shared + data here*/ + handle = (OsdiNgspiceHandle){.kind = 3, .name = gen_inst->GENname}; + /* TODO initial conditions? */ + uint32_t ret_flags = descr->eval(&handle, inst, model, &sim_info); + + /* call to $fatal in Verilog-A abort!*/ + if (ret_flags & EVAL_RET_FLAG_FATAL) { + return E_PANIC; + } + + /* init small signal analysis does not require loading values into + * matrix/rhs*/ + if (is_init_smsig) { + continue; + } + + /* handle calls to $finish, $limit, $stop + * TODO actually do something with extra_inst_data->finish and + * extra_inst_data->limt + * */ + OsdiExtraInstData *extra_inst_data = + osdi_extra_instance_data(entry, gen_inst); + if (ret_flags & EVAL_RET_FLAG_FINISH) { + extra_inst_data->finish = true; + } + if (ret_flags & EVAL_RET_FLAG_LIM) { + ckt->CKTnoncon++; + ckt->CKTtroubleElt = gen_inst; + + } + if (ret_flags & EVAL_RET_FLAG_STOP) { + ret = (E_PAUSE); + } + + if (is_tran) { + /* load dc matrix and capacitances (charge derivative multiplied with + * CKTag[0]) */ + descr->load_jacobian_tran(inst, model, ckt->CKTag[0]); + + /* load static rhs and dynamic linearized rhs (SUM Vb * dIa/dVb)*/ + descr->load_spice_rhs_tran(inst, model, ckt->CKTrhs, ckt->CKTrhsOld, + ckt->CKTag[0]); + + uint32_t *node_mapping = + (uint32_t *)(((char *)inst) + descr->node_mapping_offset); + + /* use numeric integration to obtain the remainer of the RHS*/ + int state = gen_inst->GENstate + (int) descr->num_states; + for (uint32_t i = 0; i < descr->num_nodes; i++) { + if (descr->nodes[i].react_residual_off != UINT32_MAX) { + + double residual_react = *(( + double *)(((char *)inst) + descr->nodes[i].react_residual_off)); + + /* store charges in state vector*/ + ckt->CKTstate0[state] = residual_react; + if (is_init_tran) { + ckt->CKTstate1[state] = residual_react; + } + + /* we only care about the numeric integration itself not ceq/geq + because those are already calculated by load_jacobian_tran and + load_spice_rhs_tran*/ + NIintegrate(ckt, &dump, &dump, 0, state); + + /* add the numeric derivative to the rhs */ + ckt->CKTrhs[node_mapping[i]] -= ckt->CKTstate0[state + 1]; + + if (is_init_tran) { + ckt->CKTstate1[state + 1] = ckt->CKTstate0[state + 1]; + } + + state += 2; + } + } + } else { + /* copy internal derivatives into global matrix */ + descr->load_jacobian_resist(inst, model); + + /* calculate spice RHS from internal currents and store into global RHS + */ + descr->load_spice_rhs_dc(inst, model, ckt->CKTrhs, ckt->CKTrhsOld); + } + } + } + return ret; +} diff --git a/src/osdi/osdiparam.c b/src/osdi/osdiparam.c new file mode 100644 index 000000000..49af371c8 --- /dev/null +++ b/src/osdi/osdiparam.c @@ -0,0 +1,165 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + +#include "ngspice/iferrmsg.h" +#include "ngspice/ngspice.h" +#include "ngspice/typedefs.h" + +#include "osdidefs.h" + +#include +#include + +static int osdi_param_access(OsdiParamOpvar *param_info, bool write_value, + IFvalue *value, void *ptr) { + size_t len; + void *val_ptr; + switch (param_info->flags & PARA_TY_MASK) { + case PARA_TY_REAL: + len = sizeof(double); + if (param_info->len) { + len *= param_info->len; + val_ptr = &value->v.vec.rVec; + } else { + val_ptr = &value->rValue; + } + break; + case PARA_TY_INT: + len = sizeof(int); + if (param_info->len) { + len *= param_info->len; + val_ptr = &value->v.vec.iVec; + } else { + val_ptr = &value->iValue; + } + break; + case PARA_TY_STR: + len = sizeof(char *); + if (param_info->len) { + len *= param_info->len; + val_ptr = &value->v.vec.cVec; + } else { + val_ptr = &value->cValue; + } + break; + default: + return (E_PARMVAL); + } + if (write_value) { + memcpy(val_ptr, ptr, len); + } else { + memcpy(ptr, val_ptr, len); + } + + return OK; +} + +static int osdi_write_param(void *dst, IFvalue *value, int param, + const OsdiDescriptor *descr) { + if (dst == NULL) { + return (E_PANIC); + } + + OsdiParamOpvar *param_info = &descr->param_opvar[param]; + + if (param_info->len) { + if ((uint32_t)value->v.numValue != param_info->len) { + return (E_PARMVAL); + } + } + + return osdi_param_access(param_info, false, value, dst); +} + +extern int OSDIparam(int param, IFvalue *value, GENinstance *instPtr, + IFvalue *select) { + + NG_IGNORE(select); + OsdiRegistryEntry *entry = osdi_reg_entry_inst(instPtr); + const OsdiDescriptor *descr = entry->descriptor; + + if (param > (int)descr->num_instance_params) { + + // special handleing for temperature parameters + OsdiExtraInstData *inst = osdi_extra_instance_data(entry, instPtr); + if (param == (int)entry->dt) { + inst->dt = value->rValue; + inst->dt_given = true; + return (OK); + } + if (param == (int)entry->temp) { + inst->temp = value->rValue; + inst->temp_given = true; + return (OK); + } + + return (E_BADPARM); + } + + void *inst = osdi_instance_data(entry, instPtr); + void *dst = descr->access(inst, NULL, (uint32_t)param, + ACCESS_FLAG_SET | ACCESS_FLAG_INSTANCE); + + return osdi_write_param(dst, value, param, descr); +} + +extern int OSDImParam(int param, IFvalue *value, GENmodel *modelPtr) { + OsdiRegistryEntry *entry = osdi_reg_entry_model(modelPtr); + const OsdiDescriptor *descr = entry->descriptor; + + if (param > (int)descr->num_params || + param < (int)descr->num_instance_params) { + return (E_BADPARM); + } + + void *model = osdi_model_data(modelPtr); + void *dst = descr->access(NULL, model, (uint32_t)param, ACCESS_FLAG_SET); + + return osdi_write_param(dst, value, param, descr); +} + +static int osdi_read_param(void *src, IFvalue *value, int id, + const OsdiDescriptor *descr) { + if (src == NULL) { + return (E_PANIC); + } + + OsdiParamOpvar *param_info = &descr->param_opvar[id]; + + if (param_info->len) { + value->v.numValue = (int)param_info->len; + } + + return osdi_param_access(param_info, true, value, src); +} + +extern int OSDIask(CKTcircuit *ckt, GENinstance *instPtr, int id, + IFvalue *value, IFvalue *select) { + NG_IGNORE(select); + NG_IGNORE(ckt); + + OsdiRegistryEntry *entry = osdi_reg_entry_inst(instPtr); + void *inst = osdi_instance_data(entry, instPtr); + void *model = osdi_model_data_from_inst(instPtr); + + const OsdiDescriptor *descr = entry->descriptor; + + if (id > (int)(descr->num_params + descr->num_instance_params)) { + return (E_BADPARM); + } + uint32_t flags = ACCESS_FLAG_READ; + if (id < (int)descr->num_instance_params) { + flags |= ACCESS_FLAG_INSTANCE; + } + + void *src = descr->access(inst, model, (uint32_t)id, flags); + return osdi_read_param(src, value, id, descr); +} diff --git a/src/osdi/osdipzld.c b/src/osdi/osdipzld.c new file mode 100644 index 000000000..c9285ba46 --- /dev/null +++ b/src/osdi/osdipzld.c @@ -0,0 +1,49 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + +#include "ngspice/iferrmsg.h" +#include "ngspice/memory.h" +#include "ngspice/ngspice.h" +#include "ngspice/typedefs.h" + +#include "osdi.h" +#include "osdidefs.h" + +#include +#include +#include + +int OSDIpzLoad(GENmodel *inModel, CKTcircuit *ckt, SPcomplex *s) { + NG_IGNORE(ckt); + + GENmodel *gen_model; + GENinstance *gen_inst; + + OsdiRegistryEntry *entry = osdi_reg_entry_model(inModel); + const OsdiDescriptor *descr = entry->descriptor; + for (gen_model = inModel; gen_model; gen_model = gen_model->GENnextModel) { + void *model = osdi_model_data(gen_model); + + for (gen_inst = gen_model->GENinstances; gen_inst; + gen_inst = gen_inst->GENnextInstance) { + void *inst = osdi_instance_data(entry, gen_inst); + // nothing to calculate just load the matrix entries calculated during + // operating point iterations + // the load_jacobian_tran function migh seem weird here but all this does + // is adding J_resist + J_react * a to every matrix entry (real part). + // J_resist are the conductances (normal matrix entries) and J_react the + // capcitances + descr->load_jacobian_tran(inst, model, s->real); + descr->load_jacobian_react(inst, model, s->imag); + } + } + return (OK); +} diff --git a/src/osdi/osdiregistry.c b/src/osdi/osdiregistry.c new file mode 100644 index 000000000..2370f0837 --- /dev/null +++ b/src/osdi/osdiregistry.c @@ -0,0 +1,427 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + +#include "ngspice/hash.h" +#include "ngspice/memory.h" +#include "ngspice/stringutil.h" +#include "osdidefs.h" + +#include + +#include "osdi.h" +#include +#include +#include +#include + +#if (!defined HAS_WINGUI) && (!defined __MINGW32__) && (!defined _MSC_VER) + +#include /* to load libraries*/ +#define OPENLIB(path) dlopen(path, RTLD_NOW | RTLD_LOCAL) +#define GET_SYM(lib, sym) dlsym(lib, sym) +#define FREE_DLERR_MSG(msg) + +#else /* ifdef HAS_WINGUI */ + +#undef BOOLEAN +#include +#define OPENLIB(path) LoadLibrary(path) +#define GET_SYM(lib, sym) ((void *)GetProcAddress(lib, sym)) +char *dlerror(void); +#define FREE_DLERR_MSG(msg) free_dlerr_msg(msg) +static void free_dlerr_msg(char *msg); + +#endif /* ifndef HAS_WINGUI */ + +char *inputdir = NULL; + +/* Returns true if path is an absolute path and false if it is a + * relative path. No check is done for the existance of the path. */ +inline static bool is_absolute_pathname(const char *path) { +#ifdef _WIN32 + return !PathIsRelativeA(path); +#else + return path[0] == DIR_TERM; +#endif +} /* end of funciton is_absolute_pathname */ + +/*-------------------------------------------------------------------------* + Look up the variable sourcepath and try everything in the list in order + if the file isn't in . and it isn't an abs path name. + *-------------------------------------------------------------------------*/ + +static char *resolve_path(const char *name) { + struct stat st; + +#if defined(_WIN32) + + /* If variable 'mingwpath' is set: convert mingw /d/... to d:/... */ + if (cp_getvar("mingwpath", CP_BOOL, NULL, 0) && name[0] == DIR_TERM_LINUX && + isalpha_c(name[1]) && name[2] == DIR_TERM_LINUX) { + DS_CREATE(ds, 100); + if (ds_cat_str(&ds, name) != 0) { + fprintf(stderr, "Unable to copy string while resolving path"); + controlled_exit(EXIT_FAILURE); + } + char *const buf = ds_get_buf(&ds); + buf[0] = buf[1]; + buf[1] = ':'; + char *const resolved_path = resolve_path(buf); + ds_free(&ds); + return resolved_path; + } + +#endif + + /* just try it */ + if (stat(name, &st) == 0) + return copy(name); + +#if !defined(EXT_ASC) && (defined(__MINGW32__) || defined(_MSC_VER)) + wchar_t wname[BSIZE_SP]; + if (MultiByteToWideChar(CP_UTF8, 0, name, -1, wname, + 2 * (int)strlen(name) + 1) == 0) { + fprintf(stderr, "UTF-8 to UTF-16 conversion failed with 0x%x\n", + GetLastError()); + fprintf(stderr, "%s could not be converted\n", name); + return NULL; + } + if (_waccess(wname, 0) == 0) + return copy(name); +#endif + + return (char *)NULL; +} + +static char *resolve_input_path(const char *name) { + /* if name is an absolute path name, + * or if we haven't anything to prepend anyway + */ + if (is_absolute_pathname(name)) { + return resolve_path(name); + } + + if (name[0] == '~' && name[1] == '/') { + char *const y = cp_tildexpand(name); + if (y) { + char *const r = resolve_path(y); + txfree(y); + return r; + } + } + + /* + * If called from a script inputdir != NULL so try relativ to that dir + * Otherwise try relativ to the current workdir + */ + + if (inputdir) { + DS_CREATE(ds, 100); + int rc_ds = 0; + rc_ds |= ds_cat_str(&ds, inputdir); /* copy the dir name */ + const size_t n = ds_get_length(&ds); /* end of copied dir name */ + + /* Append a directory separator if not present already */ + const char ch_last = n > 0 ? inputdir[n - 1] : '\0'; + if (ch_last != DIR_TERM +#ifdef _WIN32 + && ch_last != DIR_TERM_LINUX +#endif + ) { + rc_ds |= ds_cat_char(&ds, DIR_TERM); + } + rc_ds |= ds_cat_str(&ds, name); /* append the file name */ + + if (rc_ds != 0) { + (void)fprintf(cp_err, "Unable to build \"dir\" path name " + "in inp_pathresolve_at"); + controlled_exit(EXIT_FAILURE); + } + + char *const r = resolve_path(ds_get_buf(&ds)); + ds_free(&ds); + return r; + } else { + DS_CREATE(ds, 100); + if (ds_cat_printf(&ds, ".%c%s", DIR_TERM, name) != 0) { + (void)fprintf(cp_err, + "Unable to build \".\" path name in inp_pathresolve_at"); + controlled_exit(EXIT_FAILURE); + } + char *const r = resolve_path(ds_get_buf(&ds)); + ds_free(&ds); + if (r != (char *)NULL) { + return r; + } + } + + return NULL; +} /* end of function inp_pathresolve_at */ + +/** + * Calculates the offset that the OSDI instance data has from the beginning of + * the instance data allocated by ngspice. This offset is non trivial because + * ngspice must store the terminal pointers before the remaining instance + * data. As a result the offset is not constant and a variable amount of + * padding must be inserted to ensure correct alginment. + */ +static size_t calc_osdi_instance_data_off(const OsdiDescriptor *descr) { + size_t res = sizeof(GENinstance) /* generic data */ + + descr->num_terminals * sizeof(int); + size_t padding = sizeof(max_align_t) - res % sizeof(max_align_t); + if (padding == sizeof(max_align_t)) { + padding = 0; + } + return res + padding; +} + +#define INVALID_OBJECT \ + (OsdiObjectFile) { .num_entries = -1 } + +#define EMPTY_OBJECT \ + (OsdiObjectFile) {} + +#define ERR_AND_RET \ + error = dlerror(); \ + printf("Error opening osdi lib \"%s\": %s\n", path, error); \ + FREE_DLERR_MSG(error); \ + return INVALID_OBJECT; + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) + +#define GET_CONST(name, ty) \ + sym = GET_SYM(handle, STRINGIFY(name)); \ + if (!sym) { \ + ERR_AND_RET \ + } \ + const ty name = *((ty *)sym); + +#define GET_PTR(name, ty) \ + sym = GET_SYM(handle, STRINGIFY(name)); \ + if (!sym) { \ + ERR_AND_RET \ + } \ + const ty *name = (ty *)sym; + +#define INIT_CALLBACK(name, ty) \ + sym = GET_SYM(handle, STRINGIFY(name)); \ + if (sym) { \ + ty *slot = (ty *)sym; \ + *slot = name; \ + } + +#define IS_LIM_FUN(fun_name, num_args_, val) \ + if (strcmp(lim_table[i].name, fun_name) == 0) { \ + if (lim_table[i].num_args == num_args_) { \ + lim_table[i].func_ptr = (void *)val; \ + continue; \ + } else { \ + expected_args = num_args_; \ + } \ + } + +static NGHASHPTR known_object_files = NULL; +#define DUMMYDATA ((void *)42) +/** + * Loads an object file from the hard drive with the platform equivalent of + * dlopen. This function checks that the OSDI version of the object file is + * valid and then writes all data into the `registry`. + * If any errors occur an appropriate message is written to errMsg + * + * @param PATH path A path to the shared object file + * @param uint32_t* len The amount of entries already written into `registry` + * @param uint32_t* capacity The amount of space available in `registry` + * before reallocation is required + * @returns -1 on error, 0 otherwise + */ +extern OsdiObjectFile load_object_file(const char *input) { + void *handle; + char *error; + const void *sym; + /* ensure the hashtable exists */ + if (!known_object_files) { + known_object_files = nghash_init_pointer(8); + } + const char *path = resolve_input_path(input); + if (!path) { + printf("Error opening osdi lib \"%s\": No such file or directory!\n", + input); + return INVALID_OBJECT; + } + + handle = OPENLIB(path); + if (!handle) { + ERR_AND_RET + } + + /* Keep track of loaded shared object files to avoid loading the same model + * multiple times. We use the handle as a key because the same SO will always + * return the SAME pointer as long as dlclose is not called. + * nghash_insert returns NULL if the key (handle) was not already in the table + * and the data (DUMMYDATA) that was previously insered (!= NULL) otherwise*/ + if (nghash_insert(known_object_files, handle, DUMMYDATA)) { + return EMPTY_OBJECT; + } + + GET_CONST(OSDI_VERSION_MAJOR, uint32_t); + GET_CONST(OSDI_VERSION_MINOR, uint32_t); + + if (OSDI_VERSION_MAJOR != OSDI_VERSION_MAJOR_CURR || + OSDI_VERSION_MINOR != OSDI_VERSION_MINOR_CURR) { + printf("NGSPICE only supports OSDI v%d.%d but \"%s\" targets v%d.%d!", + OSDI_VERSION_MAJOR_CURR, OSDI_VERSION_MINOR_CURR, path, + OSDI_VERSION_MAJOR, OSDI_VERSION_MINOR); + return INVALID_OBJECT; + } + + GET_CONST(OSDI_NUM_DESCRIPTORS, uint32_t); + GET_PTR(OSDI_DESCRIPTORS, OsdiDescriptor); + + INIT_CALLBACK(osdi_log, osdi_log_ptr) + + uint32_t lim_table_len = 0; + sym = GET_SYM(handle, "OSDI_LIM_TABLE_LEN"); + if (sym) { + lim_table_len = *((uint32_t *)sym); + } + + sym = GET_SYM(handle, "OSDI_LIM_TABLE"); + OsdiLimFunction *lim_table = NULL; + if (sym) { + lim_table = (OsdiLimFunction *)sym; + } else { + lim_table_len = 0; + } + + for (uint32_t i = 0; i < lim_table_len; i++) { + int expected_args = -1; + IS_LIM_FUN("pnjlim", 2, osdi_pnjlim) + IS_LIM_FUN("limvds", 0, osdi_limvds) + IS_LIM_FUN("fetlim", 1, osdi_fetlim) + IS_LIM_FUN("limitlog", 1, osdi_limitlog) + if (expected_args == -1) { + printf("warning(osdi): unkown $limit function \"%s\"", lim_table[i].name); + } else { + printf("warning(osdi): unexpected number of arguments %i (expected %i) " + "for \"%s\", ignoring...", + lim_table[i].num_args, expected_args, lim_table[i].name); + } + } + + OsdiRegistryEntry *dst = TMALLOC(OsdiRegistryEntry, OSDI_NUM_DESCRIPTORS); + + for (uint32_t i = 0; i < OSDI_NUM_DESCRIPTORS; i++) { + const OsdiDescriptor *descr = &OSDI_DESCRIPTORS[i]; + + uint32_t dt = descr->num_params + descr->num_opvars; + uint32_t temp = descr->num_params + descr->num_opvars + 1; + for (uint32_t param_id = 0; param_id < descr->num_params; param_id++) { + OsdiParamOpvar *param = &descr->param_opvar[param_id]; + for (uint32_t j = 0; j < 1 + param->num_alias; j++) { + char *name = param->name[j]; + if (!strcmp(name, "dt")) { + dt = UINT32_MAX; + } else if (!strcasecmp(name, "dtemp") || !strcasecmp(name, "dt")) { + dt = param_id; + } else if (!strcmp(name, "temp")) { + temp = UINT32_MAX; + } else if (!strcasecmp(name, "temp") || + !strcasecmp(name, "temperature")) { + temp = param_id; + } + } + } + + size_t inst_off = calc_osdi_instance_data_off(descr); + dst[i] = (OsdiRegistryEntry){ + .descriptor = descr, + .inst_offset = (uint32_t)inst_off, + .dt = dt, + .temp = temp, + }; + } + + return (OsdiObjectFile){ + .entrys = dst, + .num_entries = (int)OSDI_NUM_DESCRIPTORS, + }; +} + +inline size_t osdi_instance_data_off(const OsdiRegistryEntry *entry) { + return entry->inst_offset; +} + +inline void *osdi_instance_data(const OsdiRegistryEntry *entry, + GENinstance *inst) { + return (void *)(((char *)inst) + osdi_instance_data_off(entry)); +} + +inline OsdiExtraInstData * +osdi_extra_instance_data(const OsdiRegistryEntry *entry, GENinstance *inst) { + OsdiDescriptor *descr = (OsdiDescriptor *)entry->descriptor; + return (OsdiExtraInstData *)(((char *)inst) + entry->inst_offset + + descr->instance_size); +} + +inline size_t osdi_model_data_off(void) { + return offsetof(OsdiModelData, data); +} + +inline void *osdi_model_data(GENmodel *model) { + return (void *)&((OsdiModelData *)model)->data; +} + +inline void *osdi_model_data_from_inst(GENinstance *inst) { + return osdi_model_data(inst->GENmodPtr); +} + +inline OsdiRegistryEntry *osdi_reg_entry_model(const GENmodel *model) { + return (OsdiRegistryEntry *)ft_sim->devices[model->GENmodType] + ->registry_entry; +} + +inline OsdiRegistryEntry *osdi_reg_entry_inst(const GENinstance *inst) { + return osdi_reg_entry_model(inst->GENmodPtr); +} + +#if defined(__MINGW32__) || defined(HAS_WINGUI) || defined(_MSC_VER) + +/* For reporting error message if formatting fails */ +static const char errstr_fmt[] = + "Unable to find message in dlerr(). System code = %lu"; +static char errstr[sizeof errstr_fmt - 3 + 3 * sizeof(unsigned long)]; + +char *dlerror(void) { + LPVOID lpMsgBuf; + + DWORD rc = FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR)&lpMsgBuf, 0, NULL); + + if (rc == 0) { /* FormatMessage failed */ + (void)sprintf(errstr, errstr_fmt, (unsigned long)GetLastError()); + return errstr; + } + + return lpMsgBuf; /* Return the formatted message */ +} /* end of function dlerror */ + +/* Free message related to dynamic loading */ +static void free_dlerr_msg(char *msg) { + if (msg != errstr) { /* msg is an allocation */ + LocalFree(msg); + } +} /* end of function free_dlerr_msg */ + +#endif /* Windows emulation of dlerr */ diff --git a/src/osdi/osdisetup.c b/src/osdi/osdisetup.c new file mode 100644 index 000000000..f77ef5547 --- /dev/null +++ b/src/osdi/osdisetup.c @@ -0,0 +1,410 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + +#include "ngspice/iferrmsg.h" +#include "ngspice/memory.h" +#include "ngspice/ngspice.h" +#include "ngspice/typedefs.h" + +#include "osdi.h" +#include "osdidefs.h" + +#include +#include +#include + +/* + * Handles any errors raised by the setup_instance and setup_model functions + */ +static int handle_init_info(OsdiInitInfo info, const OsdiDescriptor *descr) { + if (info.flags & (EVAL_RET_FLAG_FATAL | EVAL_RET_FLAG_FINISH)) { + return (E_PANIC); + } + + if (info.num_errors == 0) { + return (OK); + } + + for (uint32_t i = 0; i < info.num_errors; i++) { + OsdiInitError *err = &info.errors[i]; + switch (err->code) { + case INIT_ERR_OUT_OF_BOUNDS: { + char *param = descr->param_opvar[err->payload.parameter_id].name[0]; + printf("Parameter %s is out of bounds!\n", param); + break; + } + default: + printf("Unkown OSDO init error code %d!\n", err->code); + } + } + free(info.errors); + errMsg = tprintf("%i errors occurred during initalization", info.num_errors); + return (E_PRIVATE); +} + +/* + * The OSDI instance data contains the `node_mapping` array. + * Here an index is stored for each node. This function initalizes this array + * with its indecies {0, 1, 2, 3, .., n}. + * The node collapsing information generated by setup_instance is used to + * replace these initial indecies with those that a node is collapsed into. + * For example collapsing nodes i and j sets node_mapping[i] = j. + * + * Terminals can never be collapsed in ngspice because they are allocated by + * ngspice instead of OSDI. Therefore any node collapsing that involves nodes + * `i < connected_terminals` is ignored. + * + * @param const OsdiDescriptor *descr The OSDI descriptor + * @param void *inst The instance data connected_terminals + * @param uint32_t connected_terminals The number of terminals that are not + * internal nodes. + * + * @returns The number of nodes required after collapsing. + * */ +static uint32_t collapse_nodes(const OsdiDescriptor *descr, void *inst, + uint32_t connected_terminals) { + /* access data inside instance */ + uint32_t *node_mapping = + (uint32_t *)(((char *)inst) + descr->node_mapping_offset); + bool *collapsed = (bool *)(((char *)inst) + descr->collapsed_offset); + + /* without collapsing just return the total number of nodes */ + uint32_t num_nodes = descr->num_nodes; + + /* populate nodes with themselves*/ + for (uint32_t i = 0; i < descr->num_nodes; i++) { + node_mapping[i] = i; + } + + for (uint32_t i = 0; i < descr->num_collapsible; i++) { + /* check if the collapse hint (V(x,y) <+ 0) was executed */ + if (!collapsed[i]) { + continue; + } + + uint32_t from = descr->collapsible[i].node_1; + uint32_t to = descr->collapsible[i].node_2; + + /* terminals created by the simulator cannot be collapsed + */ + if (node_mapping[from] < connected_terminals && + (to == UINT32_MAX || node_mapping[to] < connected_terminals || + node_mapping[to] == UINT32_MAX)) { + continue; + } + /* ensure that to is always the smaller node */ + if (to != UINT32_MAX && node_mapping[from] < node_mapping[to]) { + uint32_t temp = from; + from = to; + to = temp; + } + + from = node_mapping[from]; + if (to != UINT32_MAX) { + to = node_mapping[to]; + } + + /* replace nodes mapped to from with to and reduce the number of nodes */ + for (uint32_t j = 0; j < descr->num_nodes; j++) { + if (node_mapping[j] == from) { + node_mapping[j] = to; + } else if (node_mapping[j] > from && node_mapping[j] != UINT32_MAX) { + node_mapping[j] -= 1; + } + } + num_nodes -= 1; + } + return num_nodes; +} + +/* replace node mapping local to the current instance (created by + * collapse_nodes) with global node indicies allocated with CKTmkVolt */ +static void write_node_mapping(const OsdiDescriptor *descr, void *inst, + uint32_t *nodes) { + uint32_t *node_mapping = + (uint32_t *)(((char *)inst) + descr->node_mapping_offset); + for (uint32_t i = 0; i < descr->num_nodes; i++) { + if (node_mapping[i] == UINT32_MAX) { + /* gnd node */ + node_mapping[i] = 0; + } else { + node_mapping[i] = nodes[node_mapping[i]]; + } + } +} + +/* NGSPICE state vectors for an instance are always continous so we just write + * state_start .. state_start + num_state to state_idx */ +static void write_state_ids(const OsdiDescriptor *descr, void *inst, + uint32_t state_start) { + uint32_t *state_idx = (uint32_t *)(((char *)inst) + descr->state_idx_off); + for (uint32_t i = 0; i < descr->num_states; i++) { + state_idx[i] = state_start + i; + } +} + +static int init_matrix(SMPmatrix *matrix, const OsdiDescriptor *descr, + void *inst) { + uint32_t *node_mapping = + (uint32_t *)(((char *)inst) + descr->node_mapping_offset); + + double **jacobian_ptr_resist = + (double **)(((char *)inst) + descr->jacobian_ptr_resist_offset); + + for (uint32_t i = 0; i < descr->num_jacobian_entries; i++) { + uint32_t equation = descr->jacobian_entries[i].nodes.node_1; + uint32_t unkown = descr->jacobian_entries[i].nodes.node_2; + equation = node_mapping[equation]; + unkown = node_mapping[unkown]; + double *ptr = SMPmakeElt(matrix, (int)equation, (int)unkown); + + if (ptr == NULL) { + return (E_NOMEM); + } + jacobian_ptr_resist[i] = ptr; + uint32_t react_off = descr->jacobian_entries[i].react_ptr_off; + // complex number for ac analysis + if (react_off != UINT32_MAX) { + + double **jacobian_ptr_react = (double **)(((char *)inst) + react_off); + *jacobian_ptr_react = ptr + 1; + } + } + return (OK); +} + +int OSDIsetup(SMPmatrix *matrix, GENmodel *inModel, CKTcircuit *ckt, + int *states) { + OsdiInitInfo init_info; + OsdiNgspiceHandle handle; + GENmodel *gen_model; + int res; + int error; + CKTnode *tmp; + GENinstance *gen_inst; + int err; + + OsdiRegistryEntry *entry = osdi_reg_entry_model(inModel); + const OsdiDescriptor *descr = entry->descriptor; + OsdiSimParas sim_params_ = get_simparams(ckt); + OsdiSimParas *sim_params = &sim_params_; + + /* setup a temporary buffer */ + uint32_t *node_ids = TMALLOC(uint32_t, descr->num_nodes); + + /* determine the number of states required by each instance */ + int num_states = (int)descr->num_states; + for (uint32_t i = 0; i < descr->num_nodes; i++) { + if (descr->nodes[i].react_residual_off != UINT32_MAX) { + num_states += 2; + } + } + + for (gen_model = inModel; gen_model; gen_model = gen_model->GENnextModel) { + void *model = osdi_model_data(gen_model); + + /* setup model parameter (setup_model)*/ + handle = (OsdiNgspiceHandle){.kind = 1, .name = gen_model->GENmodName}; + descr->setup_model((void *)&handle, model, sim_params, &init_info); + res = handle_init_info(init_info, descr); + if (res) { + errRtn = "OSDI setup_model"; + continue; + } + + for (gen_inst = gen_model->GENinstances; gen_inst; + gen_inst = gen_inst->GENnextInstance) { + void *inst = osdi_instance_data(entry, gen_inst); + + /* special handling for temperature parameters */ + double temp = ckt->CKTtemp; + OsdiExtraInstData *extra_inst_data = + osdi_extra_instance_data(entry, gen_inst); + if (extra_inst_data->temp_given) { + temp = extra_inst_data->temp; + } + if (extra_inst_data->dt_given) { + temp += extra_inst_data->dt; + } + + /* find number of connected ports to allow evaluation of $port_connected + * and to handle node collapsing correctly later + * */ + int *terminals = (int *)(gen_inst + 1); + uint32_t connected_terminals = descr->num_terminals; + for (uint32_t i = 0; i < descr->num_terminals; i++) { + if (terminals[i] == -1) { + connected_terminals = i; + break; + } + } + + /* calculate op independent data, init instance parameters and determine + which collapsing occurs*/ + handle = (OsdiNgspiceHandle){.kind = 2, .name = gen_inst->GENname}; + descr->setup_instance((void *)&handle, inst, model, temp, + connected_terminals, sim_params, &init_info); + res = handle_init_info(init_info, descr); + if (res) { + errRtn = "OSDI setup_instance"; + continue; + } + + /* setup the instance nodes */ + + uint32_t num_nodes = collapse_nodes(descr, inst, connected_terminals); + /* copy terminals */ + memcpy(node_ids, gen_inst + 1, sizeof(int) * connected_terminals); + /* create internal nodes as required */ + for (uint32_t i = connected_terminals; i < num_nodes; i++) { + // TODO handle currents correctly + if (descr->nodes[i].is_flow) { + error = CKTmkCur(ckt, &tmp, gen_inst->GENname, descr->nodes[i].name); + } else { + error = CKTmkVolt(ckt, &tmp, gen_inst->GENname, descr->nodes[i].name); + } + if (error) + return (error); + node_ids[i] = (uint32_t)tmp->number; + // TODO nodeset? + } + write_node_mapping(descr, inst, node_ids); + + /* now that we have the node mapping we can create the matrix entries */ + err = init_matrix(matrix, descr, inst); + if (err) { + return err; + } + + /* reserve space in the state vector*/ + gen_inst->GENstate = *states; + write_state_ids(descr, inst, (uint32_t)*states); + *states += num_states; + } + } + + free(node_ids); + + return (OK); +} + +/* OSDI does not differentiate between setup and temperature update so we just + * call the setup routines again and assume that node collapsing (and therefore + * node mapping) stays the same + */ +extern int OSDItemp(GENmodel *inModel, CKTcircuit *ckt) { + OsdiInitInfo init_info; + OsdiNgspiceHandle handle; + GENmodel *gen_model; + int res; + GENinstance *gen_inst; + + OsdiRegistryEntry *entry = osdi_reg_entry_model(inModel); + const OsdiDescriptor *descr = entry->descriptor; + + OsdiSimParas sim_params_ = get_simparams(ckt); + OsdiSimParas *sim_params = &sim_params_; + + for (gen_model = inModel; gen_model != NULL; + gen_model = gen_model->GENnextModel) { + void *model = osdi_model_data(gen_model); + + handle = (OsdiNgspiceHandle){.kind = 4, .name = gen_model->GENmodName}; + descr->setup_model((void *)&handle, model, sim_params, &init_info); + res = handle_init_info(init_info, descr); + if (res) { + errRtn = "OSDI setup_model (OSDItemp)"; + continue; + } + + for (gen_inst = gen_model->GENinstances; gen_inst != NULL; + gen_inst = gen_inst->GENnextInstance) { + void *inst = osdi_instance_data(entry, gen_inst); + + // special handleing for temperature parameters + double temp = ckt->CKTtemp; + OsdiExtraInstData *extra_inst_data = + osdi_extra_instance_data(entry, gen_inst); + if (extra_inst_data->temp_given) { + temp = extra_inst_data->temp; + } + if (extra_inst_data->dt_given) { + temp += extra_inst_data->dt; + } + + handle = (OsdiNgspiceHandle){.kind = 2, .name = gen_inst->GENname}; + /* find number of connected ports to allow evaluation of $port_connected + * and to handle node collapsing correctly later + * */ + int *terminals = (int *)(gen_inst + 1); + uint32_t connected_terminals = descr->num_terminals; + for (uint32_t i = 0; i < descr->num_terminals; i++) { + if (terminals[i] == -1) { + connected_terminals = i; + break; + } + } + + descr->setup_instance((void *)&handle, inst, model, temp, + connected_terminals, sim_params, &init_info); + res = handle_init_info(init_info, descr); + if (res) { + errRtn = "OSDI setup_instance (OSDItemp)"; + continue; + } + // TODO check that there are no changes in node collapse? + } + } + return (OK); +} + +/* delete internal nodes + */ +extern int OSDIunsetup(GENmodel *inModel, CKTcircuit *ckt) { + GENmodel *gen_model; + GENinstance *gen_inst; + int num; + + OsdiRegistryEntry *entry = osdi_reg_entry_model(inModel); + const OsdiDescriptor *descr = entry->descriptor; + + for (gen_model = inModel; gen_model != NULL; + gen_model = gen_model->GENnextModel) { + + for (gen_inst = gen_model->GENinstances; gen_inst != NULL; + gen_inst = gen_inst->GENnextInstance) { + void *inst = osdi_instance_data(entry, gen_inst); + + // reset is collapsible + bool *collapsed = (bool *)(((char *)inst) + descr->collapsed_offset); + memset(collapsed, 0, sizeof(bool) * descr->num_collapsible); + + uint32_t *node_mapping = + (uint32_t *)(((char *)inst) + descr->node_mapping_offset); + for (uint32_t i = 0; i < descr->num_nodes; i++) { + num = (int)node_mapping[i]; + // hand coded implementations just know which nodes were collapsed + // however nodes may be collapsed multiple times so we can't easily use + // an approach like that instead we delete all nodes + // Deleting twiche with CKLdltNNum is fine (entry is already removed + // from the linked list and therefore no action is taken). + // However CKTdltNNum (rightfully) throws an error when trying to delete + // an external node. Therefore we need to check for each node that it is + // an internal node + if (ckt->prev_CKTlastNode->number && + num > ckt->prev_CKTlastNode->number) { + CKTdltNNum(ckt, num); + } + } + } + } + return (OK); +} diff --git a/src/osdi/osditrunc.c b/src/osdi/osditrunc.c new file mode 100644 index 000000000..e499b60d5 --- /dev/null +++ b/src/osdi/osditrunc.c @@ -0,0 +1,43 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + */ + +#include "ngspice/cktdefs.h" +#include "osdidefs.h" + +int OSDItrunc(GENmodel *in_model, CKTcircuit *ckt, double *timestep) { + OsdiRegistryEntry *entry = osdi_reg_entry_model(in_model); + const OsdiDescriptor *descr = entry->descriptor; + uint32_t offset = descr->bound_step_offset; + bool has_boundstep = offset != UINT32_MAX; + offset += entry->inst_offset; + + for (GENmodel *model = in_model; model; model = model->GENnextModel) { + for (GENinstance *inst = model->GENinstances; inst; + inst = inst->GENnextInstance) { + + if (has_boundstep) { + double *del = (double *)(((char *)inst) + offset); + if (*del < *timestep) { + *timestep = *del; + } + } + + int state = inst->GENstate; + for (uint32_t i = 0; i < descr->num_nodes; i++) { + if (descr->nodes[i].react_residual_off != UINT32_MAX) { + CKTterr(state, ckt, timestep); + state += 2; + } + } + } + } + return 0; +} diff --git a/src/spicelib/devices/dev.c b/src/spicelib/devices/dev.c index 43b5180c9..91343a295 100644 --- a/src/spicelib/devices/dev.c +++ b/src/spicelib/devices/dev.c @@ -356,11 +356,7 @@ void load_alldevs(void){ } #endif -/*-------------------- XSPICE additions below ----------------------*/ -#ifdef XSPICE -#include "ngspice/mif.h" -#include "ngspice/cm.h" -#include "ngspice/cpextern.h" +#if defined(XSPICE) || defined(OSDI) #include "ngspice/fteext.h" /* for ft_sim */ #include "ngspice/cktdefs.h" /* for DEVmaxnum */ @@ -379,7 +375,14 @@ static void relink(void) { return; } -int add_device(int n, SPICEdev **devs, int flag){ +#endif + +/*-------------------- XSPICE additions below ----------------------*/ +#ifdef XSPICE +#include "ngspice/cm.h" +#include "ngspice/cpextern.h" +#include "ngspice/mif.h" +int add_device(int n, SPICEdev **devs, int flag) { int i; int dnum = DEVNUM + n; DEVices = TREALLOC(SPICEdev *, DEVices, dnum); @@ -566,3 +569,35 @@ static void free_dlerr_msg(char *msg) #endif /* Windows emulation of dlopen, dlsym, and dlerr */ #endif /*-------------------- end of XSPICE additions ----------------------*/ + +#ifdef OSDI +#include "ngspice/osdiitf.h" + +static int osdi_add_device(int n, OsdiRegistryEntry *devs) { + int i; + int dnum = DEVNUM + n; + DEVices = TREALLOC(SPICEdev *, DEVices, dnum); +#ifdef XSPICE + DEVicesfl = TREALLOC(int, DEVicesfl, dnum); +#endif + for (i = 0; i < n; i++) { +#ifdef TRACE + printf("Added device: %s\n", devs[i]->DEVpublic.name); +#endif + DEVices[DEVNUM + i] = osdi_create_spicedev(&devs[i]); + } + DEVNUM += n; + relink(); + return 0; +} + +int load_osdi(const char *path) { + OsdiObjectFile file = load_object_file(path); + if (file.num_entries < 0) { + return file.num_entries; + } + + osdi_add_device(file.num_entries, file.entrys); + return 0; +} +#endif diff --git a/src/spicelib/devices/dev.h b/src/spicelib/devices/dev.h index 3356d04b7..e4ec3f1bf 100644 --- a/src/spicelib/devices/dev.h +++ b/src/spicelib/devices/dev.h @@ -15,5 +15,9 @@ int DEVflag(int type); void load_alldevs(void); int load_dev(char *name); #endif + +#ifdef OSDI +int load_osdi(const char *); +#endif #endif diff --git a/src/spicelib/parser/Makefile.am b/src/spicelib/parser/Makefile.am index 5ef6e38ca..fa693d835 100644 --- a/src/spicelib/parser/Makefile.am +++ b/src/spicelib/parser/Makefile.am @@ -71,6 +71,11 @@ libinp_la_SOURCES = \ sperror.c \ inpxx.h + +if OSDI_WANTED +libinp_la_SOURCES += inp2a.c +endif + AM_CPPFLAGS = @AM_CPPFLAGS@ -I$(top_srcdir)/src/include -I$(top_srcdir)/src/frontend AM_CFLAGS = $(STATIC) AM_YFLAGS = -d diff --git a/src/spicelib/parser/inp2a.c b/src/spicelib/parser/inp2a.c new file mode 100644 index 000000000..ba3c2a3da --- /dev/null +++ b/src/spicelib/parser/inp2a.c @@ -0,0 +1,109 @@ +/********** +Copyright 1990 Regents of the University of California. All rights reserved. +Author: 1988 Thomas L. Quarles +Modified: 2001 Paolo Nenzi (Cider Integration) +**********/ + +#include "ngspice/ngspice.h" + +#include "ngspice/devdefs.h" +#include "ngspice/fteext.h" +#include "ngspice/ifsim.h" +#include "ngspice/inpdefs.h" +#include "ngspice/inpmacs.h" + +#include "inpxx.h" +#include + +#ifdef XSPICE +#include "ngspice/mifproto.h" +#endif + +void INP2A(CKTcircuit *ckt, INPtables *tab, struct card *current) { + /* Mname [L=] + * [W=] [AD=] [AS=] [PD=] + * [PS=] [NRD=] [NRS=] [OFF] + * [IC=,,] + */ + + int type; /* the type the model says it is */ + char *line; /* the part of the current line left to parse */ + char *name; /* the resistor's name */ + // limit to at most 20 nodes + const int max_i = 20; + CKTnode *node[20]; + int error; /* error code temporary */ + int numnodes; /* flag indicating 4 or 5 (or 6 or 7) nodes */ + GENinstance *fast; /* pointer to the actual instance */ + int waslead; /* flag to indicate that funny unlabeled number was found */ + double leadval; /* actual value of unlabeled number */ + INPmodel *thismodel; /* pointer to model description for user's model */ + GENmodel *mdfast; /* pointer to the actual model */ + int i; + + line = current->line; + + INPgetNetTok(&line, &name, 1); + INPinsert(&name, tab); + + for (i = 0;; i++) { + char *token; + INPgetNetTok(&line, &token, 1); + + if (i >= 2) { + txfree(INPgetMod(ckt, token, &thismodel, tab)); + + /* /1* check if using model binning -- pass in line since need 'l' and 'w' *1/ */ + /* if (!thismodel) */ + /* txfree(INPgetModBin(ckt, token, &thismodel, tab, line)); */ + + if (thismodel) { + INPinsert(&token, tab); + break; + } + } + if (i >= max_i) { + LITERR("could not find a valid modelname"); + return; + } + INPtermInsert(ckt, &token, tab, &node[i]); + } + + type = thismodel->INPmodType; + mdfast = thismodel->INPmodfast; + IFdevice *dev = ft_sim->devices[type]; + + if (!dev->registry_entry) { + +#ifdef XSPICE + MIF_INP2A(ckt, tab, current); +#else + LITERR("incorrect model type! Expected OSDI device"); +#endif + return; + } + + if (i == 0) { + LITERR("not enough nodes"); + return; + } + + if (i > *dev->terms) { + LITERR("too many nodes connected to instance"); + return; + } + + numnodes = i; + + IFC(newInstance, (ckt, mdfast, &fast, name)); + + for (i = 0; i < *dev->terms; i++) + if (i < numnodes) + IFC(bindNode, (ckt, fast, i + 1, node[i])); + else + GENnode(fast)[i] = -1; + + PARSECALL((&line, ckt, type, fast, &leadval, &waslead, tab)); + if (waslead) + LITERR(" error: no unlabeled parameter permitted on osdi devices\n"); +} diff --git a/src/spicelib/parser/inpdomod.c b/src/spicelib/parser/inpdomod.c index f4a30ce35..986e7f7f3 100644 --- a/src/spicelib/parser/inpdomod.c +++ b/src/spicelib/parser/inpdomod.c @@ -679,7 +679,7 @@ char *INPdomodel(CKTcircuit *ckt, struct card *image, INPtables * tab) /* -------- Default action --------- */ else { -#ifndef XSPICE +#if !defined(XSPICE) && !defined(OSDI) type = -1; err = tprintf("unknown model type %s - ignored\n", type_name); #else diff --git a/src/spicelib/parser/inpgmod.c b/src/spicelib/parser/inpgmod.c index cd5aa8d9f..8892343b7 100644 --- a/src/spicelib/parser/inpgmod.c +++ b/src/spicelib/parser/inpgmod.c @@ -13,8 +13,11 @@ Modified: 2001 Paolo Nenzi (Cider Integration) #include "ngspice/cpstd.h" #include "ngspice/fteext.h" #include "ngspice/compatmode.h" +#include "ngspice/devdefs.h" #include "inpxx.h" #include +#include +#include #ifdef CIDER @@ -111,6 +114,14 @@ create_model(CKTcircuit *ckt, INPmodel *modtmp, INPtables *tab) INPgetNetTok(&line, &parm, 1); /* throw away 'modname' */ tfree(parm); +#ifdef OSDI + /* osdi models don't accept their device type as an argument */ + if (device->registry_entry){ + INPgetNetTok(&line, &parm, 1); /* throw away osdi */ + tfree(parm); + } +#endif + while (*line) { INPgetTok(&line, &parm, 1); if (!*parm) { @@ -130,6 +141,7 @@ create_model(CKTcircuit *ckt, INPmodel *modtmp, INPtables *tab) /* just grab the number and throw away */ /* since we already have that info from pass1 */ INPgetValue(ckt, &line, IF_REAL, tab); + } else { p = find_instance_parameter(parm, device); diff --git a/src/spicelib/parser/inppas2.c b/src/spicelib/parser/inppas2.c index c53c85922..11e32aafb 100644 --- a/src/spicelib/parser/inppas2.c +++ b/src/spicelib/parser/inppas2.c @@ -99,7 +99,12 @@ void INPpas2(CKTcircuit *ckt, struct card *data, INPtables * tab, TSKtask *task) /* blank line (tab leading) */ break; -#ifdef XSPICE +#ifdef OSDI + case 'A': /* Aname */ + // OSDI handles xspice + INP2A(ckt, tab, current); + break; +#elif XSPICE /* gtri - add - wbk - 10/23/90 - add case for 'A' devices */ case 'A': /* Aname */ @@ -245,7 +250,6 @@ void INPpas2(CKTcircuit *ckt, struct card *data, INPtables * tab, TSKtask *task) case 'B': /* Bname [V=expr] [I=expr] */ /* Arbitrary source. */ - INP2B(ckt, tab, current); break; case '.': /* . Many possibilities */ diff --git a/src/spicelib/parser/inpxx.h b/src/spicelib/parser/inpxx.h index 81e9355db..ae47f0465 100644 --- a/src/spicelib/parser/inpxx.h +++ b/src/spicelib/parser/inpxx.h @@ -8,6 +8,9 @@ /* inp2xx.c */ +#ifdef OSDI +void INP2A(CKTcircuit *ckt, INPtables *tab, struct card *current); +#endif void INP2B(CKTcircuit *ckt, INPtables *tab, struct card *current); void INP2C(CKTcircuit *ckt, INPtables *tab, struct card *current); void INP2D(CKTcircuit *ckt, INPtables *tab, struct card *current); diff --git a/test_cases/capacitor/.empty.txt b/test_cases/capacitor/.empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test_cases/capacitor/capacitor.c b/test_cases/capacitor/capacitor.c new file mode 100644 index 000000000..411b2eb7c --- /dev/null +++ b/test_cases/capacitor/capacitor.c @@ -0,0 +1,374 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + * + * 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..973fcdf26 --- /dev/null +++ b/test_cases/capacitor/netlist.sp @@ -0,0 +1,43 @@ +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 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 +pre_osdi capacitor.osdi +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..be1fc38dc --- /dev/null +++ b/test_cases/capacitor/test_capacitor.py @@ -0,0 +1,160 @@ +""" test OSDI simulation of capacitor +""" +import os, shutil +import numpy as np +import pandas as pd +import sys +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) + +from testing import prepare_test + +# 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. + +directory = os.path.dirname(__file__) + + +def test_ngspice(): + dir_osdi, dir_built_in = prepare_test(directory) + + # 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/cccs/.empty.txt b/test_cases/cccs/.empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test_cases/ccvs/.empty.txt b/test_cases/ccvs/.empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test_cases/diode/diode.c b/test_cases/diode/diode.c new file mode 100644 index 000000000..795fe2f16 --- /dev/null +++ b/test_cases/diode/diode.c @@ -0,0 +1,913 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + * + * 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]; +extern OsdiLimFunction OSDI_LIM_TABLE[1]; +extern uint32_t OSDI_LIM_TABLE_LEN; + +#define sqrt2 1.4142135623730950488016887242097 + +#define IGNORE(x) (void)x + +// 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 4 +#define A 0 +#define C 1 +#define TNODE 2 +#define CI 3 + +#define NUM_COLLAPSIBLE 2 + +// number of matrix entries and definitions for Jacobian entries for nicer +// syntax in this file +#define NUM_MATRIX 14 +#define CI_CI 0 +#define CI_C 1 +#define C_CI 2 +#define C_C 3 +#define A_A 4 +#define A_CI 5 +#define CI_A 6 +#define A_TNODE 7 +#define C_TNODE 8 +#define CI_TNODE 9 +#define TNODE_TNODE 10 +#define TNODE_A 11 +#define TNODE_C 12 +#define TNODE_CI 13 + +// The model structure for the diode +typedef struct DiodeModel { + double Rs; + bool Rs_given; + double Is; + bool Is_given; + double zetars; + bool zetars_given; + double N; + bool N_given; + double Cj0; + bool Cj0_given; + double Vj; + bool Vj_given; + double M; + bool M_given; + double Rth; + bool Rth_given; + double zetarth; + bool zetarth_given; + double zetais; + bool zetais_given; + double Tnom; + bool Tnom_given; + double mfactor; // multiplication factor for parallel devices + bool mfactor_given; + // InitError errors[MAX_ERROR_NUM], +} DiodeModel; + +// The instace structure for the diode +typedef struct DiodeInstace { + double mfactor; // multiplication factor for parallel devices + bool mfactor_given; + double temperature; + double residual_resist[NUM_NODES]; + double lim_rhs_resist_A; + double lim_rhs_resist_CI; + double lim_rhs_react_A; + double lim_rhs_react_CI; + double residual_react_A; + double residual_react_CI; + double jacobian_resist[NUM_MATRIX]; + double jacobian_react[NUM_MATRIX]; + bool collapsed[NUM_COLLAPSIBLE]; + double *jacobian_ptr_resist[NUM_MATRIX]; + double *jacobian_ptr_react[NUM_MATRIX]; + uint32_t node_off[NUM_NODES]; + uint32_t state_idx; +} DiodeInstace; + +#define EXP_LIM 80.0 + +static double limexp(double x) { + if (x < EXP_LIM) { + return exp(x); + } else { + return exp(EXP_LIM) * (x + 1 - EXP_LIM); + } +} + +static double dlimexp(double x) { + if (x < EXP_LIM) { + return exp(x); + } else { + return exp(EXP_LIM); + } +} + +// implementation of the access function as defined by the OSDI spec +static void *osdi_access(void *inst_, void *model_, uint32_t id, + uint32_t flags) { + DiodeModel *model = (DiodeModel *)model_; + DiodeInstace *inst = (DiodeInstace *)inst_; + + bool *given; + void *value; + + switch (id) // id of params defined in param_opvar array + { + case 0: + if (flags & ACCESS_FLAG_INSTANCE) { + value = (void *)&inst->mfactor; + given = &inst->mfactor_given; + } else { + value = (void *)&model->mfactor; + given = &model->mfactor_given; + } + break; + case 1: + value = (void *)&model->Rs; + given = &model->Rs_given; + break; + case 2: + value = (void *)&model->Is; + given = &model->Is_given; + break; + case 3: + value = (void *)&model->zetars; + given = &model->zetars_given; + break; + case 4: + value = (void *)&model->N; + given = &model->N_given; + break; + case 5: + value = (void *)&model->Cj0; + given = &model->Cj0_given; + break; + case 6: + value = (void *)&model->Vj; + given = &model->Vj_given; + break; + case 7: + value = (void *)&model->M; + given = &model->M_given; + break; + case 8: + value = &model->Rth; + given = &model->Rth_given; + break; + case 9: + value = (void *)&model->zetarth; + given = &model->zetarth_given; + break; + case 10: + value = (void *)&model->zetais; + given = &model->zetais_given; + break; + case 11: + value = (void *)&model->Tnom; + given = &model->Tnom_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 +static void setup_model(void *handle, void *model_, OsdiSimParas *sim_params, + OsdiInitInfo *res) { + DiodeModel *model = (DiodeModel *)model_; + + IGNORE(handle); + IGNORE(sim_params); + + // set parameters and check bounds + if (!model->mfactor_given) { + model->mfactor = 1.0; + } + if (!model->Rs_given) { + model->Rs = 1e-9; + } + if (!model->Is_given) { + model->Is = 1e-14; + } + if (!model->zetars_given) { + model->zetars = 0; + } + if (!model->N_given) { + model->N = 1; + } + if (!model->Cj0_given) { + model->Cj0 = 0; + } + if (!model->Vj_given) { + model->Vj = 1.0; + } + if (!model->M_given) { + model->M = 0.5; + } + if (!model->Rth_given) { + model->Rth = 0; + } + if (!model->zetarth_given) { + model->zetarth = 0; + } + if (!model->zetais_given) { + model->zetais = 0; + } + if (!model->Tnom_given) { + model->Tnom = 300; + } + + *res = (OsdiInitInfo){.flags = 0, .num_errors = 0, .errors = NULL}; +} + +// implementation of the setup_instace function as defined in the OSDI spec +static void setup_instance(void *handle, void *inst_, void *model_, + double temperature, uint32_t num_terminals, + OsdiSimParas *sim_params, OsdiInitInfo *res) { + + IGNORE(handle); + IGNORE(num_terminals); + IGNORE(sim_params); + + DiodeInstace *inst = (DiodeInstace *)inst_; + DiodeModel *model = (DiodeModel *)model_; + + // Here the logic for node collapsing ist implemented. The indices in this + // list must adhere to the "collapsible" List of node pairs. + if (model->Rs < 1e-9) { // Rs between Ci C + inst->collapsed[0] = true; + } + if (model->Rth < 1e-9) { // Rs between Ci C + inst->collapsed[1] = true; + } + + if (!inst->mfactor_given) { + if (model->mfactor_given) { + inst->mfactor = model->mfactor; + } else { + inst->mfactor = 1; + } + } + + inst->temperature = temperature; + *res = (OsdiInitInfo){.flags = 0, .num_errors = 0, .errors = NULL}; +} + +#define CONSTsqrt2 1.4142135623730950488016887242097 +typedef double (*pnjlim_t)(bool, bool *, double, double, double, double); + +// implementation of the eval function as defined in the OSDI spec +static uint32_t eval(void *handle, void *inst_, void *model_, + OsdiSimInfo *info) { + IGNORE(handle); + DiodeModel *model = (DiodeModel *)model_; + DiodeInstace *inst = (DiodeInstace *)inst_; + + // get voltages + double *prev_solve = info->prev_solve; + double va = prev_solve[inst->node_off[A]]; + double vc = prev_solve[inst->node_off[C]]; + double vci = prev_solve[inst->node_off[CI]]; + double vdtj = prev_solve[inst->node_off[TNODE]]; + + double vcic = vci - vc; + double vaci = va - vci; + + double gmin = 1e-12; + for (int i = 0; info->paras.names[i] != NULL; i++) { + if (strcmp(info->paras.names[i], "gmin") == 0) { + gmin = info->paras.vals[i]; + } + } + + uint32_t ret_flags = 0; + //////////////////////////////// + // evaluate model equations + //////////////////////////////// + + // temperature update + double pk = 1.3806503e-23; + double pq = 1.602176462e-19; + double t_dev = inst->temperature + vdtj; + double tdev_tnom = t_dev / model->Tnom; + double rs_t = model->Rs * pow(tdev_tnom, model->zetars); + double rth_t = model->Rth * pow(tdev_tnom, model->zetarth); + double is_t = model->Is * pow(tdev_tnom, model->zetais); + double vt = t_dev * pk / pq; + + double delvaci = 0.0; + if (info->flags & ENABLE_LIM && OSDI_LIM_TABLE[0].func_ptr) { + double vte = inst->temperature * pk / pq; + bool icheck = false; + double vaci_old = info->prev_state[inst->state_idx]; + pnjlim_t pnjlim = OSDI_LIM_TABLE[0].func_ptr; + double vaci_new = pnjlim(info->flags & INIT_LIM, &icheck, vaci, vaci_old, + vte, vte * log(vte / (sqrt2 * model->Is))); + printf("%g %g\n", vaci, vaci_new); + delvaci = vaci_new - vaci; + vaci = vaci_new; + info->prev_state[inst->state_idx] = vaci; + } else { + printf("ok?"); + } + + // derivatives w.r.t. temperature + double rs_dt = model->zetars * model->Rs * + pow(tdev_tnom, model->zetars - 1.0) / model->Tnom; + double rth_dt = model->zetarth * model->Rth * + pow(tdev_tnom, model->zetarth - 1.0) / model->Tnom; + double is_dt = model->zetais * model->Is * + pow(tdev_tnom, model->zetais - 1.0) / model->Tnom; + double vt_tj = pk / pq; + + // evaluate model equations and calculate all derivatives + // diode current + double id = is_t * (limexp(vaci / (model->N * vt)) - 1.0); + double gd = is_t / vt * dlimexp(vaci / (model->N * vt)); + double gdt = -is_t * dlimexp(vaci / (model->N * vt)) * vaci / model->N / vt / + vt * vt_tj + + 1.0 * exp((vaci / (model->N * vt)) - 1.0) * is_dt; + + // resistor + double irs = 0; + double g = 0; + double grt = 0; + if (!inst->collapsed[0]) { + irs = vcic / rs_t; + g = 1.0 / rs_t; + grt = -irs / rs_t * rs_dt; + } + + // thermal resistance + double irth = 0; + double gt = 0; + if (!inst->collapsed[1]) { + irth = vdtj / rth_t; + gt = 1.0 / rth_t - irth / rth_t * rth_dt; + } + + // charge + double vf = model->Vj * (1.0 - pow(3.04, -1.0 / model->M)); + double x = (vf - vaci) / vt; + double x_vt = -x / vt; + double x_dtj = x_vt * vt_tj; + double x_vaci = -1.0 / vt; + double y = sqrt(x * x + 1.92); + double y_x = 0.5 / y * 2.0 * x; + double y_vaci = y_x * x_vaci; + double y_dtj = y_x * x_dtj; + double vd = vf - vt * (x + y) / (2.0); + double vd_x = -vt / 2.0; + double vd_y = -vt / 2.0; + double vd_vt = -(x + y) / (2.0); + double vd_dtj = vd_x * x_dtj + vd_y * y_dtj + vd_vt * vt_tj; + double vd_vaci = vd_x * x_vaci + vd_y * y_vaci; + double qd = model->Cj0 * vaci * model->Vj * + (1.0 - pow(1.0 - vd / model->Vj, 1.0 - model->M)) / + (1.0 - model->M); + double qd_vd = model->Cj0 * model->Vj / (1.0 - model->M) * (1.0 - model->M) * + pow(1.0 - vd / model->Vj, 1.0 - model->M - 1.0) / model->Vj; + double qd_dtj = qd_vd * vd_dtj; + double qd_vaci = qd_vd * vd_vaci; + + // thermal power source = current source + double ith = id * vaci; + double ith_vtj = gdt * vaci; + double ith_vcic = 0; + double ith_vaci = gd * vaci + id; + if (!inst->collapsed[0]) { + ith_vcic = 2.0 * vcic / rs_t; + ith += pow(vcic, 2.0) / rs_t; + ith_vtj -= -pow(vcic, 2.0) / rs_t / rs_t * rs_dt; + } + + id += gmin * vaci; + gd += gmin; + + double mfactor = inst->mfactor; + + //////////////// + // write rhs + //////////////// + + if (info->flags & CALC_RESIST_RESIDUAL) { + // write resist rhs + inst->residual_resist[A] = id * mfactor; + inst->residual_resist[CI] = -id * mfactor + irs * mfactor; + inst->residual_resist[C] = -irs * mfactor; + inst->residual_resist[TNODE] = -ith * mfactor + irth * mfactor; + } + + if (info->flags & CALC_RESIST_LIM_RHS) { + // write resist rhs + inst->lim_rhs_resist_A = gd * mfactor * delvaci; + inst->lim_rhs_resist_CI = -gd * mfactor * delvaci; + } + + if (info->flags & CALC_REACT_RESIDUAL) { + // write react rhs + inst->residual_react_A = qd * mfactor; + inst->residual_react_CI = -qd * mfactor; + } + + if (info->flags & CALC_REACT_LIM_RHS) { + // write resist rhs + inst->lim_rhs_react_A = qd_vaci * mfactor * delvaci; + inst->lim_rhs_react_CI = -qd_vaci * mfactor * delvaci; + } + + ////////////////// + // write Jacobian + ////////////////// + + if (info->flags & CALC_RESIST_JACOBIAN) { + // stamp diode (current flowing from Ci into A) + inst->jacobian_resist[A_A] = gd * mfactor; + inst->jacobian_resist[A_CI] = -gd * mfactor; + inst->jacobian_resist[CI_A] = -gd * mfactor; + inst->jacobian_resist[CI_CI] = gd * mfactor; + // diode thermal + inst->jacobian_resist[A_TNODE] = gdt * mfactor; + inst->jacobian_resist[CI_TNODE] = -gdt * mfactor; + + // stamp resistor (current flowing from C into CI) + inst->jacobian_resist[CI_CI] += g * mfactor; + inst->jacobian_resist[CI_C] = -g * mfactor; + inst->jacobian_resist[C_CI] = -g * mfactor; + inst->jacobian_resist[C_C] = g * mfactor; + // resistor thermal + inst->jacobian_resist[CI_TNODE] = grt * mfactor; + inst->jacobian_resist[C_TNODE] = -grt * mfactor; + + // stamp rth flowing into node dTj + inst->jacobian_resist[TNODE_TNODE] = gt * mfactor; + + // stamp ith flowing out of T node + inst->jacobian_resist[TNODE_TNODE] -= ith_vtj * mfactor; + inst->jacobian_resist[TNODE_CI] = (ith_vcic - ith_vaci) * mfactor; + inst->jacobian_resist[TNODE_C] = -ith_vcic * mfactor; + inst->jacobian_resist[TNODE_A] = ith_vaci * mfactor; + } + + if (info->flags & CALC_REACT_JACOBIAN) { + // write react matrix + // stamp Qd between nodes A and Ci depending also on dT + inst->jacobian_react[A_A] = qd_vaci * mfactor; + inst->jacobian_react[A_CI] = -qd_vaci * mfactor; + inst->jacobian_react[CI_A] = -qd_vaci * mfactor; + inst->jacobian_react[CI_CI] = qd_vaci * mfactor; + + inst->jacobian_react[A_TNODE] = qd_dtj * mfactor; + inst->jacobian_react[CI_TNODE] = -qd_dtj * mfactor; + } + + return ret_flags; +} + +// TODO implementation of the load_noise function as defined in the OSDI spec +static void load_noise(void *inst, void *model, double freq, double *noise_dens, + double *ln_noise_dens) { + IGNORE(inst); + IGNORE(model); + IGNORE(freq); + IGNORE(noise_dens); + IGNORE(ln_noise_dens); + // TODO add noise to example +} + +#define LOAD_RESIDUAL_RESIST(name) \ + dst[inst->node_off[name]] += inst->residual_resist[name]; + +// implementation of the load_rhs_resist function as defined in the OSDI spec +static void load_residual_resist(void *inst_, void *model, double *dst) { + DiodeInstace *inst = (DiodeInstace *)inst_; + + IGNORE(model); + LOAD_RESIDUAL_RESIST(A) + LOAD_RESIDUAL_RESIST(CI) + LOAD_RESIDUAL_RESIST(C) + LOAD_RESIDUAL_RESIST(TNODE) +} + +// implementation of the load_rhs_react function as defined in the OSDI spec +static void load_residual_react(void *inst_, void *model, double *dst) { + IGNORE(model); + DiodeInstace *inst = (DiodeInstace *)inst_; + + dst[inst->node_off[A]] += inst->residual_react_A; + dst[inst->node_off[CI]] += inst->residual_react_CI; +} + +// implementation of the load_lim_rhs_resist function as defined in the OSDI +// spec +static void load_lim_rhs_resist(void *inst_, void *model, double *dst) { + DiodeInstace *inst = (DiodeInstace *)inst_; + + IGNORE(model); + dst[inst->node_off[A]] += inst->lim_rhs_resist_A; + dst[inst->node_off[CI]] += inst->lim_rhs_resist_CI; +} + +// implementation of the load_lim_rhs_react function as defined in the OSDI spec +static void load_lim_rhs_react(void *inst_, void *model, double *dst) { + DiodeInstace *inst = (DiodeInstace *)inst_; + + IGNORE(model); + dst[inst->node_off[A]] += inst->lim_rhs_react_A; + dst[inst->node_off[CI]] += inst->lim_rhs_react_CI; +} + +#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 +static void load_jacobian_resist(void *inst_, void *model) { + IGNORE(model); + DiodeInstace *inst = (DiodeInstace *)inst_; + LOAD_MATRIX_RESIST(A_A) + LOAD_MATRIX_RESIST(A_CI) + LOAD_MATRIX_RESIST(A_TNODE) + + LOAD_MATRIX_RESIST(CI_A) + LOAD_MATRIX_RESIST(CI_CI) + LOAD_MATRIX_RESIST(CI_C) + LOAD_MATRIX_RESIST(CI_TNODE) + + LOAD_MATRIX_RESIST(C_CI) + LOAD_MATRIX_RESIST(C_C) + LOAD_MATRIX_RESIST(C_TNODE) + + LOAD_MATRIX_RESIST(TNODE_TNODE) + LOAD_MATRIX_RESIST(TNODE_A) + LOAD_MATRIX_RESIST(TNODE_C) + LOAD_MATRIX_RESIST(TNODE_CI) +} + +#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 +static void load_jacobian_react(void *inst_, void *model, double alpha) { + IGNORE(model); + DiodeInstace *inst = (DiodeInstace *)inst_; + LOAD_MATRIX_REACT(A_A) + LOAD_MATRIX_REACT(A_CI) + LOAD_MATRIX_REACT(CI_A) + LOAD_MATRIX_REACT(CI_CI) + + LOAD_MATRIX_REACT(A_TNODE) + LOAD_MATRIX_REACT(CI_TNODE) +} + +#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 +static void load_jacobian_tran(void *inst_, void *model, double alpha) { + DiodeInstace *inst = (DiodeInstace *)inst_; + + // set dc stamps + load_jacobian_resist(inst_, model); + + // add reactive contributions + LOAD_MATRIX_TRAN(A_A) + LOAD_MATRIX_TRAN(A_CI) + LOAD_MATRIX_TRAN(CI_A) + LOAD_MATRIX_TRAN(CI_CI) + + LOAD_MATRIX_TRAN(A_TNODE) + LOAD_MATRIX_TRAN(CI_TNODE) +} + +// implementation of the load_spice_rhs_dc function as defined in the OSDI spec +static void load_spice_rhs_dc(void *inst_, void *model, double *dst, + double *prev_solve) { + IGNORE(model); + DiodeInstace *inst = (DiodeInstace *)inst_; + double va = prev_solve[inst->node_off[A]]; + double vci = prev_solve[inst->node_off[CI]]; + double vc = prev_solve[inst->node_off[C]]; + double vdtj = prev_solve[inst->node_off[TNODE]]; + + dst[inst->node_off[A]] += inst->jacobian_resist[A_A] * va + + inst->jacobian_resist[A_TNODE] * vdtj + + inst->jacobian_resist[A_CI] * vci + + inst->lim_rhs_resist_A - inst->residual_resist[A]; + + dst[inst->node_off[CI]] += inst->jacobian_resist[CI_A] * va + + inst->jacobian_resist[CI_TNODE] * vdtj + + inst->jacobian_resist[CI_CI] * vci + + inst->lim_rhs_resist_CI - + inst->residual_resist[CI]; + + dst[inst->node_off[C]] += + inst->jacobian_resist[C_C] * vc + inst->jacobian_resist[C_CI] * vci + + inst->jacobian_resist[C_TNODE] * vdtj - inst->residual_resist[C]; + + dst[inst->node_off[TNODE]] += inst->jacobian_resist[TNODE_A] * va + + inst->jacobian_resist[TNODE_C] * vc + + inst->jacobian_resist[TNODE_CI] * vci + + inst->jacobian_resist[TNODE_TNODE] * vdtj - + inst->residual_resist[TNODE]; +} + +// implementation of the load_spice_rhs_tran function as defined in the OSDI +// spec +static void load_spice_rhs_tran(void *inst_, void *model, double *dst, + double *prev_solve, double alpha) { + + DiodeInstace *inst = (DiodeInstace *)inst_; + double va = prev_solve[inst->node_off[A]]; + double vci = prev_solve[inst->node_off[CI]]; + double vdtj = prev_solve[inst->node_off[TNODE]]; + + // set DC rhs + load_spice_rhs_dc(inst_, model, dst, prev_solve); + + // add contributions due to reactive elements + dst[inst->node_off[A]] += + alpha * + (inst->jacobian_react[A_A] * va + inst->jacobian_react[A_CI] * vci + + inst->jacobian_react[A_TNODE] * vdtj + inst->lim_rhs_react_A); + + dst[inst->node_off[CI]] += + alpha * + (inst->jacobian_react[CI_CI] * vci + inst->jacobian_react[CI_A] * va + + inst->jacobian_react[CI_TNODE] * vdtj + inst->lim_rhs_react_CI); +} + +#define RESIST_RESIDUAL_OFF(NODE) \ + (offsetof(DiodeInstace, residual_resist) + sizeof(uint32_t) * NODE) + +// structure that provides information of all nodes of the model +const OsdiNode nodes[NUM_NODES] = { + { + .name = "A", + .units = "V", + .residual_units = "A", + .resist_residual_off = RESIST_RESIDUAL_OFF(A), + .react_residual_off = offsetof(DiodeInstace, residual_react_A), + }, + { + .name = "C", + .units = "V", + .residual_units = "A", + .resist_residual_off = RESIST_RESIDUAL_OFF(C), + .react_residual_off = UINT32_MAX, // no reactive residual + + }, + { + .name = "dT", + .units = "K", + .residual_units = "W", + .resist_residual_off = RESIST_RESIDUAL_OFF(TNODE), + .react_residual_off = UINT32_MAX, // no reactive residual + }, + { + .name = "CI", + .units = "V", + .residual_units = "A", + .resist_residual_off = RESIST_RESIDUAL_OFF(TNODE), + .react_residual_off = offsetof(DiodeInstace, residual_react_CI), + + }, +}; +#define JACOBI_ENTRY(N1, N2) \ + { \ + .nodes = {N1, N2}, .flags = JACOBIAN_ENTRY_RESIST | JACOBIAN_ENTRY_REACT, \ + .react_ptr_off = offsetof(DiodeInstace, jacobian_ptr_react) + \ + sizeof(double *) * N1##_##N2 \ + } + +#define RESIST_JACOBI_ENTRY(N1, N2) \ + { \ + .nodes = {N1, N2}, .flags = JACOBIAN_ENTRY_RESIST, \ + .react_ptr_off = UINT32_MAX \ + } + +// these node pairs specify which entries in the Jacobian must be accounted for +OsdiJacobianEntry jacobian_entries[NUM_MATRIX] = { + JACOBI_ENTRY(CI, CI), + RESIST_JACOBI_ENTRY(CI, C), + RESIST_JACOBI_ENTRY(C, CI), + RESIST_JACOBI_ENTRY(C, C), + JACOBI_ENTRY(A, A), + JACOBI_ENTRY(A, CI), + JACOBI_ENTRY(CI, A), + JACOBI_ENTRY(A, TNODE), + RESIST_JACOBI_ENTRY(C, TNODE), + JACOBI_ENTRY(CI, TNODE), + RESIST_JACOBI_ENTRY(TNODE, TNODE), + RESIST_JACOBI_ENTRY(TNODE, A), + RESIST_JACOBI_ENTRY(TNODE, C), + RESIST_JACOBI_ENTRY(TNODE, CI), +}; +OsdiNodePair collapsible[NUM_COLLAPSIBLE] = { + {CI, C}, + {TNODE, NUM_NODES}, +}; + +#define NUM_PARAMS 12 +// 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 *[]){"$mfactor"}, + .num_alias = 0, + .description = "Verilog-A multiplication factor for parallel devices", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_INST, + .len = 0, + }, + { + .name = (char *[]){"Rs"}, + .num_alias = 0, + .description = "Ohmic res", + .units = "Ohm", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"Is"}, + .num_alias = 0, + .description = "Saturation current", + .units = "A", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"zetars"}, + .num_alias = 0, + .description = "Temperature coefficient of ohmic res", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"N"}, + .num_alias = 0, + .description = "Emission coefficient", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"Cj0"}, + .num_alias = 0, + .description = "Junction capacitance", + .units = "F", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"Vj"}, + .num_alias = 0, + .description = "Junction potential", + .units = "V", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"M"}, + .num_alias = 0, + .description = "Grading coefficient", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"Rth"}, + .num_alias = 0, + .description = "Thermal resistance", + .units = "K/W", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"zetarth"}, + .num_alias = 0, + .description = "Temperature coefficient of thermal res", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"zetais"}, + .num_alias = 0, + .description = "Temperature coefficient of Is", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"Tnom"}, + .num_alias = 0, + .description = "Reference temperature", + .units = "", + .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 = "diode_va", + + // nodes + .num_nodes = NUM_NODES, + .num_terminals = 3, + .nodes = (OsdiNode *)&nodes, + .node_mapping_offset = offsetof(DiodeInstace, node_off), + + // matrix entries + .num_jacobian_entries = NUM_MATRIX, + .jacobian_entries = (OsdiJacobianEntry *)&jacobian_entries, + .jacobian_ptr_resist_offset = offsetof(DiodeInstace, jacobian_ptr_resist), + + // node collapsing + .num_collapsible = NUM_COLLAPSIBLE, + .collapsible = collapsible, + .collapsed_offset = offsetof(DiodeInstace, collapsed), + + // noise + .noise_sources = NULL, + .num_noise_src = 0, + + // parameters and op vars + .num_params = NUM_PARAMS, + .num_instance_params = 1, + .num_opvars = 0, + .param_opvar = (OsdiParamOpvar *)¶ms, + + // step size bound + .bound_step_offset = UINT32_MAX, + + .num_states = 1, + .state_idx_off = offsetof(DiodeInstace, state_idx), + + // memory + .instance_size = sizeof(DiodeInstace), + .model_size = sizeof(DiodeModel), + + // 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, + .load_limit_rhs_react = load_lim_rhs_react, + .load_limit_rhs_resist = load_lim_rhs_resist, +}}; + +OsdiLimFunction OSDI_LIM_TABLE[1] = {{.name = "pnjlim", .num_args = 2}}; + +uint32_t OSDI_LIM_TABLE_LEN = 1; diff --git a/test_cases/diode/netlist.sp b/test_cases/diode/netlist.sp new file mode 100644 index 000000000..ac80a1bc4 --- /dev/null +++ b/test_cases/diode/netlist.sp @@ -0,0 +1,46 @@ +OSDI Diode 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 +* Rt T 0 1e10 *not supported Pascal? + +* model definitions: +.model dmod_built_in d( bv=5.0000000000e+01 is=1e-13 n=1.05 thermal=1 tnom=27 rth0=100 rs=5 cj0=1e-15 vj=0.5 m=0.6 ) +.model dmod_osdi diode_va rs=5 is=1e-13 n=1.05 Rth=100 cj0=1e-15 vj=0.5 m=0.6 + +*OSDI Diode: +*OSDI_ACTIVATE*A1 D 0 T dmod_osdi + +*Built-in Diode: +*BUILT_IN_ACTIVATE*D1 D 0 T dmod_built_in + + +.control +pre_osdi diode.osdi + +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) v(t) + +* 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 diff --git a/test_cases/diode/test_diode.py b/test_cases/diode/test_diode.py new file mode 100644 index 000000000..efea23937 --- /dev/null +++ b/test_cases/diode/test_diode.py @@ -0,0 +1,162 @@ +""" test OSDI simulation of diode +""" +import os, shutil +import numpy as np +import pandas as pd +import sys +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) + +from testing import prepare_test + +# This test runs a DC, AC and Transient Simulation of a simple diode. +# The diode is available in the "OSDI" Git project and needs to be compiled to a shared object +# and then bet put into /usr/local/share/ngspice/osdi: +# +# > make osdi_diode +# > cp diode_osdi.osdi /usr/local/share/ngspice/osdi/diode_osdi.osdi +# +# The integration test proves the functioning of the OSDI interface. The Ngspice diode is quite +# complicated and the results are therefore not exactly the same. +# Future tests will target Verilog-A models like HICUM/L2 that should yield exactly the same results as the Ngspice implementation. + +directory = os.path.dirname(__file__) + + +def test_ngspice(): + dir_osdi, dir_built_in = prepare_test(directory) + + # 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_built_in, "dc_sim.ngspice"), sep="\\s+" + ) + + id_osdi = 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_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_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[20:40], id_built_in[20:40], rtol=0.03) + + 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() + ax2 = ax1.twinx() + ax1.semilogy( + dc_data_built_in["v(d)"], + dc_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + ax1.semilogy( + dc_data_osdi["v(d)"], + dc_data_osdi["i(vsense)"] * 1e3, + label="OSDI", + ) + ax2.plot( + dc_data_built_in["v(d)"], + dc_data_built_in["v(t)"], + label="built-in", + linestyle=" ", + marker="x", + ) + ax2.plot( + dc_data_osdi["v(d)"], + dc_data_osdi["v(t)"], + label="OSDI", + ) + ax1.set_ylabel(r"$I_{\mathrm{D}} (\mathrm{mA})$") + ax2.set_ylabel(r"$\Delta T_{\mathrm{j}}(\mathrm{K})$") + ax1.set_xlabel(r"$V_{\mathrm{D}}(\mathrm{V})$") + plt.legend() + + # AC Plot + omega = 2 * np.pi * ac_data_osdi["frequency"] + 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"] * 1e3 / omega, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.semilogx( + ac_data_osdi["frequency"], + ac_data_osdi["i(vsense).1"] * 1e3 / omega, + label="OSDI", + ) + plt.xlabel("$f(\\mathrm{H})$") + plt.ylabel("$\\Im\\left\{Y_{11}\\right\}/(\\omega) (\\mathrm{mF})$") + 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_osdi.zip b/test_cases/diode/test_osdi.zip new file mode 100644 index 000000000..5f5690adb Binary files /dev/null and b/test_cases/diode/test_osdi.zip differ diff --git a/test_cases/hicuml2/.empty.txt b/test_cases/hicuml2/.empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test_cases/inductor/.empty.txt b/test_cases/inductor/.empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test_cases/multiple_devices/.empty.txt b/test_cases/multiple_devices/.empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test_cases/multiple_devices/capacitor.c b/test_cases/multiple_devices/capacitor.c new file mode 100644 index 000000000..411b2eb7c --- /dev/null +++ b/test_cases/multiple_devices/capacitor.c @@ -0,0 +1,374 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + * + * 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..1bf802c85 --- /dev/null +++ b/test_cases/multiple_devices/netlist.sp @@ -0,0 +1,49 @@ +OSDI Multiple Devices 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 10M) +Vsense Dx D DC 0 + +* model definitions: +.model rmod_osdi resistor_va r=20 +.model cmod_osdi capacitor_va c=5 + +*OSDI Resistor and Capacitor: +*OSDI_ACTIVATE*A1 D 0 rmod_osdi +*OSDI_ACTIVATE*A2 D 0 rmod_osdi +*OSDI_ACTIVATE*A3 D 0 cmod_osdi + +*Built-in Capacitor and Resistor: +*BUILT_IN_ACTIVATE*R1 D 0 20 +*BUILT_IN_ACTIVATE*R2 D 0 20 +*BUILT_IN_ACTIVATE*C1 D 0 5 + + +.control +pre_osdi resistor.osdi capacitor.osdi + +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 diff --git a/test_cases/multiple_devices/resistor.c b/test_cases/multiple_devices/resistor.c new file mode 100644 index 000000000..2ef8e86da --- /dev/null +++ b/test_cases/multiple_devices/resistor.c @@ -0,0 +1,364 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + * + * 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..db4c85326 --- /dev/null +++ b/test_cases/multiple_devices/test_multiple.py @@ -0,0 +1,172 @@ +""" test OSDI simulation of resistor and capacitor +""" +import os, shutil +import numpy as np +import pandas as pd +import sys +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) + +from testing import prepare_test + +# 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.osdi /usr/local/share/ngspice/osdi/resistor_osdi.osdi +# > make osdi_capacitor +# > cp capacitor_osdi.osdi /usr/local/share/ngspice/osdi/capacitor_osdi.osdi +# +# 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. + + +directory = os.path.dirname(__file__) + +def test_ngspice(): + dir_osdi, dir_built_in = prepare_test(directory) + + # 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_built_in, "dc_sim.ngspice"), sep="\\s+" + ) + + id_osdi = 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_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_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"] / omega, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.semilogx( + ac_data_built_in["frequency"], + np.ones_like(ac_data_built_in["i(vsense).1"]) *5, + label="analytical", + linestyle="--", + marker="s", + ) + plt.semilogx( + ac_data_osdi["frequency"], + ac_data_osdi["i(vsense).1"] / omega, + label="OSDI", + ) + plt.ylim(4, 6) + plt.xlabel("$f(\\mathrm{H})$") + plt.ylabel("$\\Im\\left\{Y_{11}\\right\}/(\\omega) (\\mathrm{F})$") + 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/node_collapsing/.empty.txt b/test_cases/node_collapsing/.empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test_cases/node_collapsing/diode.c b/test_cases/node_collapsing/diode.c new file mode 100644 index 000000000..acfa84ace --- /dev/null +++ b/test_cases/node_collapsing/diode.c @@ -0,0 +1,831 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + * + * 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 4 +#define A 0 +#define C 1 +#define TNODE 2 +#define CI 3 + +#define NUM_COLLAPSIBLE 2 + +// number of matrix entries and definitions for Jacobian entries for nicer +// syntax in this file +#define NUM_MATRIX 14 +#define CI_CI 0 +#define CI_C 1 +#define C_CI 2 +#define C_C 3 +#define A_A 4 +#define A_CI 5 +#define CI_A 6 +#define A_TNODE 7 +#define C_TNODE 8 +#define CI_TNODE 9 +#define TNODE_TNODE 10 +#define TNODE_A 11 +#define TNODE_C 12 +#define TNODE_CI 13 + +// The model structure for the diode +typedef struct DiodeModel +{ + double Rs; + bool Rs_given; + double Is; + bool Is_given; + double zetars; + bool zetars_given; + double N; + bool N_given; + double Cj0; + bool Cj0_given; + double Vj; + bool Vj_given; + double M; + bool M_given; + double Rth; + bool Rth_given; + double zetarth; + bool zetarth_given; + double zetais; + bool zetais_given; + double Tnom; + bool Tnom_given; + double mfactor; // multiplication factor for parallel devices + bool mfactor_given; + // InitError errors[MAX_ERROR_NUM], +} DiodeModel; + +// The instace structure for the diode +typedef struct DiodeInstace +{ + double mfactor; // multiplication factor for parallel devices + bool mfactor_given; + double temperature; + double rhs_resist[NUM_NODES]; + double rhs_react[NUM_NODES]; + double jacobian_resist[NUM_MATRIX]; + double jacobian_react[NUM_MATRIX]; + bool is_collapsible[NUM_COLLAPSIBLE]; + double *jacobian_ptr_resist[NUM_MATRIX]; + double *jacobian_ptr_react[NUM_MATRIX]; + uint32_t node_off[NUM_NODES]; +} DiodeInstace; + +#define EXP_LIM 80.0 + +double limexp(double x) +{ + if (x < EXP_LIM) + { + return exp(x); + } + else + { + return exp(EXP_LIM) * (x + 1 - EXP_LIM); + } +} + +double dlimexp(double x) +{ + if (x < EXP_LIM) + { + return exp(x); + } + else + { + return exp(EXP_LIM); + } +} + +// implementation of the access function as defined by the OSDI spec +void *osdi_access(void *inst_, void *model_, uint32_t id, uint32_t flags) +{ + DiodeModel *model = (DiodeModel *)model_; + DiodeInstace *inst = (DiodeInstace *)inst_; + + bool *given; + void *value; + + switch (id) // id of params defined in param_opvar array + { + case 0: + if (flags & ACCESS_FLAG_INSTANCE) + { + value = (void *)&inst->mfactor; + given = &inst->mfactor_given; + } + else + { + value = (void *)&model->mfactor; + given = &model->mfactor_given; + } + break; + case 1: + value = (void *)&model->Rs; + given = &model->Rs_given; + break; + case 2: + value = (void *)&model->Is; + given = &model->Is_given; + break; + case 3: + value = (void *)&model->zetars; + given = &model->zetars_given; + break; + case 4: + value = (void *)&model->N; + given = &model->N_given; + break; + case 5: + value = (void *)&model->Cj0; + given = &model->Cj0_given; + break; + case 6: + value = (void *)&model->Vj; + given = &model->Vj_given; + break; + case 7: + value = (void *)&model->M; + given = &model->M_given; + break; + case 8: + value = &model->Rth; + given = &model->Rth_given; + break; + case 9: + value = (void *)&model->zetarth; + given = &model->zetarth_given; + break; + case 10: + value = (void *)&model->zetais; + given = &model->zetais_given; + break; + case 11: + value = (void *)&model->Tnom; + given = &model->Tnom_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_) +{ + DiodeModel *model = (DiodeModel *)model_; + + // set parameters and check bounds + if (!model->mfactor_given) + { + model->mfactor = 1.0; + } + if (!model->Rs_given) + { + model->Rs = 1e-9; + } + if (!model->Is_given) + { + model->Is = 1e-14; + } + if (!model->zetars_given) + { + model->zetars = 0; + } + if (!model->N_given) + { + model->N = 1; + } + if (!model->Cj0_given) + { + model->Cj0 = 0; + } + if (!model->Vj_given) + { + model->Vj = 1.0; + } + if (!model->M_given) + { + model->M = 0.5; + } + if (!model->Rth_given) + { + model->Rth = 0; + } + if (!model->zetarth_given) + { + model->zetarth = 0; + } + if (!model->zetais_given) + { + model->zetais = 0; + } + if (!model->Tnom_given) + { + model->Tnom = 300; + } + + 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) +{ + DiodeInstace *inst = (DiodeInstace *)inst_; + DiodeModel *model = (DiodeModel *)model_; + + // Here the logic for node collapsing ist implemented. The indices in this list must adhere to the "collapsible" List of node pairs. + if (model->Rs<1e-9){ // Rs between Ci C + inst->is_collapsible[0] = true; + } + if (model->Rth<1e-9){ // Rs between Ci C + inst->is_collapsible[1] = true; + } + + if (!inst->mfactor_given) + { + if (model->mfactor_given) + { + inst->mfactor = model->mfactor; + } + else + { + inst->mfactor = 1; + } + } + + 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) +{ + DiodeModel *model = (DiodeModel *)model_; + DiodeInstace *inst = (DiodeInstace *)inst_; + + // get voltages + double va = prev_solve[inst->node_off[A]]; + double vc = prev_solve[inst->node_off[C]]; + double vci = prev_solve[inst->node_off[CI]]; + double vdtj = prev_solve[inst->node_off[TNODE]]; + + double vcic = vci - vc; + double vaci = va - vci; + + 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]; + } + } + + //////////////////////////////// + // evaluate model equations + //////////////////////////////// + + // temperature update + double pk = 1.3806503e-23; + double pq = 1.602176462e-19; + double t_dev = inst->temperature + vdtj; + double tdev_tnom = t_dev / model->Tnom; + double rs_t = model->Rs * powf(tdev_tnom, model->zetars); + double rth_t = model->Rth * powf(tdev_tnom, model->zetarth); + double is_t = model->Is * powf(tdev_tnom, model->zetais); + double vt = t_dev * pk / pq; + + // derivatives w.r.t. temperature + double rs_dt = model->zetars * model->Rs * + powf(tdev_tnom, model->zetars - 1.0) / model->Tnom; + double rth_dt = model->zetarth * model->Rth * + powf(tdev_tnom, model->zetarth - 1.0) / model->Tnom; + double is_dt = model->zetais * model->Is * + powf(tdev_tnom, model->zetais - 1.0) / model->Tnom; + double vt_tj = pk / pq; + + // evaluate model equations and calculate all derivatives + // diode current + double id = is_t * (limexp(vaci / (model->N * vt)) - 1.0); + double gd = is_t / vt * dlimexp(vaci / (model->N * vt)); + double gdt = -is_t * dlimexp(vaci / (model->N * vt)) * vaci / model->N / vt / + vt * vt_tj + + 1.0 * exp((vaci / (model->N * vt)) - 1.0) * is_dt; + + // resistor + double irs = 0; + double g = 0; + double grt = 0; + if (!inst->is_collapsible[0]) { + irs = vcic / rs_t; + g = 1.0 / rs_t; + grt = -irs / rs_t * rs_dt; + } + + + // thermal resistance + double irth = 0; + double gt = 0; + if (!inst->is_collapsible[1]) { + irth = vdtj / rth_t; + gt = 1.0 / rth_t - irth / rth_t * rth_dt; + } + + // charge + double vf = model->Vj * (1.0 - powf(3.04, -1.0 / model->M)); + double x = (vf - vaci) / vt; + double x_vt = -x / vt; + double x_dtj = x_vt * vt_tj; + double x_vaci = -1.0 / vt; + double y = sqrt(x * x + 1.92); + double y_x = 0.5 / y * 2.0 * x; + double y_vaci = y_x * x_vaci; + double y_dtj = y_x * x_dtj; + double vd = vf - vt * (x + y) / (2.0); + double vd_x = -vt / 2.0; + double vd_y = -vt / 2.0; + double vd_vt = -(x + y) / (2.0); + double vd_dtj = vd_x * x_dtj + vd_y * y_dtj + vd_vt * vt_tj; + double vd_vaci = vd_x * x_vaci + vd_y * y_vaci; + double qd = model->Cj0 * vaci * model->Vj * + (1.0 - powf(1.0 - vd / model->Vj, 1.0 - model->M)) / + (1.0 - model->M); + double qd_vd = model->Cj0 * model->Vj / (1.0 - model->M) * (1.0 - model->M) * + powf(1.0 - vd / model->Vj, 1.0 - model->M - 1.0) / model->Vj; + double qd_dtj = qd_vd * vd_dtj; + double qd_vaci = qd_vd * vd_vaci; + + // thermal power source = current source + double ith = id * vaci ; + double ith_vtj = gdt * vaci ; + double ith_vcic = 0; + double ith_vaci = gd * vaci + id; + if (!inst->is_collapsible[0]) { + ith_vcic = 2.0 * vcic / rs_t; + ith += powf(vcic, 2.0) / rs_t; + ith_vtj -= - powf(vcic, 2.0) / rs_t / rs_t * rs_dt; + } + + id += gmin * vaci; + gd += gmin; + + double mfactor = inst->mfactor; + + //////////////// + // write rhs + //////////////// + + if (flags & CALC_RESIST_RESIDUAL) + { + // write resist rhs + inst->rhs_resist[A] = id * mfactor; + inst->rhs_resist[CI] = -id * mfactor + irs * mfactor; + inst->rhs_resist[C] = -irs * mfactor; + inst->rhs_resist[TNODE] = -ith * mfactor + irth * mfactor; + } + + if (flags & CALC_REACT_RESIDUAL) + { + // write react rhs + inst->rhs_react[A] = qd * mfactor; + inst->rhs_react[CI] = -qd * mfactor; + } + + ////////////////// + // write Jacobian + ////////////////// + + if (flags & CALC_RESIST_JACOBIAN) + { + // stamp diode (current flowing from Ci into A) + inst->jacobian_resist[A_A] = gd * mfactor; + inst->jacobian_resist[A_CI] = -gd * mfactor; + inst->jacobian_resist[CI_A] = -gd * mfactor; + inst->jacobian_resist[CI_CI] = gd * mfactor; + // diode thermal + inst->jacobian_resist[A_TNODE] = gdt * mfactor; + inst->jacobian_resist[CI_TNODE] = -gdt * mfactor; + + // stamp resistor (current flowing from C into CI) + inst->jacobian_resist[CI_CI] += g * mfactor; + inst->jacobian_resist[CI_C] = -g * mfactor; + inst->jacobian_resist[C_CI] = -g * mfactor; + inst->jacobian_resist[C_C] = g * mfactor; + // resistor thermal + inst->jacobian_resist[CI_TNODE] = grt * mfactor; + inst->jacobian_resist[C_TNODE] = -grt * mfactor; + + // stamp rth flowing into node dTj + inst->jacobian_resist[TNODE_TNODE] = gt * mfactor; + + // stamp ith flowing out of T node + inst->jacobian_resist[TNODE_TNODE] -= ith_vtj * mfactor; + inst->jacobian_resist[TNODE_CI] = (ith_vcic - ith_vaci) * mfactor; + inst->jacobian_resist[TNODE_C] = -ith_vcic * mfactor; + inst->jacobian_resist[TNODE_A] = ith_vaci * mfactor; + } + + if (flags & CALC_REACT_JACOBIAN) + { + // write react matrix + // stamp Qd between nodes A and Ci depending also on dT + inst->jacobian_react[A_A] = qd_vaci * mfactor; + inst->jacobian_react[A_CI] = -qd_vaci * mfactor; + inst->jacobian_react[CI_A] = -qd_vaci * mfactor; + inst->jacobian_react[CI_CI] = qd_vaci * mfactor; + + inst->jacobian_react[A_TNODE] = qd_dtj * mfactor; + inst->jacobian_react[CI_TNODE] = -qd_dtj * mfactor; + } + + 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) +{ + DiodeInstace *inst = (DiodeInstace *)inst_; + + LOAD_RHS_RESIST(A) + LOAD_RHS_RESIST(CI) + LOAD_RHS_RESIST(C) + LOAD_RHS_RESIST(TNODE) +} + +#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) +{ + DiodeInstace *inst = (DiodeInstace *)inst_; + + LOAD_RHS_REACT(A) + LOAD_RHS_REACT(CI) +} + +#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_) +{ + DiodeInstace *inst = (DiodeInstace *)inst_; + LOAD_MATRIX_RESIST(A_A) + LOAD_MATRIX_RESIST(A_CI) + LOAD_MATRIX_RESIST(A_TNODE) + + LOAD_MATRIX_RESIST(CI_A) + LOAD_MATRIX_RESIST(CI_CI) + LOAD_MATRIX_RESIST(CI_C) + LOAD_MATRIX_RESIST(CI_TNODE) + + LOAD_MATRIX_RESIST(C_CI) + LOAD_MATRIX_RESIST(C_C) + LOAD_MATRIX_RESIST(C_TNODE) + + LOAD_MATRIX_RESIST(TNODE_TNODE) + LOAD_MATRIX_RESIST(TNODE_A) + LOAD_MATRIX_RESIST(TNODE_C) + LOAD_MATRIX_RESIST(TNODE_CI) +} + +#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) +{ + DiodeInstace *inst = (DiodeInstace *)inst_; + LOAD_MATRIX_REACT(A_A) + LOAD_MATRIX_REACT(A_CI) + LOAD_MATRIX_REACT(CI_A) + LOAD_MATRIX_REACT(CI_CI) + + LOAD_MATRIX_REACT(A_TNODE) + LOAD_MATRIX_REACT(CI_TNODE) +} + +#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) +{ + DiodeInstace *inst = (DiodeInstace *)inst_; + + // set dc stamps + load_jacobian_resist(inst_); + + // add reactive contributions + LOAD_MATRIX_TRAN(A_A) + LOAD_MATRIX_TRAN(A_CI) + LOAD_MATRIX_TRAN(CI_A) + LOAD_MATRIX_TRAN(CI_CI) + + LOAD_MATRIX_TRAN(A_TNODE) + LOAD_MATRIX_TRAN(CI_TNODE) +} + +// 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) +{ + DiodeInstace *inst = (DiodeInstace *)inst_; + double va = prev_solve[inst->node_off[A]]; + double vci = prev_solve[inst->node_off[CI]]; + double vc = prev_solve[inst->node_off[C]]; + double vdtj = prev_solve[inst->node_off[TNODE]]; + + dst[inst->node_off[A]] += inst->jacobian_resist[A_A] * va + + inst->jacobian_resist[A_TNODE] * vdtj + + inst->jacobian_resist[A_CI] * vci - + inst->rhs_resist[A]; + + dst[inst->node_off[CI]] += inst->jacobian_resist[CI_A] * va + + inst->jacobian_resist[CI_TNODE] * vdtj + + inst->jacobian_resist[CI_CI] * vci - + inst->rhs_resist[CI]; + + dst[inst->node_off[C]] += inst->jacobian_resist[C_C] * vc + + inst->jacobian_resist[C_CI] * vci + + inst->jacobian_resist[C_TNODE] * vdtj - + inst->rhs_resist[C]; + + dst[inst->node_off[TNODE]] += inst->jacobian_resist[TNODE_A] * va + + inst->jacobian_resist[TNODE_C] * vc + + inst->jacobian_resist[TNODE_CI] * vci + + inst->jacobian_resist[TNODE_TNODE] * vdtj - + inst->rhs_resist[TNODE]; +} + +// 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) +{ + + DiodeInstace *inst = (DiodeInstace *)inst_; + double va = prev_solve[inst->node_off[A]]; + double vci = prev_solve[inst->node_off[CI]]; + double vdtj = prev_solve[inst->node_off[TNODE]]; + + // set DC rhs + load_spice_rhs_dc(inst_, dst, prev_solve); + + // add contributions due to reactive elements + dst[inst->node_off[A]] += + alpha * (inst->jacobian_react[A_A] * va + + inst->jacobian_react[A_CI] * vci + + inst->jacobian_react[A_TNODE] * vdtj); + + dst[inst->node_off[CI]] += alpha * (inst->jacobian_react[CI_CI] * vci + + inst->jacobian_react[CI_A] * va + + inst->jacobian_react[CI_TNODE] * vdtj); +} + +// structure that provides information of all nodes of the model +OsdiNode nodes[NUM_NODES] = { + {.name = "A", .units = "V", .is_reactive = true}, + {.name = "C", .units = "V"}, + {.name = "dT", .units = "K"}, + {.name = "CI", .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] = { + {CI, CI}, + {CI, C}, + {C, CI}, + {C, C}, + {A, A}, + {A, CI}, + {CI, A}, + {A, TNODE}, + {C, TNODE}, + {CI, TNODE}, + {TNODE, TNODE}, + {TNODE, A}, + {TNODE, C}, + {TNODE, CI}, +}; +OsdiNodePair collapsible[NUM_COLLAPSIBLE] = { + {CI, C}, + {TNODE, NUM_NODES}, +}; + +#define NUM_PARAMS 12 +// 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 *[]){"$mfactor"}, + .num_alias = 0, + .description = "Verilog-A multiplication factor for parallel devices", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_INST, + .len = 0, + }, + { + .name = (char *[]){"Rs"}, + .num_alias = 0, + .description = "Ohmic res", + .units = "Ohm", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"Is"}, + .num_alias = 0, + .description = "Saturation current", + .units = "A", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"zetars"}, + .num_alias = 0, + .description = "Temperature coefficient of ohmic res", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"N"}, + .num_alias = 0, + .description = "Emission coefficient", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"Cj0"}, + .num_alias = 0, + .description = "Junction capacitance", + .units = "F", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"Vj"}, + .num_alias = 0, + .description = "Junction potential", + .units = "V", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"M"}, + .num_alias = 0, + .description = "Grading coefficient", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"Rth"}, + .num_alias = 0, + .description = "Thermal resistance", + .units = "K/W", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"zetarth"}, + .num_alias = 0, + .description = "Temperature coefficient of thermal res", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"zetais"}, + .num_alias = 0, + .description = "Temperature coefficient of Is", + .units = "", + .flags = PARA_TY_REAL | PARA_KIND_MODEL, + .len = 0, + }, + { + .name = (char *[]){"Tnom"}, + .num_alias = 0, + .description = "Reference temperature", + .units = "", + .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 = "diode_va", + + // nodes + .num_nodes = NUM_NODES, + .num_terminals = 3, + .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(DiodeInstace), + .model_size = sizeof(DiodeModel), + .residual_resist_offset = offsetof(DiodeInstace, rhs_resist), + .residual_react_offset = offsetof(DiodeInstace, rhs_react), + .node_mapping_offset = offsetof(DiodeInstace, node_off), + .jacobian_resist_offset = offsetof(DiodeInstace, jacobian_resist), + .jacobian_react_offset = offsetof(DiodeInstace, jacobian_react), + .jacobian_ptr_resist_offset = offsetof(DiodeInstace, jacobian_ptr_resist), + .jacobian_ptr_react_offset = offsetof(DiodeInstace, jacobian_ptr_react), + + // node collapsing + .num_collapsible = NUM_COLLAPSIBLE, + .collapsible = collapsible, + .is_collapsible_offset = offsetof(DiodeInstace, is_collapsible), + + // noise + .noise_sources = NULL, + .num_noise_src = 0, + + // parameters and op vars + .num_params = NUM_PARAMS, + .num_instance_params = 1, + .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/node_collapsing/netlist.sp b/test_cases/node_collapsing/netlist.sp new file mode 100644 index 000000000..acd8bc3ad --- /dev/null +++ b/test_cases/node_collapsing/netlist.sp @@ -0,0 +1,46 @@ +OSDI Diode 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 +* Rt T 0 1e10 *not supported Pascal? + +* model definitions: +.model dmod_built_in d( bv=5.0000000000e+01 is=1e-13 n=1.05 thermal=1 tnom=27 rth0=1 rs=0 cj0=1e-15 vj=0.5 m=0.6 ) +.model dmod_osdi diode_va rs=0 is=1e-13 n=1.05 Rth=0 cj0=1e-15 vj=0.5 m=0.6 + +*OSDI Diode: +*OSDI_ACTIVATE*A1 D 0 dmod_osdi + +*Built-in Diode: +*BUILT_IN_ACTIVATE*D1 D 0 T dmod_built_in + + +.control +pre_osdi diode.osdi + +set filetype=ascii +set wr_vecnames +set wr_singlescale + +* a DC sweep from 0.3V to 1V +dc Vd 0.3 0.6 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 diff --git a/test_cases/node_collapsing/test_diode.py b/test_cases/node_collapsing/test_diode.py new file mode 100644 index 000000000..486b0e51e --- /dev/null +++ b/test_cases/node_collapsing/test_diode.py @@ -0,0 +1,148 @@ +""" test OSDI simulation of diode +""" +import os, shutil +import numpy as np +import pandas as pd +import sys +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) + +from testing import prepare_test + +# This test runs a DC, AC and Transient Simulation of a simple diode. +# The diode is available in the "OSDI" Git project and needs to be compiled to a shared object +# and then bet put into /usr/local/share/ngspice/osdi: +# +# > make osdi_diode +# > cp diode_osdi.osdi /usr/local/share/ngspice/osdi/diode_osdi.osdi +# +# The integration test proves the functioning of the OSDI interface. The Ngspice diode is quite +# complicated and the results are therefore not exactly the same. +# Future tests will target Verilog-A models like HICUM/L2 that should yield exactly the same results as the Ngspice implementation. + +directory = os.path.dirname(__file__) + + +def test_ngspice(): + dir_osdi, dir_built_in = prepare_test(directory) + + # 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_built_in, "dc_sim.ngspice"), sep="\\s+" + ) + + id_osdi = 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_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_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) #ngspice diode doesnt work with node collapsing :D + + 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.semilogy( + dc_data_built_in["v(d)"], + dc_data_built_in["i(vsense)"] * 1e3, + label="built-in", + linestyle=" ", + marker="x", + ) + ax1.semilogy( + dc_data_osdi["v(d)"], + dc_data_osdi["i(vsense)"] * 1e3, + label="OSDI", + ) + ax1.set_ylabel(r"$I_{\mathrm{D}} (\mathrm{mA})$") + ax1.set_xlabel(r"$V_{\mathrm{D}}(\mathrm{V})$") + plt.legend() + + # AC Plot + omega = 2 * np.pi * ac_data_osdi["frequency"] + 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"] * 1e3 / omega, + label="built-in", + linestyle=" ", + marker="x", + ) + plt.semilogx( + ac_data_osdi["frequency"], + ac_data_osdi["i(vsense).1"] * 1e3 / omega, + label="OSDI", + ) + plt.xlabel("$f(\\mathrm{H})$") + plt.ylabel("$\\Im\\left\{Y_{11}\\right\}/(\\omega) (\\mathrm{mF})$") + 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/resistor/.empty.txt b/test_cases/resistor/.empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test_cases/resistor/netlist.sp b/test_cases/resistor/netlist.sp new file mode 100644 index 000000000..5325b0345 --- /dev/null +++ b/test_cases/resistor/netlist.sp @@ -0,0 +1,44 @@ +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 resistor_va r=10 + +*OSDI Resistor: +*OSDI_ACTIVATE*A1 D 0 rmod_osdi + +*Built-in Resistor: +*BUILT_IN_ACTIVATE*R1 D 0 10 + + +.control +pre_osdi resistor.osdi + +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.c b/test_cases/resistor/resistor.c new file mode 100644 index 000000000..5a100b1ab --- /dev/null +++ b/test_cases/resistor/resistor.c @@ -0,0 +1,364 @@ +/* + * This file is part of the OSDI component of NGSPICE. + * Copyright© 2022 SemiMod GmbH. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Author: Pascal Kuthe + * + * 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 2 +#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..06810b7b2 --- /dev/null +++ b/test_cases/resistor/test_resistor.py @@ -0,0 +1,180 @@ +""" test OSDI simulation of resistor +""" +import os, shutil +import numpy as np +import subprocess +import pandas as pd + +import sys +sys.path.append( + os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) + +from testing import prepare_test + + +# 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. + +directory = os.path.dirname(__file__) + + +def test_ngspice(): + dir_osdi, dir_built_in = prepare_test(directory) + + # 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_built_in, "dc_sim.ngspice"), sep="\\s+" + ) + + id_osdi = 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_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_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/testing.py b/test_cases/testing.py new file mode 100644 index 000000000..76826f5a6 --- /dev/null +++ b/test_cases/testing.py @@ -0,0 +1,586 @@ +#this file defines some common routines used by the OSDI test cases +import os +import shutil +import glob +from pathlib import Path +from typing import Optional, List, Dict, Tuple +import regex as re +from subprocess import run, PIPE +import pandas as pd +import numpy as np +from math import atan2 +import sys + +# specify location of Ngspice executable to be tested +directory_testing = os.path.dirname(__file__) +ngspice_path = os.path.join(directory_testing, "../release/src/ngspice") +ngspice_path = os.path.abspath(ngspice_path) + +rtol = 0.032 +atol_dc = 1e-14 +atol_ac = 4e-19 + +twoPi = 8.0*atan2(1.0,1.0) + +def create_shared_objects(directory): + c_files = [] + for c_file in glob.glob(directory + "/*.c"): + basename = Path(c_file).stem + c_files.append(basename) + + for c_file in c_files: + run( + [ + "gcc", + "-c", + "-Wall", + "-I", + "../../src/osdi/", + "-fpic", + c_file + ".c", + "-ggdb", + ], + cwd=directory, + ) + run( + ["gcc", "-shared", "-o", c_file + ".osdi", c_file + ".o", "-ggdb"], + cwd=directory, + ) + run( + ["mv", c_file + ".osdi", "test_osdi/" + c_file + ".osdi"], cwd=directory + ) + run(["rm", c_file + ".o"], cwd=directory) + + # for va_file in glob.glob(directory + "/*.va"): + # result = run( + # [ + # "openvaf","-b", va_file + # ], + # # capture_output=True, + # cwd=directory, + # ) + + # run( + # ["cp", result.stdout[:-1], "test_osdi/" + Path(va_file).stem + ".osdi"], cwd=directory + # ) + + + + + +def prepare_dirs(directory): + # directories for test cases + dir_osdi = os.path.join(directory, "test_osdi") + dir_built_in = os.path.join(directory, "test_built_in") + + for directory_i in [dir_osdi, dir_built_in]: + # remove old results + shutil.rmtree(directory_i, ignore_errors=True) + # make new directories + os.makedirs(directory_i, exist_ok=True) + + + return dir_osdi, dir_built_in + +def prepare_netlists(directory): + path_netlist = os.path.join(directory, "netlist.sp") + + # directories for test cases + dir_osdi = os.path.join(directory, "test_osdi") + dir_built_in = os.path.join(directory, "test_built_in") + + # 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*", "") + + # 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) + +def run_simulations(dirs): + for dir_i in dirs: + run( + [ + ngspice_path, + "netlist.sp", + "-b", + ], + cwd=dir_i, + ) + +def prepare_test(directory): + dir_osdi, dir_built_in = prepare_dirs(directory) + create_shared_objects(directory) + prepare_netlists(directory) + run_simulations([dir_osdi, dir_built_in]) + + return dir_osdi, dir_built_in + + + +def parse_list(line): + return (val for val in re.split(r"\s+", line) if val != '') + +def parse_temps(line): + return [temp for temp in parse_list(line)] + + +class TestInfo: + biases: Optional[Dict[str, str]] = None + bias_list: Optional[Tuple[str, List[str]]] = None + bias_sweep = None + temps: Optional[List[str]] = None + freqs: Optional[str] = None + dc_outputs: Optional[List[Tuple[str, str]]] = None + ac_outputs: Optional[Dict[str,List[Tuple[str, str, bool, str, str]]]] = None + instanceParameters: str= "" + modelParameters: str = "" + line: str = "" + + def __init__(self, name, lines, parent): + self.name = name + self.lines= lines + self.parse() + if self.temps is None: + self.temps = parent.temps + self.pins = parent.pins + self.floating = parent.floating + + + + def parse_temps(self): + temps = parse_temps(self.line) + if self.temps is None: + self.temps = temps + else: + self.temps += temps + + def parse_model_params(self): + for param in parse_list(self.line): + path = Path(param) + if path.exists(): + self.modelParameters = path.read_text() + else: + self.modelParameters += f"+ {param}\n" + + def parse_instance_params(self): + for param in parse_list(self.line): + self.instanceParameters += f" {param}" + + + def parse_bias_list(self): + if self.bias_list: + raise ValueError(f"ERROR second bias_list spec {self.line}") + res = re.match(r"V\s*\(\s*(\w+)\s*\)\s*=", self.line) + pin = res[1] + vals = self.line[res.end():].strip() + vals = [val for val in re.split(r"\s*,\s*", vals)] + self.bias_list = (pin, vals) + + + def parse_biases(self): + if self.biases: + raise ValueError(f"ERROR second biases spec {self.line}") + self.biases = {} + for bias in parse_list(self.line): + res = re.match(r"V\s*\(\s*(\w+)\s*\)\s*=", bias) + pin = res[1] + val = bias[res.end():].strip() + self.biases[pin] = val + + def parse_outputs(self): + for output in parse_list(self.line): + res = re.match(r"([IV])\s*\(\s*(\w+)\s*\)", output) + if res: + pin = res[2] + if res[1] == "I": + output = f"i(v{pin})", f"I({pin})" + else: + output = f"v({pin})", f"V({pin})" + if self.dc_outputs: + self.dc_outputs.append(output) + else: + self.dc_outputs = [output] + continue + + + res = re.match(r"([CG])\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)", output) + if res: + kind = res[1] + pin1 = res[2] + pin2 = res[3] + + if kind == "G": + output = f"real(i(v{pin1}))", f"g({pin1},{pin2})", False, pin1, pin2 + elif kind == "C": + output = f"imag(i(v{pin1}))", f"c({pin1},{pin2})", True, pin1, pin2 + + if self.ac_outputs: + if pin2 in self.ac_outputs: + self.ac_outputs[pin2].append(output) + else: + self.ac_outputs[pin2] = [output] + else: + self.ac_outputs = {pin2: [output]} + continue + + def parse_frequency(self): + res = re.match(r"(lin|oct|dec)\s+(\S+)\s+(\S+)\s+(\S+)\s*", self.line) + kind = res[1] + num_steps = int(res[2]) + start = res[3] + end = res[4] + if start != end: + + if kind == "lin": + num_points = num_steps + 1 + else: + num_points = num_steps + else: + assert num_steps == 1 + num_points = 1 + self.freqs = f"{kind} {num_points} {start} {end}" + + + def parse_bias_sweep(self): + res = re.match(r"V\s*\(\s*(\w+)\s*\)\s*=", self.line) + pin = res[1] + args = self.line[res.end():] + args = [float(arg) for arg in re.split(r"\s*,\s*", args)] + if len(args) != 3: + raise ValueError(f"bias sweep must have 3 arguments found {args} in {self.line}") + self.bias_sweep = (pin, args) + + + def try_parse(self, prefix: str, f): + if self.line.startswith(prefix): + self.line = self.line[len(prefix):].strip() + f() + + def parse_line(self): + if self.try_parse("temperature", self.parse_temps): + return + if self.try_parse("modelParameters", self.parse_model_params): + return + if self.try_parse("instanceParameters", self.parse_instance_params): + return + if self.try_parse("biasList", self.parse_bias_list): + return + if self.try_parse("listBias", self.parse_bias_list): + return + if self.try_parse("biases", self.parse_biases): + return + if self.try_parse("output", self.parse_outputs): + return + if self.try_parse("outputs", self.parse_outputs): + return + if self.try_parse("biasSweep", self.parse_bias_sweep): + return + if self.try_parse("freq", self.parse_frequency): + return + if self.try_parse("frequency", self.parse_frequency): + return + + def parse(self): + for line in self.lines: + self.line = line + self.parse_line() + + def gen_netlist(self, osdi_file, va_module, type_arg): + if self.bias_list: + bias_start = f"foreach bias {' '.join(self.bias_list[1])}\nalter v{self.bias_list[0]}=$bias" + bias_end = "end" + else: + bias_start = bias_end = "" + + if self.dc_outputs: + if not self.bias_sweep: + raise ValueError("dc bias sweep msising") + outputs = " ".join(output for output, _ in self.dc_outputs) + sweep = f"dc v{self.bias_sweep[0]} {self.bias_sweep[1][0]} {self.bias_sweep[1][1]} {self.bias_sweep[1][2]}\n wrdata {self.dc_results_path()} {outputs}" + elif self.ac_outputs: + freqs = self.freqs + if not self.freqs: + freqs = f"lin 1 {1/twoPi} {1/twoPi}" + if self.bias_sweep: + if self.bias_list: + bias_start += "\n" + bias_end += "\n" + vals = np.arange(self.bias_sweep[1][0], self.bias_sweep[1][1] + self.bias_sweep[1][2]*0.1, self.bias_sweep[1][2]) + vals = [str(val) for val in vals] + bias_start += f"foreach bias {' '.join(vals)}\nalter v{self.bias_sweep[0]}=$bias" + bias_end += "end" + + sweep = "" + for pin, outputs in self.ac_outputs.items(): + sweep += f"alter v{pin} ac = 1\nac {freqs}\n" + outputs = " ".join(output[0] for output in outputs) + sweep += f"wrdata {self.ac_results_path(pin)} {outputs}\n" + sweep += f"alter v{pin} ac = 0\n" + else: + return "" + + biases = self.biases + if not biases: + biases = dict() + + source = "\n".join(f"v{pin} {pin} {0} dc={biases.get(pin, 0)}" for pin in self.pins if not pin in self.floating) + source += "".join(f"\nr{i} {pin} {0} r=1G" for i,pin in enumerate(self.floating)) + + return f"""CMC testsuite {self.name} +.options abstol=1e-15 + +{source} + +.model test_model {va_module} +{self.modelParameters} {type_arg} + +A1 {' '.join(self.pins)} test_model {self.instanceParameters} + +.control +pre_osdi {osdi_file} + +set filetype=ascii +set wr_vecnames +set wr_singlescale +set appendwrite + +foreach tamb {' '.join(self.temps)} + set temp=$tamb + {bias_start} + {sweep} + {bias_end} +end +quit 0 +.endc +.end +""" + + def dc_results_path(self, old=False) -> Path: + dir = "results" + if old: + dir = "results_old" + return Path(dir)/f"{self.name}.ngspice" + + def ac_results_path(self, pin: str, old=False) -> Path: + dir = "results" + if old: + dir = "results_old" + return Path(dir)/f"{self.name}_{pin}.ngspice" + + def run(self, osdi_file, va_module, type_arg, old_sim_ref=False, capture=True, check=True): + if not (self.dc_outputs or self.ac_outputs): + return + + print(f"running {self.name}...") + + netlist_path = Path("netlists")/f"{self.name}.sp" + netlist = self.gen_netlist(osdi_file, va_module, type_arg) + Path(netlist_path).write_text(netlist) + + res = run([ngspice_path, netlist_path, "-b"], capture_output=capture) + res.check_returncode() + # res.check_returncode() + + reference_path = Path("reference")/f"{self.name}.standard" + references = pd.read_csv(reference_path, sep="\\s+") + + if not check: + return + + if self.dc_outputs: + results_path = self.dc_results_path() + + if not results_path.exists(): + print(f"ERROR check failed for {self.name}\nsimulation file is missing - likely convergence issues!") + return + + results = pd.read_csv(results_path, sep="\\s+") + results = results.apply(pd.to_numeric, errors='coerce') + firstcol = results.iloc[:,1].to_numpy() + results = results[np.bitwise_not(np.isnan(firstcol))] + + if old_sim_ref: + ref_path = self.dc_results_path(old=True) + references = pd.read_csv(ref_path, sep="\\s+") + references = references.apply(pd.to_numeric, errors='coerce') + firstcol = references.iloc[:,1].to_numpy() + references = references[np.bitwise_not(np.isnan(firstcol))] + + for result_col, ref_col in self.dc_outputs: + reference = references[ref_col].to_numpy() + result = results[result_col].to_numpy() + if "I(" in ref_col: + result = -result + + adiff = np.abs(result-reference) + rdiff = adiff/np.abs(reference) + err = np.bitwise_not(np.bitwise_or(rdiff < rtol, adiff < atol_dc)) + if not np.any(err): + continue + maxatol = np.max(adiff[err]) + maxrtol = np.max(rdiff[err]) + print(f"ERROR check failed for {ref_col}\nrtol={maxrtol} atol={maxatol}\nresult:\n{result[err]}\nreference:\n{reference[err]}\nrtol:\n{rdiff[err]}") + + elif self.ac_outputs: + for pin, outputs in self.ac_outputs.items(): + results_path = self.ac_results_path(pin) + if not results_path.exists(): + print(f"ERROR check failed for {self.name} (ac {pin})\nsimulation file is missing - likely convergence issues!") + continue + + results = pd.read_csv(results_path, sep="\\s+") + results = results.apply(pd.to_numeric, errors='coerce') + firstcol = results.iloc[:,1].to_numpy() + results = results[np.bitwise_not(np.isnan(firstcol))] + + if old_sim_ref: + ref_path = self.ac_results_path(pin, old=True) + references = pd.read_csv(ref_path, sep="\\s+") + references = references.apply(pd.to_numeric, errors='coerce') + firstcol = references.iloc[:,1].to_numpy() + references = references[np.bitwise_not(np.isnan(firstcol))] + + for result_col, ref_col, is_cap, pin1, pin2 in outputs: + result = results[result_col].to_numpy() + if old_sim_ref: + reference = references[result_col].to_numpy() + # print(ref_col) + # print(references) + # print(results) + else: + reference = references[ref_col].to_numpy() + if not old_sim_ref: + if is_cap: + if"Freq" in references: + result = result /(twoPi*results["frequency"]) + if pin1 == pin2: + result = -result + else: + result = -result + + adiff = np.abs(result-reference) + rdiff = adiff/np.abs(reference) + err = np.bitwise_not(np.bitwise_or(rdiff < rtol, adiff < atol_ac)) + if not np.any(err): + continue + maxatol = np.max(adiff[err]) + maxrtol = np.max(rdiff[err]) + print(f"ERROR check failed for {ref_col}\nrtol={maxrtol} atol={maxatol}\nresult:\n{result[err]}\nreference:\n{reference[err]}\nrtol:\n{rdiff[err]}") + + + + + + + + +def removeComments(string): + string = re.sub(re.compile(r"/\*.*?\*/",re.DOTALL ) ,"" ,string) # remove all occurrences streamed comments (/*COMMENT */) from string + string = re.sub(re.compile(r"//.*?\n" ) ,"" ,string) # remove all occurrence single-line comments (//COMMENT\n ) from string + return string + +class QaSpec: + temps: List[str] + pins: List[str] + floating: List[str] + tests: List[TestInfo] + dir: Path + + def __init__(self, dir: Path): + self.dir = dir + self.temps = [] + self.pins = [] + self.tests = [] + self.floating = [] + self.parse() + + def parse(self): + old_dir = os.getcwd() + os.chdir(self.dir) + qa_spec = Path("qaSpec").read_text() + qa_spec = removeComments(qa_spec) + lines = [line.strip() for line in qa_spec.split('\n')] + + i = 0 + while i < len(lines): + line = lines[i] + i+= 1 + if line.startswith("temperature"): + line = line[len("temperature"):] + self.temps = parse_temps(line) + elif line.startswith("pins"): + line = line[len("pins"):] + self.pins = [pin for pin in re.findall(r"\w+", line) if pin != "pins"] + + elif line.startswith("float") or line.startswith("floating"): + self.floating = [pin for pin in re.findall(r"\w+", line) if pin != "floating" and pin != "float"] + elif line.startswith("test"): + test_name = line[4:].strip() + start = i + while i < len(lines) and lines[i] != "": + i += 1 + end = i + + test = TestInfo(test_name, lines[start:end], self) + self.tests.append(test) + + os.chdir(old_dir) + + def run(self, va_file, va_module, type_arg, filter=None, openvaf=None, cache = None, old_sim_ref=False, capture=True, check=True): + if openvaf: + if not cache: + result = run( + ["md5sum", openvaf], + stdout=PIPE, + ) + result.check_returncode() + md5sum = result.stdout[:-1].decode("utf-8").split(" ")[0] + cache = f"./.cache/{md5sum}" + Path(cache).mkdir(parents=True,exist_ok=True) + else: + openvaf = "openvaf" + + args = [openvaf,"-b", va_file] + if cache: + args.append("--cache-dir") + args.append(cache) + # print(args, cache) + result = run( + args, + stdout=PIPE, + ) + result.check_returncode() + osdi_file = result.stdout[:-1].decode("utf-8") + + old_dir = os.getcwd() + os.chdir(self.dir) + + + + dirpath = Path('netlists') + if dirpath.exists(): + shutil.rmtree(dirpath) + os.mkdir("netlists") + + dirpath = Path('results') + if old_sim_ref: + old_path = Path("results_old") + if old_path.exists(): + shutil.rmtree(old_path) + shutil.move(dirpath,old_path) + elif dirpath.exists(): + shutil.rmtree(dirpath) + + dirpath.mkdir(exist_ok=False) + for test in self.tests: + if filter and not test.name in filter: + continue + test.run(osdi_file, va_module, type_arg, old_sim_ref=old_sim_ref, capture=capture, check=check) + os.chdir(old_dir) diff --git a/test_cases/vccs/.empty.txt b/test_cases/vccs/.empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test_cases/vcvs/.empty.txt b/test_cases/vcvs/.empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test_docker.sh b/test_docker.sh new file mode 100755 index 000000000..b9893c1cb --- /dev/null +++ b/test_docker.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +IMAGENAME="registry.gitlab.com/dospm/ngspice" +TAG="latest" + +docker build -t $IMAGENAME:$TAG . +docker run -it --rm --user "$(id -u)":"$(id -g)" -v "${PWD}":/tmp $IMAGENAME:$TAG /bin/bash \ No newline at end of file