From d2afe9acb1538fec66112d51448eaddad748968e Mon Sep 17 00:00:00 2001 From: Umesh Timalsina Date: Sun, 21 Feb 2021 22:29:37 -0600 Subject: [PATCH] Add ConvertCircuitToNetlist Plugin (v0.1.0). Closes #23 (#25) * WIP- #23 Initial Commit for Spice Conversion * WIP- Remove __pycache__ * WIP- Add netlist parsing for subcircuits and additional components * WIP- Add Conda to github workflow test * WIP- Fix testing in github workflows * WIP- Fix setup conda action name * WIP- Fix python environment.yml path * WIP- Test for node 12 only * WIP- Don't activate environment before test * WIP- Deploy branch for testing * WIP- Fix testing in CI * WIP- Fix tests * WIP- Fix conda shell * WIP- Use different conda setup action * WIP- Fix workflow syntax * WIP- Use Source activate in github workflow * WIP- Add netlist for PySpice Components * WIP- Deploy branch * WIP- Add test-deploy on node 10 only * WIP- Remove tests * WIP- Skip Missing Nodes in the Netlist * WIP- Add Sinusoidal Sources, generate Netlist Labels using chr * WIP- Add Basic Testing * WIP- Add Test seed * WIP- Activate conda environment before test * WIP- Fix typo in environment name * WIP- Fix eslint issues in tests/globals.js * WIP- Readd node 12 to test matrix * WIP- Minor fixes and cleanup * WIP- Run black * WIP- Run Isort * WIP- Add Dummy Modelname for netlist for transistor and MOSFET * WIP- Add units, extended test * WIP- Don't deploy branch * WIP- Lint Python using black * WIP- Run black on plugin files --- .github/workflows/linter.yml | 1 + .github/workflows/test-deploy.yml | 4 +- Dockerfile | 8 +- config/config.webgme.js | 8 +- docker-compose.yml | 1 + environment.yml | 1 + package-lock.json | 349 +++++++++-- package.json | 6 +- .../ConvertCircuitToNetlist.js | 143 +++++ .../ConvertCircuitToNetlist/__init__.py | 544 ++++++++++++++++++ .../ConvertCircuitToNetlist/metadata.json | 21 + .../ConvertCircuitToNetlist/run_debug.py | 73 +++ .../ConvertCircuitToNetlist/run_plugin.py | 54 ++ src/seeds/project/project.webgmex | Bin 127892 -> 127936 bytes src/seeds/tests/tests.webgmex | Bin 0 -> 228113 bytes test/globals.js | 24 + .../ConvertCircuitToNetlist.spec.js | 123 ++++ .../netlist_to_json.py | 65 +++ utils/install-miniconda.sh | 17 + webgme-setup.json | 21 +- 20 files changed, 1410 insertions(+), 53 deletions(-) create mode 100644 src/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist.js create mode 100644 src/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist/__init__.py create mode 100644 src/plugins/ConvertCircuitToNetlist/metadata.json create mode 100644 src/plugins/ConvertCircuitToNetlist/run_debug.py create mode 100644 src/plugins/ConvertCircuitToNetlist/run_plugin.py create mode 100644 src/seeds/tests/tests.webgmex create mode 100644 test/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist.spec.js create mode 100644 test/plugins/ConvertCircuitToNetlist/netlist_to_json.py create mode 100644 utils/install-miniconda.sh diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index ae59083..0b49df1 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -24,3 +24,4 @@ jobs: LINTER_RULES_PATH: / JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.yml VALIDATE_JAVASCRIPT_ES: true + VALIDATE_PYTHON_BLACK: true diff --git a/.github/workflows/test-deploy.yml b/.github/workflows/test-deploy.yml index ba9ef10..361b242 100644 --- a/.github/workflows/test-deploy.yml +++ b/.github/workflows/test-deploy.yml @@ -38,7 +38,9 @@ jobs: run: npm install - name: Run Tests - run: npm test + run: | + source activate electric-circuits + npm test dockerAndDeploy: runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index f471f84..930098a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,14 @@ ADD . /webgme WORKDIR /webgme +RUN chmod +x utils/install-miniconda.sh && ./utils/install-miniconda.sh + +ENV PATH /root/miniconda3/bin:$PATH + +RUN conda init && conda env create --file environment.yml + RUN npm install -g npm -RUN npm config set unsafe-perm true && npm install +RUN npm config set unsafe-perm true && npm install && npm config set script-shell /bin/bash ENTRYPOINT NODE_ENV=production npm start diff --git a/config/config.webgme.js b/config/config.webgme.js index 6f6f4fd..d079a57 100644 --- a/config/config.webgme.js +++ b/config/config.webgme.js @@ -16,12 +16,18 @@ config.seedProjects.basePaths.push(__dirname + '/../src/seeds/project'); - +config.rest.components['BindingsDocs'] = { + src: __dirname + '/../node_modules/webgme-bindings/src/routers/BindingsDocs/BindingsDocs.js', + mount: 'bindings-docs', + options: {} +}; // Visualizer descriptors // Add requirejs paths config.requirejsPaths = { + 'BindingsDocs': 'node_modules/webgme-bindings/src/routers/BindingsDocs', + 'webgme-bindings': './node_modules/webgme-bindings/src/common', 'electric-circuits': './src/common' }; diff --git a/docker-compose.yml b/docker-compose.yml index 8f469d7..9c4d3f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: - DEPLOYMENT_BLOB_DIR=/data/blob ports: - "8888:8888" + - "5555:5555" volumes: - "$HOME/.webgme/blob:/data/blob" - "${TOKEN_KEYS_DIR}:/token_keys" diff --git a/environment.yml b/environment.yml index a4867e4..bbce463 100644 --- a/environment.yml +++ b/environment.yml @@ -8,3 +8,4 @@ dependencies: - pyspice - pip: - requests + - webgme-bindings diff --git a/package-lock.json b/package-lock.json index 722e825..65969a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,8 +83,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "3.2.1", @@ -111,6 +110,11 @@ "normalize-path": "^2.0.0" } }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, "archiver": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz", @@ -141,6 +145,15 @@ "readable-stream": "^2.0.0" } }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, "argh": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/argh/-/argh-0.1.4.tgz", @@ -417,8 +430,7 @@ "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "base64id": { "version": "1.0.0", @@ -786,7 +798,6 @@ "version": "5.7.0", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.0.tgz", "integrity": "sha512-cd+5r1VLBwUqTrmnzW+D7ABkJUM6mr7uv1dv+6jRw4Rcl7tFIFHDqHPL98LhpGFn3dbAt3gtLxtrWp4m1kFrqg==", - "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -983,6 +994,11 @@ "readdirp": "^2.0.0" } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -1050,8 +1066,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collection-visit": { "version": "1.0.0", @@ -1261,6 +1276,11 @@ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", "dev": true }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -1334,8 +1354,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "crc": { "version": "3.8.0", @@ -1462,6 +1481,14 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "requires": { + "mimic-response": "^2.0.0" + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -1471,6 +1498,11 @@ "type-detect": "^4.0.0" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "default-user-agent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-user-agent/-/default-user-agent-1.0.0.tgz", @@ -1545,6 +1577,11 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -1579,6 +1616,11 @@ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", "dev": true }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, "detective": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", @@ -1793,7 +1835,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -2041,6 +2082,11 @@ "fill-range": "^2.1.0" } }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, "express": { "version": "4.16.3", "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", @@ -2370,8 +2416,7 @@ "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs.realpath": { "version": "1.0.0", @@ -2396,6 +2441,21 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, "get-assigned-identifiers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", @@ -2414,6 +2474,11 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -2528,6 +2593,11 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -2710,8 +2780,7 @@ "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "indexof": { "version": "0.0.1", @@ -2738,8 +2807,12 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "ink-docstrap": { "version": "1.3.2", @@ -2882,7 +2955,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2949,8 +3021,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isobject": { "version": "2.1.0", @@ -3463,6 +3534,11 @@ "mime-db": "1.44.0" } }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -3621,6 +3697,11 @@ "minimist": "0.0.8" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "mocha": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", @@ -3767,9 +3848,7 @@ "nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", - "dev": true, - "optional": true + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" }, "nanomatch": { "version": "1.2.13", @@ -3810,6 +3889,11 @@ } } }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", @@ -3828,12 +3912,25 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, + "node-abi": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz", + "integrity": "sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg==", + "requires": { + "semver": "^5.4.1" + } + }, "nodemailer": { "version": "6.4.14", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.14.tgz", "integrity": "sha512-0AQHOOT+nRAOK6QnksNaK7+5vjviVvEBzmZytKU7XSA+Vze2NLykTx/05ti1uJgXFTWrMq08u3j3x4r4OE6PAA==", "dev": true }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" + }, "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", @@ -3849,11 +3946,21 @@ "integrity": "sha512-AgSt+cP5XMooho1Ppn8NB3FFaVWefV+qZoZncYTUSch2GAEwlYLcIIbT5YVkMlFeNHnfwOvc4HDlbvrB5BRxXA==", "dev": true }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "nunjucks": { "version": "2.4.3", @@ -3875,8 +3982,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-component": { "version": "0.0.3", @@ -3969,7 +4075,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -4196,6 +4301,35 @@ } } }, + "prebuild-install": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.6.tgz", + "integrity": "sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg==", + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + } + } + }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -4217,8 +4351,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "proxy-addr": { "version": "2.0.6", @@ -4252,6 +4385,15 @@ } } }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -4261,8 +4403,7 @@ "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, "qs": { "version": "6.5.2", @@ -4417,6 +4558,29 @@ "unpipe": "1.0.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + } + } + }, "read-all-stream": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-1.0.2.tgz", @@ -4436,7 +4600,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4893,8 +5056,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -4932,8 +5094,7 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "send": { "version": "0.16.2", @@ -4991,6 +5152,11 @@ "send": "0.16.2" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -5055,11 +5221,25 @@ "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", "dev": true }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, "simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } }, "snapdragon": { "version": "0.8.2", @@ -5403,7 +5583,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5414,7 +5593,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -5423,7 +5601,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5493,6 +5670,51 @@ "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", "dev": true }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + }, + "dependencies": { + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + } + } + }, "tar-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", @@ -5633,6 +5855,14 @@ "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", "dev": true }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", @@ -5853,8 +6083,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utility": { "version": "1.7.1", @@ -5910,6 +6139,15 @@ "webgme-user-management-page": "^0.5.0" } }, + "webgme-bindings": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/webgme-bindings/-/webgme-bindings-1.2.1.tgz", + "integrity": "sha512-LxBqKEvqY0aZj7Xm33mkjqqlkrcyMnwpvGMNtZvEkoRLIjVpgX0FrjUjygQi9PpHHzUiQtbEjg2ZlLQ8PGfn1g==", + "requires": { + "q": "^1.5.1", + "zeromq": "^5.1.0" + } + }, "webgme-engine": { "version": "2.25.1", "resolved": "https://registry.npmjs.org/webgme-engine/-/webgme-engine-2.25.1.tgz", @@ -6102,6 +6340,19 @@ } } }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "win-release": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/win-release/-/win-release-1.1.1.tgz", @@ -6152,8 +6403,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { "version": "3.3.3", @@ -6243,6 +6493,15 @@ "validator": "^10.0.0" } }, + "zeromq": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/zeromq/-/zeromq-5.2.0.tgz", + "integrity": "sha512-qsckhCmrg6et6zrAJytC971SSN/4iLxKgkXK1Wqn2Gij5KXMY+TA+3cy/iFwehaWdU5usg5HNOOgaBdjSqtCVw==", + "requires": { + "nan": "^2.14.0", + "prebuild-install": "^5.3.2" + } + }, "zip-stream": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", diff --git a/package.json b/package.json index 385933d..0d66936 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "electric-circuits", "scripts": { - "start": "node app.js", + "start": "source activate electric-circuits && node app.js", "test": "node ./node_modules/mocha/bin/mocha --recursive test", "apply": "node ./node_modules/webgme-engine/src/bin/apply.js", "diff": "node ./node_modules/webgme-engine/src/bin/diff.js", @@ -19,7 +19,9 @@ "mocha": "^5.2.0", "webgme": "^2.42.0" }, - "dependencies": {}, + "dependencies": { + "webgme-bindings": "^1.2.1" + }, "description": "First, install the electric-circuits following: - [NodeJS](https://nodejs.org/en/) (v4.x.x recommended) - [MongoDB](https://www.mongodb.com/)", "main": "app.js", "directories": { diff --git a/src/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist.js b/src/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist.js new file mode 100644 index 0000000..7678116 --- /dev/null +++ b/src/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist.js @@ -0,0 +1,143 @@ +/* globals define */ +/* eslint-env node */ + +/** + * Generated by PluginGenerator 2.20.5 from webgme on Thu Dec 17 2020 12:43:35 GMT-0600 (Central Standard Time). + * A plugin that inherits from the PluginBase. To see source code documentation about available + * properties and methods visit %host%/docs/source/PluginBase.html. + */ + +define([ + 'q', + 'plugin/PluginConfig', + 'text!./metadata.json', + 'plugin/PluginBase', + 'module' +], function ( + Q, + PluginConfig, + pluginMetadata, + PluginBase, + module) { + 'use strict'; + + pluginMetadata = JSON.parse(pluginMetadata); + const path = require('path'); + // Modify these as needed.. + const START_PORT = 5555; + const COMMAND = 'python'; + const SCRIPT_FILE = path.join(path.dirname(module.uri), 'run_plugin.py'); + + /** + * Initializes a new instance of PythonBindings. + * @class + * @augments {PluginBase} + * @classdesc This class represents the plugin PythonBindings. + * @constructor + */ + function ConvertCircuitToNetlist() { + // Call base class' constructor. + PluginBase.call(this); + this.pluginMetadata = pluginMetadata; + } + + /** + * Metadata associated with the plugin. Contains id, name, version, description, icon, configStructue etc. + * This is also available at the instance at this.pluginMetadata. + * @type {object} + */ + ConvertCircuitToNetlist.metadata = pluginMetadata; + + // Prototypical inheritance from PluginBase. + ConvertCircuitToNetlist.prototype = Object.create(PluginBase.prototype); + ConvertCircuitToNetlist.prototype.constructor = ConvertCircuitToNetlist; + + /** + * Main function for the plugin to execute. This will perform the execution. + * Notes: + * - Always log with the provided logger.[error,warning,info,debug]. + * - Do NOT put any user interaction logic UI, etc. inside this method. + * - callback always has to be called even if error happened. + * + * @param {function(null|Error|string, plugin.PluginResult)} callback - the result callback + */ + ConvertCircuitToNetlist.prototype.main = function (callback) { + const CoreZMQ = require('webgme-bindings').CoreZMQ; + const cp = require('child_process'); + const logger = this.logger; + + // due to the limited options on the script return values, we need this hack + this.result.setSuccess(null); + + const callScript = (program, scriptPath, port) => { + let deferred = Q.defer(), + options = {}, + args = [ + scriptPath, + port, + `"${this.commitHash}"`, + `"${this.branchName}"`, + `"${this.core.getPath(this.activeNode)}"`, + `"${this.activeSelection.map(node => this.core.getPath(node)).join(',')}"`, + `"${this.namespace}"`, + ]; + + const childProc = cp.spawn(program, args, options); + + childProc.stdout.on('data', data => { + logger.info(data.toString()); + // logger.debug(data.toString()); + }); + + childProc.stderr.on('data', data => { + logger.error(data.toString()); + }); + + childProc.on('close', (code) => { + if (code > 0) { + // This means an execution error or crash, so we are failing the plugin + deferred.reject(new Error(`${program} ${args.join(' ')} exited with code ${code}.`)); + this.result.setSuccess(false); + } else { + if(this.result.getSuccess() === null) { + // The result have not been set inside the python, but it suceeded, so we go with the true value + this.result.setSuccess(true); + } + deferred.resolve(); + } + }); + + childProc.on('error', (err) => { + // This is a hard execution error, like the child process cannot be instantiated... + logger.error(err); + this.result.setSuccess(false); + deferred.reject(err); + }); + + return deferred.promise; + }; + + const corezmq = new CoreZMQ(this.project, this.core, this.logger, {port: START_PORT, plugin: this}); + corezmq.startServer() + .then((port) => { + logger.info(`zmq-server listening at port ${port}`); + return callScript(COMMAND, SCRIPT_FILE, port); + }) + .then(() => { + return corezmq.stopServer(); + }) + .then(() => { + callback(null, this.result); + }) + .catch((err) => { + this.logger.error(err.stack); + corezmq.stopServer() + .finally(() => { + // Result success is false at invocation. + callback(err, this.result); + }); + }); + }; + + return ConvertCircuitToNetlist; +}); diff --git a/src/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist/__init__.py b/src/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist/__init__.py new file mode 100644 index 0000000..ccdf5a3 --- /dev/null +++ b/src/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist/__init__.py @@ -0,0 +1,544 @@ +""" +This is where the implementation of the plugin code goes. +The ConvertCircuitToNetlist-class is imported from both run_plugin.py and run_debug.py +""" +import logging +import re +import sys +from functools import partial +from typing import Iterable, List, Optional, Union + +from PySpice.Spice.Netlist import Circuit, SubCircuit +from PySpice.Unit import * +from webgme_bindings import PluginBase + +# Setup a logger +logger = logging.getLogger("ConvertCircuitToNetlist") +logger.setLevel(logging.INFO) +handler = logging.StreamHandler(sys.stdout) # By default it logs to stderr.. +handler.setLevel(logging.INFO) +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +handler.setFormatter(formatter) +logger.addHandler(handler) + +# The labels for the components are grabbed from the following source +# https://pyspice.fabrice-salvaire.fr/releases/v1.4/api/PySpice/Spice/BasicElement.html#module-PySpice.Spice.BasicElement +component_counts = {chr(j): 0 for j in range(65, 65 + 26)} + +SKIP_NODES = [ + "VariableResistor", + "VariableConductor", + "VariableCapacitor", + "VariableInductor", + "SaturatingInductor", + "OpAmp", + "OpAmpDetailed", + "Gyrator", + "OpAmpDetailed", + "Potentiometer", + "Transformer", +] + + +def get_next_label_for(component: str, component_name: str) -> str: + assert component in component_counts + component_counts[component] = component_counts[component] + 1 + return f"{component_name}_{component_counts[component]}" + + +class NetListConversionError(Exception): + """Error to be raised when there's an error in netlist conversion""" + + +class ConvertCircuitToNetlist(PluginBase): + def main(self) -> None: + self._assign_meta_functions() + if not self.is_circuit(node=self.active_node): + err_msg = ( + f"Active Node ({self.core.get_path(node=self.active_node)}) " + f"is not of type Circuit" + ) + self._log_error(err_msg) + self.result_set_success(False) + self.result_set_error(err_msg) + else: + circuit = self.active_node + self._initialize(circuit) + self._identify_pins(circuit) + self._expand_junction_adjacency() + self._assign_spice_node_labels_to_pins() + self._populate_netlist(circuit, self._netlist_ckt) + output_filename = self.get_current_config().get("file_name") + if not output_filename: + output_filename = self.core.get_attribute(circuit, "name") + self.add_file(f"{output_filename}.cir", str(self._netlist_ckt)) + self.result_set_success(True) + + def _assign_meta_functions(self) -> None: + """Assign is_* function for easier meta type checks""" + for name, meta_node in self.META.items(): + p = partial(self.core.is_type_of, node=None, type_node_or_path=meta_node) + setattr(self, f"is_{self._to_snake_case(name)}", p) + self._log_debug( + f"Assigned meta type classification functions to {self.__class__.__name__}" + ) + + def _initialize(self, circuit: dict) -> None: + """Initialize an empty netlist and the necessary data-structures for netlist conversion + + Parameters + ---------- + circuit: dict + A gme node of type Circuit + """ + self._adj_list = dict() + self._junction_pin_ids = set() + self._ground_pins = set() + self.nodes_count = 0 + self.pin_labels = dict() + circuit_name = self.core.get_attribute(circuit, "name") + self._netlist_ckt = Circuit(circuit_name) + self._log_debug(f"Initialized empty spice netlist for {circuit_name}") + + def _identify_pins(self, circuit: dict) -> None: + """Identify the pins and build adjacency list for pins + This method(recursively) identifies all the connected pins + and builds an adjacency list for individual pin. + There are three possible situations to handle: + 1. The Pin is contained inside a normal component node + 2. The Pin is a ground Pin (Special Case for SPICE labeled '0') + 3. The Pin is contained in a Junction node + + Parameters: + ---------- + circuit: dict + A gme node of type Circuit + """ + sub_circuits = self._get_children_of_type(circuit, "Circuit") + self._log_info( + f'Identifying pins for {self.core.get_attribute(circuit, "name")}. ' + f"Number of SubCircuits {len(sub_circuits)}" + ) + + for sub_circuit in sub_circuits: + self._identify_pins(sub_circuit) + + wires = sorted( + self._get_children_of_type(circuit, "Wire"), + key=lambda x: self.core.get_path(x), + ) + + for wire in wires: + src_pin = self.core.load_pointer(wire, "src") + dst_pin = self.core.load_pointer(wire, "dst") + + src_pin_id = self.core.get_path(src_pin) + dst_pin_id = self.core.get_path(dst_pin) + self._log_debug( + f"Wire {self.core.get_path(wire)}, src: {src_pin_id}, dst: {dst_pin_id} " + ) + + if not self._adj_list.get(src_pin_id): + self._adj_list[src_pin_id] = set() + if not self._adj_list.get(dst_pin_id): + self._adj_list[dst_pin_id] = set() + + self._adj_list[src_pin_id].add(dst_pin_id) + self._adj_list[dst_pin_id].add(src_pin_id) + + for pin, pin_id in [(src_pin, src_pin_id), (dst_pin, dst_pin_id)]: + if self._is_junction_pin(pin): + remaining_pin_ids = self._get_remaining_pin_ids(pin) + self._junction_pin_ids.add(pin_id) + for remaining_pin_id in remaining_pin_ids: + self._adj_list[src_pin_id].add(remaining_pin_id) + self._adj_list[dst_pin_id].add(remaining_pin_id) + self._junction_pin_ids.add(remaining_pin_id) + + if self._is_ground_pin(src_pin) or self._is_ground_pin(dst_pin): + self._ground_pins.add(src_pin_id) + self._ground_pins.add(dst_pin_id) + + self._log_info( + f"Successfully identified all the pins for " + f'{self.core.get_attribute(circuit, "name")}' + f" with id {self.core.get_path(circuit)}" + ) + + def _expand_junction_adjacency(self): + """Expand the adjacency for pins connected to Junctions""" + for pin_id in list(self._adj_list.keys()): + self._visit_junctions(pin_id, set()) + + def _visit_junctions(self, pin_id, visited): + """Traverse the adjacency list of a pin to visit any connected Junctions""" + adj_pins = self._adj_list.get(pin_id, set()) + for adj_pin_id in list(adj_pins): + if adj_pin_id in self._junction_pin_ids and adj_pin_id not in visited: + visited.add(adj_pin_id) + extra_pin_ids = self._adj_list.get(adj_pin_id, set()) + for extra_pin_id in list(extra_pin_ids): + self._adj_list[pin_id].add(extra_pin_id) + self._visit_junctions(extra_pin_id, visited) + + def _assign_spice_node_labels_to_pins(self): + """Assign SPICE node labels to pins based on the adjacency list""" + component_pin_ids = [] + junction_pin_ids = [] + for pin_id in self._ground_pins: + self.pin_labels[pin_id] = "0" + + for pin_id in self._adj_list: + if pin_id in self._junction_pin_ids: + junction_pin_ids.append(pin_id) + else: + component_pin_ids.append(pin_id) + + for pin_id in component_pin_ids + junction_pin_ids: + if pin_id in self.pin_labels: + for adj_pin in self._adj_list[pin_id]: + self.pin_labels[adj_pin] = self.pin_labels[pin_id] + else: + current_label = None + for adj_pin in self._adj_list[pin_id]: + if adj_pin in self.pin_labels: + current_label = self.pin_labels[adj_pin] + + if not current_label: + self.nodes_count += 1 + current_label = f"N000{self.nodes_count}" + + self.pin_labels[pin_id] = current_label + + for adj_pin in self._adj_list[pin_id]: + self.pin_labels[adj_pin] = self.pin_labels[pin_id] + + def _get_remaining_pin_ids(self, pin): + """Returns the remaining pin ids of the component containing this pin""" + parent = self.core.get_parent(pin) + pins = self._get_children_of_type(parent, "Pin") + pins.remove(pin) + return list(map(lambda node: self.core.get_path(node), pins)) + + def _is_junction_pin(self, pin: dict) -> bool: + """Returns if the pin is a junction pin""" + parent = self.core.get_parent(pin) + return self.is_junction(node=parent) + + def _is_ground_pin(self, pin: dict) -> bool: + """Returns if the pin is a ground pin""" + parent = self.core.get_parent(pin) + return self.is_ground(node=parent) + + def _is_circuit_pin(self, pin: dict) -> bool: + """Returns if the pin is contained inside a circuit""" + parent = self.core.get_parent(pin) + return self.is_circuit(node=parent) + + def _get_parent_id(self, node: dict) -> str: + """Returns the parent id of a GME node""" + parent = self.core.get_parent(node) + return self.core.get_path(parent) + + def _populate_netlist( + self, circuit: dict, parent_netlist: Optional[Union[Circuit, SubCircuit]] = None + ): + """Populate the netlist with components from the Circuit + + Parameters + ---------- + circuit: dict + GME Node of type Circuit + parent_netlist: Optional[Union[Circuit, SubCircuit]], default=None + The PySpice Circuit or SubCircuit object of which the components will be a part of + """ + components = self._get_children_except( + circuit, "Pin", "Wire", "Junction", "Ground" + ) + sub_circuits = self._get_children_of_type(circuit, "Circuit") + + for sub_circuit in sub_circuits: + exposed_nodes = self._get_external_spice_nodes_for(sub_circuit) + subckt_netlist = SubCircuit( + self.core.get_attribute(sub_circuit, "name"), *exposed_nodes + ) + parent_netlist.subcircuit(subckt_netlist) + self._populate_netlist(sub_circuit, subckt_netlist) + + components_map = {} + for component in components: + component_id = self.core.get_path(component) + components_map[component_id] = dict() + components_map[component_id]["node"] = component + pins = self._get_children_of_type(component, "Pin") + for pin in pins: + pin_id = self.core.get_path(pin) + pin_name = self.core.get_attribute(pin, "name") + components_map[component_id][ + pin_name + ] = self._resolve_spice_node_label_for(pin_id) + + for component in components_map.values(): + self._add_to_netlist(component, parent_netlist) + self._log_info( + f"Circuit ({self.core.get_attribute(circuit, 'name')})'s components have been" + f"added to the netlist." + ) + + def _add_to_netlist( + self, component: dict, netlist_ckt: Union[Circuit, SubCircuit] + ) -> None: + """Add a particular component (GME Node) to the netlist""" + try: + self._is_capable_to_convert(component["node"]) + except NetListConversionError as e: + self._log_error(str(e)) + self.create_message(component["node"], str(e), severity="error") + raise e + + if self.core.is_type_of(component["node"], self.META["Basic"]): + self._add_basic_elements(component, netlist_ckt) + + elif self.core.is_type_of(component["node"], self.META["Semiconductors"]): + self._add_semiconductors(component, netlist_ckt) + + self._log_info( + f"Added element ({self.core.get_attribute(component['node'], 'name')}), " + f"of type {self._get_meta_name(component['node'])} to the netlist." + ) + + def _add_basic_elements( + self, component: dict, netlist_ckt: Union[Circuit, SubCircuit] + ) -> None: + """Add Basic elements to the netlist""" + node = component["node"] + if is_res := (self.is_resistor(node=node)) or self.is_conductor(node=node): + netlist_ckt.R( + get_next_label_for("R", self.core.get_attribute(node, "name")), + component["p"], + component["n"], + u_Ohm( + self.core.get_attribute(node, "R") + if is_res + else 1 / self.core.get_attribute(node, "G") + ), + ) + if self.is_inductor(node=node): + netlist_ckt.L( + get_next_label_for("L", self.core.get_attribute(node, "name")), + component["p"], + component["n"], + u_H(self.core.get_attribute(node, "L")), + ) + if self.is_capacitor(node=node): + netlist_ckt.Capacitor( + get_next_label_for("C", self.core.get_attribute(node, "name")), + component["p"], + component["n"], + capacitance=u_F(self.core.get_attribute(node, "C")), + ) + if self.is_voltage(node=node): + netlist_ckt.V( + get_next_label_for("V", self.core.get_attribute(node, "name")), + component["p"], + component["n"], + self.core.get_attribute(node, "V"), + ) + + if self.is_current(node=node): + netlist_ckt.I( + get_next_label_for("I", self.core.get_attribute(node, "name")), + component["p"], + component["n"], + self.core.get_attribute(node, "I"), + ) + + if is_vcc := self.is_vcc(node=node) or self.is_vcv(node=node): + netlist_ckt.G( + get_next_label_for( + "G" if is_vcc else "E", self.core.get_attribute(node, "name") + ), + component["p2"], + component["n2"], + component["p1"], + component["n1"], + self.core.get_attribute(node, "transConductance" if is_vcc else "gain"), + ) + + if is_ccc := self.is_ccc(node=node) or self.is_ccv(node=node): + voltage_label = get_next_label_for("V", "CCSourceVoltage") + netlist_ckt.V(voltage_label, component["p1"], component["n1"]) + if is_ccc: + netlist_ckt.F( + get_next_label_for("F", self.core.get_attribute(node, "name")), + component["p2"], + component["n2"], + source=voltage_label, + current_gain=self.core.get_attribute(node, "gain"), + ) + else: + netlist_ckt.H( + get_next_label_for("H", self.core.get_attribute(node, "name")), + component["p2"], + component["n2"], + source=voltage_label, + transresistance=self.core.get_attribute(node, "transResistance"), + ) + + if any( + [ + vs1 := self.is_sinusoidal_voltage_source(node=node), + self.is_sinusoidal_current_source(node=node), + vs2 := self.is_piece_wise_linear_voltage_source(node=node), + self.is_piece_wise_linear_current_source(node=node), + vs3 := self.is_random_voltage_source(node=node), + self.is_random_current_source(node=node), + vs4 := self.is_single_frequency_fm_voltage_source(node=node), + self.is_single_frequency_fm_current_source(node=node), + vs5 := self.is_pulse_voltage_source(node=node), + self.is_pulse_current_source(node=node), + vs6 := self.is_amplitude_modulated_voltage_source(node=node), + self.is_amplitude_modulated_current_source(node=node), + vs7 := self.is_exponential_voltage_source(node=node), + self.is_exponential_current_source(node=node), + vs8 := self.is_pulse_voltage_source(node=node), + self.is_pulse_current_source(node=node), + vs9 := self.is_ac_line(node=node), + ] + ): + attrs = self.core.get_valid_attribute_names(node) + attrs.remove("name") + class_name = self._get_meta_name(node) + class_callable = getattr(netlist_ckt, class_name) + ctor_kwargs = {attr: self.core.get_attribute(node, attr) for attr in attrs} + + if self.is_piece_wise_linear_voltage_source( + node=node + ) or self.is_piece_wise_linear_current_source(node=node): + ctor_kwargs["values"] = eval(ctor_kwargs["values"]) + for value in ctor_kwargs["values"]: + assert len(value) == 2 + assert all( + isinstance(val, int) or isinstance(val, float) for val in value + ), self._log_error("Could not cast values to float") + + class_callable( + get_next_label_for( + "V" if any([vs1, vs2, vs3, vs4, vs5, vs6, vs7, vs8, vs9]) else "I", + self.core.get_attribute(node, "name"), + ), + component["p"], + component["n"], + **ctor_kwargs, + ) + + def _add_semiconductors( + self, component: dict, netlist_ckt: Union[Circuit, SubCircuit] + ) -> None: + """Add Semiconductor components to the netlist""" + node = component["node"] + # SemiConductors + if ( + self.is_diode(node=node) + or self.is_led(node=node) + or self.is_schottky_diode(node=node) + or self.is_z_diode(node=node) + ): + netlist_ckt.D( + get_next_label_for("D", self.core.get_attribute(node, "name")), + component["p"], + component["n"], + model="DDummy", + ) + + if self.is_npn(node=node) or self.is_pnp(node=node): + netlist_ckt.Q( + get_next_label_for("Q", self.core.get_attribute(node, "name")), + component["C"], + component["B"], + component["E"], + model="QDummy", + ) + + if self.is_nmos(node=node) or self.is_pmos(node=node): + netlist_ckt.M( + get_next_label_for("M", self.core.get_attribute(node, "name")), + component["D"], + component["G"], + component["B"], + component["S"], + model="MDummy", + ) + + def _get_external_spice_nodes_for(self, circuit: dict) -> list: + """Get external exposed nodes for a sub-circuit/circuit + This function extract pins of a circuit/sub-circuit to add to the netlist + """ + msg = "Provided node is not of type Circuit" + assert self.is_circuit(node=circuit), self._log_error(msg) + + pins = self._get_children_of_type(circuit, "Pin") + external_spice_nodes = [] + for pin in pins: + pin_id = self.core.get_path(pin) + spice_node_id = self._resolve_spice_node_label_for(pin_id) + external_spice_nodes.append(spice_node_id) + return external_spice_nodes + + def _resolve_spice_node_label_for(self, pin_id): + """Return the SPICE node label for a particular pin""" + if pin_id in self.pin_labels: + return self.pin_labels[pin_id] + else: + self.nodes_count += 1 + return f"N000{self.nodes_count}" + + def _get_meta_name(self, node: dict) -> str: + """From a GME Node, get its META name""" + meta_node = self.core.get_meta_type(node) + if meta_node: + return self.core.get_attribute(meta_node, "name") + + # Helper Methods for gme nodes + def _get_children_of_type(self, node: dict, type_: str) -> List[dict]: + """Returns children of a GME node of specific type""" + return list( + filter( + lambda x: self.core.get_meta_type(x) == self.META[type_], + self.core.load_children(node), + ) + ) + + def _get_children_except(self, node: dict, *args: Iterable[str]) -> List[dict]: + """Returns children of a GME Node except provided as positional arguments""" + assert (type(arg) == str for arg in args), "Please Provide a specific type" + children = [] + for child in self.core.load_children(node): + if all(self.core.get_meta_type(child) != self.META[arg] for arg in args): + children.append(child) + return children + + def _is_capable_to_convert(self, node) -> bool: + """Returns whether or not conversion is possible""" + for skip in SKIP_NODES: + if self.core.is_type_of(node, self.META[skip]): + raise NetListConversionError( + f"Node of type {skip} is not supported yet" + ) + return True + + # General Logging functions + def _log_error(self, msg: str) -> None: + self.logger.error(msg) + + def _log_info(self, msg: str) -> None: + self.logger.info(msg) + + def _log_debug(self, msg: str) -> None: + self.logger.debug(msg) + + @staticmethod + def _to_snake_case(string: str) -> str: + """Convert a camel case `CamelCase` string to snake case (camel_case)""" + string = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", string) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", string).lower() diff --git a/src/plugins/ConvertCircuitToNetlist/metadata.json b/src/plugins/ConvertCircuitToNetlist/metadata.json new file mode 100644 index 0000000..51e017e --- /dev/null +++ b/src/plugins/ConvertCircuitToNetlist/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "ConvertCircuitToNetlist", + "name": "ConvertCircuitToNetlist", + "version": "0.1.0", + "description": "", + "icon": { + "class": "glyphicon glyphicon-cog", + "src": "" + }, + "disableServerSideExecution": false, + "disableBrowserSideExecution": true, + "dependencies": [], + "writeAccessRequired": false, + "configStructure": [{ + "name": "file_name", + "displayName": "Output Filename", + "description": "The output filename for the netlist. Note: The filename will be suffixed with .cir extension", + "valueType": "string", + "readOnly": false + }] +} diff --git a/src/plugins/ConvertCircuitToNetlist/run_debug.py b/src/plugins/ConvertCircuitToNetlist/run_debug.py new file mode 100644 index 0000000..9e49fdc --- /dev/null +++ b/src/plugins/ConvertCircuitToNetlist/run_debug.py @@ -0,0 +1,73 @@ +""" +This file can be used as the entry point when debugging the python portion of the plugin. +Rather than relying on be called from a node-process with a corezmq server already up and running +(which is the case for run_plugin.py) this script starts such a server in a sub-process. + +To change the context (project-name etc.) modify the CAPITALIZED options passed to the spawned node-js server. + +Note! This must run with the root of the webgme-repository as cwd. +""" + +import sys +import os +import subprocess +import signal +import atexit +import logging +from webgme_bindings import WebGME +from ConvertCircuitToNetlist import ConvertCircuitToNetlist + +logger = logging.getLogger("ConvertCircuitToNetlist") + +# Modify these or add option or parse from sys.argv (as in done in run_plugin.py) +PORT = "5555" +PROJECT_NAME = "Example" +BRANCH_NAME = "master" +ACTIVE_NODE_PATH = "" +ACTIVE_SELECTION_PATHS = [] +NAMESPACE = "" +METADATA_PATH = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "metadata.json" +) + +COREZMQ_SERVER_FILE = os.path.join( + os.getcwd(), "node_modules", "webgme-bindings", "bin", "corezmq_server.js" +) + +if not os.path.isfile(COREZMQ_SERVER_FILE): + COREZMQ_SERVER_FILE = os.path.join(os.getcwd(), "bin", "corezmq_server.js") + +# Star the server (see bin/corezmq_server.js for more options e.g. for how to pass a pluginConfig) +node_process = subprocess.Popen( + ["node", COREZMQ_SERVER_FILE, PROJECT_NAME, "-p", PORT, "-m", METADATA_PATH], + stdout=sys.stdout, + stderr=sys.stderr, +) + +logger.info("Node-process running at PID {0}".format(node_process.pid)) +# Create an instance of WebGME and the plugin +webgme = WebGME(PORT, logger) + + +def exit_handler(): + logger.info("Cleaning up!") + webgme.disconnect() + node_process.send_signal(signal.SIGTERM) + + +atexit.register(exit_handler) + +commit_hash = webgme.project.get_branch_hash(BRANCH_NAME) +plugin = ConvertCircuitToNetlist( + webgme, + commit_hash, + BRANCH_NAME, + ACTIVE_NODE_PATH, + ACTIVE_SELECTION_PATHS, + NAMESPACE, +) + +# Do the work +plugin.main() + +# The exit_handler will be invoked after this line diff --git a/src/plugins/ConvertCircuitToNetlist/run_plugin.py b/src/plugins/ConvertCircuitToNetlist/run_plugin.py new file mode 100644 index 0000000..c756a8f --- /dev/null +++ b/src/plugins/ConvertCircuitToNetlist/run_plugin.py @@ -0,0 +1,54 @@ +""" +This script is called by the plugin-wrapper, ConvertCircuitToNetlist.js, which passes down the +plugin context via arguments. These can be modified to include more information if needed. +Notes: + - The current working directory when called from a plugin is the root of your webgme repo. + - At the point of invocation of this plugin - it is assumed that a coreZMQ-server is running at 127.0.0.1:PORT. + - For debugging use run_debug.py which starts coreZMQ-server (make sure to modify the context in run_debug.py first) +""" + +import sys +import logging +from webgme_bindings import WebGME +from ConvertCircuitToNetlist import ConvertCircuitToNetlist + +logger = logging.getLogger("ConvertCircuitToNetlist") + +# Read in the context from sys.argv passed by the plugin +logger.info("sys.args: {0}".format(sys.argv)) + +PORT = sys.argv[1] +COMMIT_HASH = sys.argv[2].strip('"') +BRANCH_NAME = sys.argv[3].strip('"') +ACTIVE_NODE_PATH = sys.argv[4].strip('"') +ACTIVE_SELECTION_PATHS = [] + +if sys.argv[5] != '""': + ACTIVE_SELECTION_PATHS = sys.argv[5].strip('"').split(",") + if ACTIVE_SELECTION_PATHS[0] == "": + ACTIVE_SELECTION_PATHS.pop(0) + +NAMESPACE = sys.argv[6].strip('"') + +logger.debug("commit-hash: {0}".format(COMMIT_HASH)) +logger.debug("branch-name: {0}".format(BRANCH_NAME)) +logger.debug("active-node-path: {0}".format(ACTIVE_NODE_PATH)) +logger.debug("active-selection-paths: {0}".format(ACTIVE_SELECTION_PATHS)) +logger.debug("name-space: {0}".format(NAMESPACE)) + +# Create an instance of WebGME and the plugin +webgme = WebGME(PORT, logger) +plugin = ConvertCircuitToNetlist( + webgme, + COMMIT_HASH, + BRANCH_NAME, + ACTIVE_NODE_PATH, + ACTIVE_SELECTION_PATHS, + NAMESPACE, +) + +# Do the work +plugin.main() + +# Finally disconnect from the zmq-server +webgme.disconnect() diff --git a/src/seeds/project/project.webgmex b/src/seeds/project/project.webgmex index 6dbb0a9bb7b5d4921d9f6343d218a5905e856b8d..aad3ec66d30cb6f3036ff867ab7d08b57490ac90 100644 GIT binary patch delta 596 zcma))y-EW?6oq#ZH5R52B7z~JD=`5h%+Ait?$}rat0)9*L=>H0T?it{MtlJU!6Hk9 zZH3epaSK~tL9n#2OKFi8FgB^TxYfCwbHB@Z9_2npxqP#cohexJmSq(OM^V03F`8)) z8AQ0Iib>z69>Z8Fju9i;HPjdvLls>dSEZ&zQi(iIOMwM+s4oPEj4OgTGzPhfVS}CM zXC@Q8FZ!O0!SgDQrOD-m%OR1TjUp{VvL zM9_7O@DYKS5oRzF$!i9cPEuM^yJnvU%HFXzRLY0+7a#VM`?cSzYTAtW6M>~<_}I&& zhX|e)qRn*3@yh@#$8ROj+p5{OycMWst9uz-cP3Qzw&tPUYiSv_16dE+vKxe*10C{s pSO&%S(^c^ORW9tZSvm&R|1=vQvz?tb<#4LSXJg>Z-Aq<2>km2VsB8cL delta 555 zcma))y-EX75QX<9Vnkd;5CcLmu4qz3ynlOttWszdi(n;M=-j;-%p3Ru3K5Ge5%wu8 zbOl?*SMUL}N@)?WHa4lZI7~5zZ#ctw8&@92m9X0>&#eUuK@hZ;kJ7N!kd|u@EY{R< zPpwa;LwR8ZP#SCPHBc9WNH0&DXq414QJ(h%)xek&!ZGc9qP^jkK@Yhw6w|MbqS(N3`@l$^zWC_M!~?@ z`#xFEo@->7)sd`>+IKInY?Fx3o%8Uwx;P=hzq%b#+AGfra5CFvg9%~thf_-s`~g!q Bn=1eS diff --git a/src/seeds/tests/tests.webgmex b/src/seeds/tests/tests.webgmex new file mode 100644 index 0000000000000000000000000000000000000000..269d7e2a631ebe9dc10d13e62d14144d928110ef GIT binary patch literal 228113 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 OPyFxy>$iRK$^Qp4t8u9S literal 0 HcmV?d00001 diff --git a/test/globals.js b/test/globals.js index f512d02..17353fb 100644 --- a/test/globals.js +++ b/test/globals.js @@ -20,5 +20,29 @@ WebGME.addToRequireJsPaths(gmeConfig); testFixture.getGmeConfig = getGmeConfig; testFixture.EC_SEED_DIR = testFixture.path.join(__dirname, '..', 'src', 'seeds'); +testFixture.testSeedPath = testFixture.path.join(testFixture.EC_SEED_DIR, 'tests', 'tests.webgmex'); + +testFixture.CIRCUITS = { + HPF: '/C/n', + LPF: '/C/B', + VoltageLimiter: '/e/M', + PeakDetector: '/e/C', + ClampedCapacitor: '/e/b', + VoltageDoubler: '/e/I', + CEAmplifier: '/i/R', + NPNSwitch: '/i/b' +}; + +testFixture.getChildrenOfType = async function (core, node, type) { + return (await core.loadChildren(node)).filter(child => { + return core.getAttribute(core.getMetaType(child), 'name') === type; + }); +}; + +testFixture.getChildrenExcept = async function(core, node, types=[]) { + return (await core.loadChildren(node)).filter(child => { + return !types.includes(core.getAttribute(core.getMetaType(child), 'name')); + }); +}; module.exports = testFixture; diff --git a/test/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist.spec.js b/test/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist.spec.js new file mode 100644 index 0000000..612be04 --- /dev/null +++ b/test/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist.spec.js @@ -0,0 +1,123 @@ +/*eslint-env node, mocha*/ + +describe('ConvertCircuitToNetlist', function () { + const testFixture = require('../../globals'); + const {promisify} = require('util'); + const gmeConfig = testFixture.getGmeConfig(); + const logger = testFixture.logger.fork('ConvertCircuitToNetlist'); + const PluginCliManager = testFixture.WebGME.PluginCliManager; + const manager = new PluginCliManager(null, logger, gmeConfig); + const projectName = 'testProject'; + const pluginName = 'ConvertCircuitToNetlist'; + const PROJECT_SEED = testFixture.testSeedPath; + manager.runPluginMain = promisify(manager.runPluginMain); + const assert = require('assert'); + const {spawnSync} = require('child_process'); + const PYSPICE_SCRIPT = testFixture.path.resolve(__dirname, 'netlist_to_json.py'); + + let gmeAuth, + storage, + context, + project, + pluginConfig, + plugin, + core; + + before(async function () { + gmeAuth = await testFixture.clearDBAndGetGMEAuth(gmeConfig, projectName); + storage = testFixture.getMemoryStorage(logger, gmeConfig, gmeAuth); + await storage.openDatabase(); + const importParam = { + projectSeed: PROJECT_SEED, + projectName: projectName, + branchName: 'master', + logger: logger, + gmeConfig: gmeConfig + }; + + const importResult = await testFixture.importProject(storage, importParam); + const commitHash = importResult.commitHash; + project = importResult.project; + + plugin = await manager.initializePlugin(pluginName); + context = { + project: project, + commitHash: commitHash, + branchName: 'master' + }; + + pluginConfig = { + file_name: null + }; + + }); + + after(async function () { + await storage.closeDatabase(); + await gmeAuth.unload(); + }); + + function arrayEquals(arr1, arr2) { + return Array.isArray(arr1) && + Array.isArray(arr2) && + arr1.length === arr2.length && + arr1.every((val, index) => val === arr2[index]); + } + + async function runPluginAndReturnNetlist(activeNode) { + context.activeNode = activeNode; + await manager.configurePlugin(plugin, pluginConfig, context); + core = plugin.core; + const result = await manager.runPluginMain( + plugin + ); + return await plugin.blobClient.getObjectAsString( + result.artifacts.pop() + ); + } + + async function getElementNamesFor(circuit) { + return (await testFixture.getChildrenExcept( + core, + circuit, + ['Wire', 'Pin', 'Ground', 'Junction', 'Circuit'] + )).map(child => core.getAttribute(child, 'name')); + } + + async function assertValidNetlist(netlist, circuitNode) { + assert(circuitNode); + const pySpiceProcess = spawnSync('python', [PYSPICE_SCRIPT, netlist]); + const netlistJSON = JSON.parse(pySpiceProcess.stdout.toString()); + + const subCircuits = await testFixture.getChildrenOfType( + core, + circuitNode, + 'Circuit' + ); + + const elements = await getElementNamesFor(circuitNode); + + for (let subCircuit of subCircuits) { + const subCircuitName = core.getAttribute(subCircuit, 'name'); + const subCircuitInNetlist = netlistJSON.sub_circuits + .find(sub_ckt => sub_ckt.name === subCircuitName); + + const subCircuitElements = await getElementNamesFor(subCircuit); + + assert(subCircuitInNetlist); + assert(arrayEquals(subCircuitElements.sort(), Object.keys(subCircuitInNetlist.nodes).sort())); + } + + assert(subCircuits.length === netlistJSON.sub_circuits.length); + assert(arrayEquals(elements.sort(), Object.keys(netlistJSON.nodes).sort())); + } + + describe('conversion', function (){ + Object.keys(testFixture.CIRCUITS).forEach(cktName => { + it(`Should convert ${cktName}`, async () => { + const netlist = await runPluginAndReturnNetlist(testFixture.CIRCUITS[cktName]); + await assertValidNetlist(netlist, plugin.activeNode); + }); + }); + }); +}); diff --git a/test/plugins/ConvertCircuitToNetlist/netlist_to_json.py b/test/plugins/ConvertCircuitToNetlist/netlist_to_json.py new file mode 100644 index 0000000..aef6ab1 --- /dev/null +++ b/test/plugins/ConvertCircuitToNetlist/netlist_to_json.py @@ -0,0 +1,65 @@ +import json +import re +import sys + +from PySpice.Spice.Parser import SpiceParser +from PySpice.Spice.Netlist import Circuit, SubCircuit + + +class NetListJSON: + def __init__(self, netlist: str) -> None: + spice_parser = SpiceParser(source=netlist) + self.ckt = self._build_circuit(spice_parser) + self.webgme_name_re = re.compile(r"([A-Z])(.*)(_\d)") + + def json(self, ckt=None) -> str: + ckt_json = {} + if ckt is None: + ckt = self.ckt + self.get_json(ckt, ckt_json) + return json.dumps(ckt_json) + + def get_json(self, ckt, initial): + initial.update( + { + "name": ckt.name if isinstance(ckt, SubCircuit) else ckt.title, + "nodes": { + self._get_webgme_name(element_name): self._get_nodes_for( + ckt.element(element_name) + ) + for element_name in ckt.element_names + }, + } + ) + initial["sub_circuits"] = [] + for sub_ckt in ckt.subcircuits: + sub_ckt_nodes = {} + self.get_json(sub_ckt, sub_ckt_nodes) + initial["sub_circuits"].append(sub_ckt_nodes) + + @staticmethod + def _get_nodes_for(element): + return [str(pin.node) for pin in element.pins] + + @staticmethod + def _build_circuit(spice_parser: SpiceParser) -> Circuit: + ckt = spice_parser.build_circuit() + for sub_ckt in spice_parser.subcircuits: + ckt.subcircuit(sub_ckt.build()) + return ckt + + def _get_webgme_name(self, element_name: str) -> str: + match = self.webgme_name_re.match(element_name) + if match: + return match.group(2) + else: + return element_name + + +def main(netlist): + jsonifier = NetListJSON(netlist) + sys.stdout.write(jsonifier.json()) + + +if __name__ == "__main__": + main(sys.argv[1]) diff --git a/utils/install-miniconda.sh b/utils/install-miniconda.sh new file mode 100644 index 0000000..3711054 --- /dev/null +++ b/utils/install-miniconda.sh @@ -0,0 +1,17 @@ +#!/bin/bash +MINICONDA=Miniconda3-latest-Linux-x86_64.sh +MINICONDA_MD5=$(curl -s https://repo.anaconda.com/miniconda/ | grep -A3 $MINICONDA | sed -n '4p' | sed -n 's/ *\(.*\)<\/td> */\1/p') +curl -O https://repo.anaconda.com/miniconda/$MINICONDA +MD5SUM=$(md5sum $MINICONDA | cut -d ' ' -f 1) +if [[ $MINICONDA_MD5 != $MD5SUM ]]; then + echo "Miniconda MD5 mismatch" + echo $MINICONDA_MD5 + echo $MD5SUM + exit 1 +fi +bash $MINICONDA -b +rm -f $MINICONDA + +export PATH=$HOME/miniconda3/bin:$PATH + +conda update -yq conda diff --git a/webgme-setup.json b/webgme-setup.json index 0d9ee1f..f96cd9d 100644 --- a/webgme-setup.json +++ b/webgme-setup.json @@ -9,6 +9,10 @@ "CreateElectricCircuitsMeta": { "src": "src/plugins/CreateElectricCircuitsMeta", "test": "test/plugins/CreateElectricCircuitsMeta" + }, + "ConvertCircuitToNetlist": { + "src": "src/plugins/ConvertCircuitToNetlist", + "test": "test/plugins/ConvertCircuitToNetlist" } }, "seeds": { @@ -17,11 +21,22 @@ }, "project": { "src": "src/seeds/project" + }, + "test": { + "src": "src/seeds/test" } }, - "dependencies": { - "plugins": {}, - "seeds": {} + "routers": {} + }, + "dependencies": { + "plugins": {}, + "seeds": {}, + "routers": { + "BindingsDocs": { + "project": "webgme-bindings", + "path": "node_modules/webgme-bindings/src/routers/BindingsDocs", + "mount": "bindings-docs" + } } } } \ No newline at end of file