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 6dbb0a9..aad3ec6 100644 Binary files a/src/seeds/project/project.webgmex and b/src/seeds/project/project.webgmex differ diff --git a/src/seeds/tests/tests.webgmex b/src/seeds/tests/tests.webgmex new file mode 100644 index 0000000..269d7e2 Binary files /dev/null and b/src/seeds/tests/tests.webgmex differ 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