From acfaf023b3d014ae4f2f01f3ea02cb1a2734863e Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 20 Apr 2022 18:12:10 +0200 Subject: [PATCH] prototype for Verilog-A integration using OSDI and OpenVAF This initial prototype is capable of performing DC, transient and AC analysis. Not all features of OSDI are supported yet and there are still some open questions regarding ngspice integration. However many usecase already work very well and a large amount of CMC models are supported. The biggest missing feature right now is noise analysis. test: test case for diode DC working with SH test: add transient analysis to osdi_diode test test: added docu text to osdi_diode test test: added test case directories fix: bug in osdi_load test: small change to netlist fix: implement DEVunsetup fix: correct behaviour for MODEINITSMSIG test: osdi diode enable all analysis modes removed netlist ignoring test results added the build of the diode shared object to the python test script deleting old stuff and always rebuilding the shared object added diode_va.c to the repo preparing CI Create .gitlab-ci.yml file (testing) add res, cap and multiple devices test feat: use osdi command to load files Previously OSDI shared object files were loaded from fixed directories. This was unreliable, inconvenient and caused conflicts with XSPICE. This commit remove the old loading mechanism and instead introduces the `osdi` command that can load (a list of) osdi object files (like the codemodel command for XSPICE). A typical usecase will use this as a precommand in the netlist: .control pre_osdi foo.osdi .endc If the specified file is a relative path it is first resolved relative to the parent directory of the netlist. If the osdi command is invoked from the interactive prompt the file is resolved relative to the current working directory instead. This commit also moves osdi from the devices folder to the root src folder like xspice. This better reflects the role of the code as users may otherthwise (mistakenly) assume that osdi is just another handwritten model. test: update tests to new command fix: do not ignore first parameter feat: implement log message callback fix: don't generate ddt matrix/rhs in DC sweep fix: missing linker script update to osdi 0.3 (testing) simplify test cases, fix bug (testing) multiple devices test improvement (testig) node collapsing bugfix test: increase tolerance in tests feat: update to newest OSDI header fix: temperature update dt behaviour fix: ignored models fix: compilation script fix: allow hicum/l2 to compile with older c++ compilers fix: set required compiler flags for osdi fix: disable x by default fix: add missing SPICE functions fix: update diode to latest ngspice version feat: implement python CMC test runner doc: Add README_OSDI.md fix: make testing script work with python version before 3.9 fix: free of undefined local variable fix: do not calculate time derivative during tran op update osdi version fixes for compilation on windows --- .gitignore | 4 + AUTHORS | 1 + COPYING | 380 ++++++++ Dockerfile | 8 + README_OSDI.md | 33 + compile_linux.sh | 4 +- configure.ac | 46 + src/Makefile.am | 32 +- src/frontend/com_dl.c | 15 + src/frontend/com_dl.h | 4 + src/frontend/commands.c | 6 + src/frontend/inp.c | 30 +- src/frontend/inpcom.c | 49 +- src/include/ngspice/ifsim.h | 4 + src/include/ngspice/osdiitf.h | 32 + src/osdi/Makefile.am | 25 + src/osdi/osdi.h | 209 +++++ src/osdi/osdiacld.c | 44 + src/osdi/osdicallbacks.c | 88 ++ src/osdi/osdidefs.h | 74 ++ src/osdi/osdiext.h | 36 + src/osdi/osdiinit.c | 187 ++++ src/osdi/osdiload.c | 211 +++++ src/osdi/osdiparam.c | 165 ++++ src/osdi/osdipzld.c | 49 + src/osdi/osdiregistry.c | 427 +++++++++ src/osdi/osdisetup.c | 410 +++++++++ src/osdi/osditrunc.c | 43 + src/spicelib/devices/dev.c | 47 +- src/spicelib/devices/dev.h | 4 + src/spicelib/parser/Makefile.am | 5 + src/spicelib/parser/inp2a.c | 109 +++ src/spicelib/parser/inpdomod.c | 2 +- src/spicelib/parser/inpgmod.c | 12 + src/spicelib/parser/inppas2.c | 8 +- src/spicelib/parser/inpxx.h | 3 + test_cases/capacitor/.empty.txt | 0 test_cases/capacitor/capacitor.c | 374 ++++++++ test_cases/capacitor/netlist.sp | 43 + test_cases/capacitor/test_capacitor.py | 160 ++++ test_cases/cccs/.empty.txt | 0 test_cases/ccvs/.empty.txt | 0 test_cases/diode/diode.c | 913 +++++++++++++++++++ test_cases/diode/netlist.sp | 46 + test_cases/diode/test_diode.py | 162 ++++ test_cases/diode/test_osdi.zip | Bin 0 -> 83311 bytes test_cases/hicuml2/.empty.txt | 0 test_cases/inductor/.empty.txt | 0 test_cases/multiple_devices/.empty.txt | 0 test_cases/multiple_devices/capacitor.c | 374 ++++++++ test_cases/multiple_devices/netlist.sp | 49 + test_cases/multiple_devices/resistor.c | 364 ++++++++ test_cases/multiple_devices/test_multiple.py | 172 ++++ test_cases/node_collapsing/.empty.txt | 0 test_cases/node_collapsing/diode.c | 831 +++++++++++++++++ test_cases/node_collapsing/netlist.sp | 46 + test_cases/node_collapsing/test_diode.py | 148 +++ test_cases/resistor/.empty.txt | 0 test_cases/resistor/netlist.sp | 44 + test_cases/resistor/resistor.c | 364 ++++++++ test_cases/resistor/test_resistor.py | 180 ++++ test_cases/testing.py | 586 ++++++++++++ test_cases/vccs/.empty.txt | 0 test_cases/vcvs/.empty.txt | 0 test_docker.sh | 7 + 65 files changed, 7635 insertions(+), 34 deletions(-) create mode 100644 Dockerfile create mode 100644 README_OSDI.md create mode 100644 src/include/ngspice/osdiitf.h create mode 100644 src/osdi/Makefile.am create mode 100644 src/osdi/osdi.h create mode 100644 src/osdi/osdiacld.c create mode 100644 src/osdi/osdicallbacks.c create mode 100644 src/osdi/osdidefs.h create mode 100644 src/osdi/osdiext.h create mode 100644 src/osdi/osdiinit.c create mode 100644 src/osdi/osdiload.c create mode 100644 src/osdi/osdiparam.c create mode 100644 src/osdi/osdipzld.c create mode 100644 src/osdi/osdiregistry.c create mode 100644 src/osdi/osdisetup.c create mode 100644 src/osdi/osditrunc.c create mode 100644 src/spicelib/parser/inp2a.c create mode 100644 test_cases/capacitor/.empty.txt create mode 100644 test_cases/capacitor/capacitor.c create mode 100644 test_cases/capacitor/netlist.sp create mode 100644 test_cases/capacitor/test_capacitor.py create mode 100644 test_cases/cccs/.empty.txt create mode 100644 test_cases/ccvs/.empty.txt create mode 100644 test_cases/diode/diode.c create mode 100644 test_cases/diode/netlist.sp create mode 100644 test_cases/diode/test_diode.py create mode 100644 test_cases/diode/test_osdi.zip create mode 100644 test_cases/hicuml2/.empty.txt create mode 100644 test_cases/inductor/.empty.txt create mode 100644 test_cases/multiple_devices/.empty.txt create mode 100644 test_cases/multiple_devices/capacitor.c create mode 100644 test_cases/multiple_devices/netlist.sp create mode 100644 test_cases/multiple_devices/resistor.c create mode 100644 test_cases/multiple_devices/test_multiple.py create mode 100644 test_cases/node_collapsing/.empty.txt create mode 100644 test_cases/node_collapsing/diode.c create mode 100644 test_cases/node_collapsing/netlist.sp create mode 100644 test_cases/node_collapsing/test_diode.py create mode 100644 test_cases/resistor/.empty.txt create mode 100644 test_cases/resistor/netlist.sp create mode 100644 test_cases/resistor/resistor.c create mode 100644 test_cases/resistor/test_resistor.py create mode 100644 test_cases/testing.py create mode 100644 test_cases/vccs/.empty.txt create mode 100644 test_cases/vcvs/.empty.txt create mode 100755 test_docker.sh 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 0000000000000000000000000000000000000000..5f5690adb876f42a638e84cd1458d97096bdcd01 GIT binary patch literal 83311 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

^ueks)oj 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 + * + * 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