diff --git a/package-lock.json b/package-lock.json index 54d51ea..57855b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,6 @@ "@hyvor/hyvor-talk-react": "^1.0.2", "@mui/icons-material": "^7.3.1", "@mui/material": "^7.3.1", - "@yaffle/expression": "^0.0.47", - "algebrite": "^1.4.0", "bootstrap": "^5.3.8", "echarts": "^5.6.0", "echarts-for-react": "^3.0.2", @@ -23,7 +21,7 @@ "pyodide": "^0.28.2", "react": "^19.1.1", "react-dom": "^19.1.1", - "visiojs": "^0.0.7" + "visiojs": "file:/Users/will-personal/Documents/visiojs/package" }, "devDependencies": { "@eslint/js": "^9.34.0", @@ -44,6 +42,31 @@ "vitest": "^3.2.4" } }, + "../visiojs": { + "name": "@28raining/visiojs-monorepo", + "version": "1.0.0", + "workspaces": [ + "package", + "examples/*" + ], + "devDependencies": {} + }, + "../visiojs/package": { + "name": "visiojs", + "version": "0.0.8", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0" + }, + "devDependencies": { + "@eslint/js": "^9.22.0", + "eslint": "^9.26.0", + "jsdom": "^26.0.0", + "prettier": "^3.6.2", + "vite": "^6.3.5", + "vitest": "^3.1.1" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -2363,26 +2386,6 @@ "node": ">=10.0.0" } }, - "node_modules/@yaffle/bigdecimal": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/@yaffle/bigdecimal/-/bigdecimal-1.0.36.tgz", - "integrity": "sha512-ADLt8lRsO1OH45/nhuT8CQfJCGKOCgkxo1udoPz2K976DOpK2pzaTq/lIgSlVwQRnH7v+agqZYnYQFt7RX33Og==", - "license": "ISC" - }, - "node_modules/@yaffle/expression": { - "version": "0.0.47", - "resolved": "https://registry.npmjs.org/@yaffle/expression/-/expression-0.0.47.tgz", - "integrity": "sha512-VRrNERm+NmP/MNDkieRsY7j82w0gcLp3gNk/W3cymzdpmCDQxoBXjG9C3cKgY9ix4uhaW/vOMV1qdD8lEfceeA==", - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "@yaffle/bigdecimal": "^1.0.27", - "bigint-gcd": "^1.0.28", - "js-big-integer": "^3.0.25", - "quadraticsievefactorization": "^1.0.72", - "seedrandom": "^3.0.5" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2423,15 +2426,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/algebrite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/algebrite/-/algebrite-1.4.0.tgz", - "integrity": "sha512-YPqD1baZOjuRnvR6xwyGYDJ9OvcqkPLLuxFAxnTGYVnKadzA5PyoMXm8mA1pYCE2xloMSgcZkTL1cXkK5C87cg==", - "license": "MIT", - "dependencies": { - "big-integer": "^1.6.32" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2507,21 +2501,6 @@ "dev": true, "license": "MIT" }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "license": "Unlicense", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/bigint-gcd": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/bigint-gcd/-/bigint-gcd-1.0.46.tgz", - "integrity": "sha512-LS+5iwYDi5kHu4antUAYaM0uqVN3dA9bNzZvbZlsRofuB4TclaL93LYxF/dDxFmaVcmw5twkWE9aykMb0IVWsA==", - "license": "SEE LICENSE IN LICENSE" - }, "node_modules/bootstrap": { "version": "5.3.8", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", @@ -2726,15 +2705,6 @@ "dev": true, "license": "MIT" }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2794,407 +2764,6 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -3239,15 +2808,6 @@ "node": ">=8" } }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3817,18 +3377,6 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3865,15 +3413,6 @@ "node": ">=0.8.19" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3964,11 +3503,6 @@ "dev": true, "license": "ISC" }, - "node_modules/js-big-integer": { - "version": "3.0.26", - "resolved": "https://registry.npmjs.org/js-big-integer/-/js-big-integer-3.0.26.tgz", - "integrity": "sha512-+UhYeZAZYehN52VNRL0p7LNm/WKA3jKoXfDqRSTU931HxPM7U32jqq5URGD/3b4hHk+N3ZDdnbSwGr3CPBBEyQ==" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4450,11 +3984,6 @@ "node": ">=18" } }, - "node_modules/pollardsrho": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/pollardsrho/-/pollardsrho-1.0.9.tgz", - "integrity": "sha512-+I2kgzhZM59J+yWft/5aDT0JHkr+h6i1LeXsOluCal9bI4Ke9pdH/r+nvZeBK3JTSPFvv7fw6XU37NkKkF7HOw==" - }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -4584,14 +4113,6 @@ "node": ">=18.0.0" } }, - "node_modules/quadraticsievefactorization": { - "version": "1.0.78", - "resolved": "https://registry.npmjs.org/quadraticsievefactorization/-/quadraticsievefactorization-1.0.78.tgz", - "integrity": "sha512-L0nLnLg4zGFGuZyy4lAyw+WPUFFaN3ttuBDqw1NuCP8av/q84M2fdPBExOszXxc3Yr+SmbvmcgWHxGo8NKPGzw==", - "dependencies": { - "pollardsrho": "^1.0.5" - } - }, "node_modules/react": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", @@ -4684,12 +4205,6 @@ "node": ">=4" } }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, "node_modules/rollup": { "version": "4.48.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.1.tgz", @@ -4771,30 +4286,12 @@ "node": ">= 12" } }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, - "node_modules/seedrandom": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", - "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", - "license": "MIT" - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5153,13 +4650,8 @@ } }, "node_modules/visiojs": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/visiojs/-/visiojs-0.0.7.tgz", - "integrity": "sha512-h/bk6bs0UWmCszTqt+5UjlQnJcTkj84AJNfqgk5xoAg6SlwGBcCh3PU/jA7hYsmhBD676EzPRepljeMkVkLxvQ==", - "license": "MIT", - "dependencies": { - "d3": "^7.9.0" - } + "resolved": "../visiojs/package", + "link": true }, "node_modules/vite": { "version": "7.1.12", diff --git a/package.json b/package.json index 5a05983..94d83d9 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "pyodide": "^0.28.2", "react": "^19.1.1", "react-dom": "^19.1.1", - "visiojs": "^0.0.7" + "visiojs": "file:/Users/will-personal/Documents/visiojs/package" }, "devDependencies": { "@eslint/js": "^9.34.0", diff --git a/src/App.jsx b/src/App.jsx index ca7f1fb..ea6dc81 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,6 +10,7 @@ import { FreqAdjusters } from "./FreqAdjusters.jsx"; // import Grid from "@mui/material/Grid"; import { units, formatMathML } from "./common.js"; import { calcBilinear, new_calculate_tf } from "./new_solveMNA.js"; +import { buildComponentValuesForSympy } from "./sympyValues.js"; import { NavBar } from "./NavBar.jsx"; import { ChoseTF } from "./ChoseTF.jsx"; @@ -25,18 +26,19 @@ import SnackbarContent from "@mui/material/SnackbarContent"; import Button from "@mui/material/Button"; import Grid from "@mui/material/Grid"; +/** Shape indices in initialSchematic: inductor 3, resistor 4, capacitor 5 */ const initialComponents = { - L0: { + 3: { type: "inductor", value: 1, unit: "uH", }, - R0: { + 4: { type: "resistor", value: 10, unit: "KΩ", }, - C0: { + 5: { type: "capacitor", value: 10, unit: "fF", @@ -66,8 +68,13 @@ function stateFromURL() { if (componentsParam) { urlContainsState = true; // Set the flag if components are present in the URL modifiedComponents = componentsParam.split("__").reduce((acc, comp) => { - const [key, type, value, unit] = comp.split("_"); - acc[key] = { type, value: parseFloat(value), unit }; + const parts = comp.split("_"); + if (parts.length < 4) return acc; + const id = parts[0]; + const type = parts[1]; + const value = parts[2]; + const unit = parts.slice(3).join("_"); + acc[id] = { type, value: parseFloat(value), unit }; return acc; }, {}); // setComponentValues(componentsArray); @@ -98,9 +105,12 @@ function compToURL(key, value) { } function App() { + const schematicRef = useRef(null); const [nodes, setNodes] = useState([]); const [fullyConnectedComponents, setFullyConnectedComponents] = useState({}); + /** All placed shapes with connectors (from createNodeMap), including off the vin subgraph — used for names & duplicate-value UI */ + const [schematicComponents, setSchematicComponents] = useState({}); const [results, setResults] = useState({ text: "", mathML: "", complexResponse: "", solver: null, probeName: "", drivers: [] }); const [numericResults, setNumericResults] = useState({ numericML: "", numericText: "" }); const [bilinearResults, setBilinearResults] = useState({ bilinearML: "", bilinearText: "" }); @@ -191,8 +201,8 @@ function App() { ); } - const componentValuesSolved = {}; - for (const key in componentValues) componentValuesSolved[key] = componentValues[key].value * units[componentValues[key].type][componentValues[key].unit]; + // Numeric values keyed by SymPy symbol name for algebraic/numeric TF (canonical row per shared name) + const componentValuesSolved = buildComponentValuesForSympy(componentValues, schematicComponents); const [freq_new, setFreqNew] = useState(null); const [mag_new, setMagNew] = useState(null); @@ -217,8 +227,7 @@ function App() { return; } const fRange = { fmin: settings.fmin * units.frequency[settings.fminUnit], fmax: settings.fmax * units.frequency[settings.fmaxUnit] }; - const componentValuesSolved2 = {}; - for (const key in componentValues) componentValuesSolved2[key] = componentValues[key].value * units[componentValues[key].type][componentValues[key].unit]; + const componentValuesSolved2 = buildComponentValuesForSympy(componentValues, schematicComponents); const { freq_new, mag_new, phase_new, numericML, numericText } = await new_calculate_tf( results.solver, fRange, @@ -243,7 +252,7 @@ function App() { clearTimeout(debounceTimerRef.current); } }; - }, [results, settings, componentValues]); + }, [results, settings, componentValues, schematicComponents]); function stateToURL() { const url = new URL(window.location.href); @@ -302,16 +311,18 @@ function App() {
- +
diff --git a/src/ChoseTF.jsx b/src/ChoseTF.jsx index cf89da2..6fc4132 100644 --- a/src/ChoseTF.jsx +++ b/src/ChoseTF.jsx @@ -7,6 +7,12 @@ import CircularProgress from "@mui/material/CircularProgress"; import { initPyodideAndSympy } from "./pyodideLoader"; import { emptyResults, formatMathML } from "./common.js"; // Import the emptyResults object +function probeFractionLabel(fcc, p) { + if (!p.includes("-")) return fcc[p]?.sympyName ?? p; + const [a, b] = p.split("-"); + return `${fcc[a]?.sympyName ?? a}-${fcc[b]?.sympyName ?? b}`; +} + export function ChoseTF({ setResults, nodes, fullyConnectedComponents, componentValuesSolved, setUnsolveSnackbar }) { const [loading, setLoading] = useState(false); const [calculating, setCalculating] = useState(false); @@ -47,13 +53,8 @@ export function ChoseTF({ setResults, nodes, fullyConnectedComponents, component probes.push(`${vprobes[1]}-${vprobes[0]}`); } - // add a value field based on if user chose algebraic or numeric - const valueForAlgebra = {}; - for (const c in fullyConnectedComponents) { - if (c in componentValuesSolved) valueForAlgebra[c] = componentValuesSolved[c]; - // else valueForAlgebra[c] = c; - } - // console.log("componentValuesSolved", componentValuesSolved, fullyConnectedComponents, algebraic); + // componentValuesSolved is keyed by SymPy symbol; passed through to build_and_solve_mna / subs + // console.log("componentValuesSolved", componentValuesSolved, fullyConnectedComponents); return ( {drivers.length == 0 || probes.length == 0 ? ( @@ -68,6 +69,7 @@ export function ChoseTF({ setResults, nodes, fullyConnectedComponents, component {probes.map((p) => { const int_probes = p.includes("-") ? p.split("-") : [p]; + const label = probeFractionLabel(fullyConnectedComponents, p); return ( @@ -80,23 +82,21 @@ export function ChoseTF({ setResults, nodes, fullyConnectedComponents, component sx={{ py: 1, justifyContent: "center", fontSize: "1.4em" }} onClick={async () => { setCalculating(true); - setResults({ ...emptyResults }); // Reset results to empty - //this console log is for collecting data for testing - // console.log(nodes.length, int_probes, fullyConnectedComponents, valueForAlgebra, loadedPyo); - const [textResult, mathml] = await build_and_solve_mna(nodes.length, int_probes, fullyConnectedComponents, valueForAlgebra, loadedPyo); + setResults({ ...emptyResults }); + const [textResult, mathml] = await build_and_solve_mna(nodes.length, int_probes, fullyConnectedComponents, componentValuesSolved, loadedPyo); if (textResult === "" && mathml === "") { setUnsolveSnackbar((x) => { if (!x) return true; else return x; }); } - const editedMathMl = formatMathML(mathml, p, drivers); + const editedMathMl = formatMathML(mathml, label, drivers); setResults({ text: textResult, mathML: editedMathMl, complexResponse: "", solver: loadedPyo, - probeName: p, + probeName: label, drivers: drivers, }); setCalculating(false); @@ -110,7 +110,7 @@ export function ChoseTF({ setResults, nodes, fullyConnectedComponents, component ) : ( - {p} + {label} {drivers[0] == "vin" ? ( V diff --git a/src/ComponentAdjuster.jsx b/src/ComponentAdjuster.jsx index dbc6e90..c8f3bfc 100644 --- a/src/ComponentAdjuster.jsx +++ b/src/ComponentAdjuster.jsx @@ -7,12 +7,24 @@ import Select from "@mui/material/Select"; import MenuItem from "@mui/material/MenuItem"; import Card from "@mui/material/Card"; -import CardContent from "@mui/material/CardContent"; import FormControl from "@mui/material/FormControl"; +import { useEffect, useState } from "react"; import { units } from "./common"; +import { sympyNameGroupsForValueTypes, isCanonicalValueRow } from "./sympyValues.js"; + +function LabelField({ shapeId, sympyName, onCommit }) { + const [val, setVal] = useState(sympyName); + useEffect(() => { + setVal(sympyName); + }, [sympyName]); + + return setVal(e.target.value)} onBlur={() => onCommit(shapeId, val)} />; +} + +export function ComponentAdjuster({ componentValues, setComponentValues, schematicComponents, schematicRef }) { + const groups = sympyNameGroupsForValueTypes(schematicComponents); -export function ComponentAdjuster({ componentValues, setComponentValues }) { function handleValueChange(name, value) { setComponentValues((prevValues) => ({ ...prevValues, @@ -26,36 +38,55 @@ export function ComponentAdjuster({ componentValues, setComponentValues }) { })); } + function commitLabel(shapeId, raw) { + schematicRef?.current?.setShapeLabel(shapeId, raw); + } + + const sortedIds = Object.keys(componentValues).sort((a, b) => Number(a) - Number(b)); + return ( - {Object.keys(componentValues).map((key) => ( - - - - - {key} - - handleValueChange(key, e.target.value)} - // fullWidth - /> - - - - - - - ))} + {sortedIds.map((key) => { + const el = schematicComponents[key]; + const sympyName = el?.sympyName ?? ""; + const showValue = isCanonicalValueRow(key, schematicComponents, groups); + const dupList = el ? groups[el.sympyName] : null; + const sharedHint = dupList && dupList.length > 1 && !showValue; + + return ( + + + + + + + {sharedHint ? ( + + Same symbol as {el.sympyName}; value is set there. + + ) : null} + {showValue ? (<> + handleValueChange(key, e.target.value)} + // fullWidth + /> + + ) : null} + + + + ); + })} ); } diff --git a/src/VisioJSSchematic.jsx b/src/VisioJSSchematic.jsx index bbcd289..9b089a0 100644 --- a/src/VisioJSSchematic.jsx +++ b/src/VisioJSSchematic.jsx @@ -1,17 +1,17 @@ // import { visiojs } from "/visiojs/package/dist/visiojs.js"; import visiojs from "visiojs"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useRef } from "react"; import { createNodeMap } from "./visiojs_to_matrix.js"; import Button from "@mui/material/Button"; import IconButton from "@mui/material/IconButton"; import DeleteIcon from "@mui/icons-material/Delete"; import UndoIcon from "@mui/icons-material/Undo"; import RedoIcon from "@mui/icons-material/Redo"; -import Box from "@mui/material/Box"; import Tooltip from "@mui/material/Tooltip"; import Grid from "@mui/material/Grid"; import { addShapes, emptyResults } from "./common.js"; +import { nextDefaultLabel, commitLabelForType } from "./componentNaming.js"; const shapesWithLabels = { resistor: "R", @@ -41,152 +41,187 @@ Object.keys(shapesWithLabels).forEach((key) => { initialLabels[key] = `${shapesWithLabels[key]}0`; }); -function calculateNextIndex(components, type, prefix) { - if (!components) return initialLabels[type]; - if (Object.keys(components).length == 0) return initialLabels[type]; - // console.log("calculateNextIndex", type, prefix, components); - const existingLabels = Object.keys(components) - .filter((k) => components[k].type == type) - .map((c) => Number(c.slice(1))); - // console.log("existingLabels", type, existingLabels, Math.max(...existingLabels)+1); - return `${prefix}${existingLabels.length == 0 ? 0 : Math.max(...existingLabels) + 1}`; +function calculateNextLabel(components, type) { + const names = Object.values(components) + .filter((c) => c.type === type) + .map((c) => c.sympyName); + return nextDefaultLabel(type, names) || initialLabels[type]; } -export function VisioJSSchematic({ setResults, setNodes, setComponentValues, setFullyConnectedComponents, history, setHistory }) { - // const initializedRef = useRef(false); - // const [history, setHistory] = useState({ pointer: 0, state: [] }); +export function VisioJSSchematic({ + schematicApiRef, + setResults, + setNodes, + setComponentValues, + setFullyConnectedComponents, + setSchematicComponents, + history, + setHistory, +}) { const [nextComponent, setNextComponent] = useState(initialLabels); const [vjs, setVjs] = useState(null); const [oldComponents, setComponents] = useState({}); + const historyRef = useRef(history); + const vjsRef = useRef(null); + + useEffect(() => { + historyRef.current = history; + }, [history]); + + useEffect(() => { + vjsRef.current = vjs; + }, [vjs]); const numUndos = 15; - const regenerateNodeMaps = useCallback( - (newState) => { - // console.log("newState", newState); - const { nodeMap, components, fullyConnectedComponents } = createNodeMap(newState, addShapes); + function regenerateNodeMaps(newState) { + const { nodeMap, components, fullyConnectedComponents } = createNodeMap(newState, addShapes); + for (const [key, value] of Object.entries(components)) { + if (!(key in oldComponents) && value.type in componentDefaults) components[key] = { ...value, ...componentDefaults[value.type] }; + } + setComponentValues((oldValues) => { + const newValues = { ...oldValues }; for (const [key, value] of Object.entries(components)) { - if (!(key in oldComponents) && value.type in componentDefaults) components[key] = { ...value, ...componentDefaults[value.type] }; + if (!(key in newValues) && value.type in componentDefaults) newValues[key] = { type: value.type, ...componentDefaults[value.type] }; } - setComponentValues((oldValues) => { - const newValues = { ...oldValues }; - for (const [key, value] of Object.entries(components)) { - if (!(key in newValues) && value.type in componentDefaults) newValues[key] = { type: value.type, ...componentDefaults[value.type] }; - } - for (const key in newValues) { - if (!(key in components)) delete newValues[key]; //remove components that are no longer present - } - if (JSON.stringify(oldValues) == JSON.stringify(newValues)) return oldValues; - return newValues; - }); - - //if the state didn't change then return the same nodeMap to prevent re-rendering - setFullyConnectedComponents((old) => { - if (JSON.stringify(old) == JSON.stringify(fullyConnectedComponents)) return old; - return fullyConnectedComponents; - }); - // setFullyConnectedComponents(fullyConnectedComponents) - //CHANGE - 8th Feb 2026 - now if schematic changes at all, reset the results to empty. Otherwise hit bug where new components are added and Sympy can't handle it - setNodes((/*old*/) => { - // if (JSON.stringify(old) == JSON.stringify(nodeMap)) return old; - setResults({ ...emptyResults }); - return nodeMap; - }); - - setComponents(components); - //the keys of components are the names of the components. Find the next available name for each component type - const tempNewComponent = {}; - for (const key in shapesWithLabels) tempNewComponent[key] = calculateNextIndex(components, key, shapesWithLabels[key]); - setNextComponent(tempNewComponent); - }, - [oldComponents, setComponentValues, setFullyConnectedComponents, setNodes, setResults], - ); + for (const key in newValues) { + if (!(key in components)) delete newValues[key]; //remove components that are no longer present + } + if (JSON.stringify(oldValues) == JSON.stringify(newValues)) return oldValues; + return newValues; + }); - const trackHistory = useCallback( - (newState) => { - setHistory((old_h) => { - const deepCopyState = JSON.parse(JSON.stringify(newState)); - const h = { ...old_h }; - //there was an undo, then a new state was created. Throwing away the future history - if (h.pointer < h.state.length - 1) h.state = h.state.slice(0, h.pointer + 1); - if (h.state.length == numUndos) h.state = [...h.state.slice(1), deepCopyState]; - else h.state = [...h.state, deepCopyState]; - h.pointer = h.state.length - 1; - return h; - }); - regenerateNodeMaps(newState); - }, - [regenerateNodeMaps, setHistory], - ); + setFullyConnectedComponents((old) => { + if (JSON.stringify(old) == JSON.stringify(fullyConnectedComponents)) return old; + return fullyConnectedComponents; + }); + setSchematicComponents((old) => { + const next = { ...components }; + if (JSON.stringify(old) == JSON.stringify(next)) return old; + return next; + }); + setNodes((/*old*/) => { + setResults({ ...emptyResults }); + return nodeMap; + }); + + setComponents(components); + const tempNewComponent = {}; + for (const key in shapesWithLabels) tempNewComponent[key] = calculateNextLabel(components, key); + setNextComponent(tempNewComponent); + } + + const regenerateNodeMapsRef = useRef(regenerateNodeMaps); + regenerateNodeMapsRef.current = regenerateNodeMaps; - const undo = useCallback(() => { - //when undo is called form useeffect it receives stale state. Therefore, all state accessing is done inside the setHistory function + function trackHistory(newState) { setHistory((old_h) => { - if (old_h.pointer == 0) return old_h; //no more undos - vjs.redraw(old_h.state[old_h.pointer - 1]); + const deepCopyState = JSON.parse(JSON.stringify(newState)); const h = { ...old_h }; - h.pointer = h.pointer - 1; + if (h.pointer < h.state.length - 1) h.state = h.state.slice(0, h.pointer + 1); + if (h.state.length == numUndos) h.state = [...h.state.slice(1), deepCopyState]; + else h.state = [...h.state, deepCopyState]; + h.pointer = h.state.length - 1; return h; }); - regenerateNodeMaps(history.state[history.pointer - 1]); - }, [regenerateNodeMaps, setHistory, history, vjs]); + } - const redo = useCallback(() => { + useEffect(() => { + const newState = history.state[history.pointer]; + if (!newState) return; + regenerateNodeMapsRef.current(newState); + }, [history]); + + function undo() { setHistory((old_h) => { - if (old_h.pointer >= old_h.state.length - 1) return old_h; //no more redos - vjs.redraw(old_h.state[old_h.pointer + 1]); - const h = { ...old_h }; - h.pointer = h.pointer + 1; - return h; + if (old_h.pointer == 0) return old_h; + const instance = vjsRef.current; + const nextState = old_h.state[old_h.pointer - 1]; + if (instance) instance.applyState(nextState, { source: "programmatic" }); + return { ...old_h, pointer: old_h.pointer - 1 }; + }); + } + + function redo() { + setHistory((old_h) => { + if (old_h.pointer >= old_h.state.length - 1) return old_h; + const instance = vjsRef.current; + const nextState = old_h.state[old_h.pointer + 1]; + if (instance) instance.applyState(nextState, { source: "programmatic" }); + return { ...old_h, pointer: old_h.pointer + 1 }; }); - regenerateNodeMaps(history.state[history.pointer + 1]); - }, [regenerateNodeMaps, setHistory, history, vjs]); + } + + const undoRef = useRef(undo); + const redoRef = useRef(redo); + undoRef.current = undo; + redoRef.current = redo; useEffect(() => { - if (!vjs) { - var newVjs = visiojs({ - initialState: history.state[0], - stateChanged: trackHistory, - }); - setVjs(newVjs); - } - }, [trackHistory, history, vjs]); + if (!schematicApiRef) return; + schematicApiRef.current = { + setShapeLabel(shapeId, rawText) { + const h = historyRef.current; + const base = h.state[h.pointer]; + if (!base) return; + const newState = JSON.parse(JSON.stringify(base)); + const shape = newState.shapes[shapeId]; + if (!shape?.label) return; + const shapeType = shape.image.split(".")[0]; + shape.label.text = commitLabelForType(shapeType, rawText, shapeId); + const instance = vjsRef.current; + if (!instance) return; + instance.applyState(newState, { source: "user" }); + }, + }; + return () => { + schematicApiRef.current = null; + }; + }, [schematicApiRef, vjs]); + + useEffect(() => { + const newVjs = visiojs({ + initialState: history.state[0], + stateChanged: trackHistory, + }); + setVjs(newVjs); + }, []); useEffect(() => { if (vjs) vjs.init(); }, [vjs]); - //capture keypresses useEffect(() => { const handleKeyDown = (e) => { const isUndo = (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z" && !e.shiftKey; const isRedo = (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z" && e.shiftKey; if (isUndo) { - console.log("undo"); - e.preventDefault(); // optional: prevent default browser undo - undo(); + e.preventDefault(); + undoRef.current(); } else if (isRedo) { - e.preventDefault(); // optional: prevent default browser redo - redo(); + e.preventDefault(); + redoRef.current(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [undo, redo]); + }, []); + const hasVin = Object.values(oldComponents).some((c) => c.type === "vin"); + const hasIin = Object.values(oldComponents).some((c) => c.type === "iin"); const allowedToAdd = {}; - allowedToAdd["vin"] = !("iin" in oldComponents || "vin" in oldComponents); - allowedToAdd["vprobe"] = Object.keys(oldComponents).filter((k) => oldComponents[k].type == "vprobe").length < 2; - // console.log("allowedToAdd", allowedToAdd); + allowedToAdd["vin"] = !hasVin && !hasIin; + allowedToAdd["iin"] = !hasVin && !hasIin; + allowedToAdd["vprobe"] = Object.values(oldComponents).filter((c) => c.type == "vprobe").length < 2; return ( {Object.keys(addShapes).map((key) => { - const shape = addShapes[key]; - if ("label" in shape) shape.label.text = nextComponent[key]; + const shape = { ...addShapes[key] }; + if ("label" in shape) shape.label = { ...shape.label, text: nextComponent[key] }; return ( { window.dragData = shape; diff --git a/src/componentNaming.js b/src/componentNaming.js new file mode 100644 index 0000000..70a22ab --- /dev/null +++ b/src/componentNaming.js @@ -0,0 +1,61 @@ +/** Single-letter prefix for each component type (SymPy symbol names). */ +export const typeToPrefix = { + resistor: "R", + capacitor: "C", + inductor: "L", + vcvs: "A", + vcis: "G", + opamp: "U", + vprobe: "X", + iprobe: "Y", +}; + +/** Strip characters that are not valid in SymPy identifiers we generate (ASCII letter, digit, underscore). */ +export function sanitizeSymPyIdentifierTail(raw) { + if (!raw) return ""; + return String(raw).replace(/[^A-Za-z0-9_]/g, ""); +} + +/** + * Ensure label matches type prefix and is safe for SymPy. + * @param {string} type - component type (e.g. resistor) + * @param {string} input - user or label text + * @param {number} [shapeId] - fallback suffix if body empty + */ +export function commitLabelForType(type, input, shapeId = 0) { + const prefix = typeToPrefix[type]; + if (!prefix) { + if (type === "vin") return "vin"; + if (type === "iin") return "iin"; + if (type === "gnd") return "gnd"; + return ""; + } + let s = sanitizeSymPyIdentifierTail(input ?? ""); + if (s.length === 0) return prefix + String(shapeId); + if (!s.startsWith(prefix)) { + s = prefix + s.replace(/^[A-Za-z_]+/, ""); + } + let tail = sanitizeSymPyIdentifierTail(s.slice(prefix.length)); + if (tail.length === 0) tail = String(shapeId); + return prefix + tail; +} + +/** + * Default next label R0, R1, … from existing sympy names of this type. + */ +export function nextDefaultLabel(type, existingSympyNames) { + const prefix = typeToPrefix[type]; + if (!prefix) return ""; + const nums = []; + for (const name of existingSympyNames) { + if (!name || !name.startsWith(prefix)) continue; + const tail = name.slice(prefix.length); + if (/^\d+$/.test(tail)) nums.push(Number(tail)); + } + const next = nums.length === 0 ? 0 : Math.max(...nums) + 1; + return prefix + next; +} + +export function typesWithValueUnit() { + return ["resistor", "capacitor", "inductor", "vcvs", "vcis"]; +} diff --git a/src/new_solveMNA.js b/src/new_solveMNA.js index 5eff5cb..79c66cc 100644 --- a/src/new_solveMNA.js +++ b/src/new_solveMNA.js @@ -6,9 +6,22 @@ function removeFenced(mathml) { // return mathml; return mathml.replaceAll(//g, "(").replaceAll(/<\/mfenced>/g, ")"); // replace with { } -async function solveWithSymPy(matrixStr, mnaMatrix, elementMap, resIndex, resIndex2, componentValuesSolved, pyodide) { + +/** SymPy symbol names used in the MNA matrix (RLC + VCVS + VCIS). */ +function collectMnaSymbolNames(elementMap) { + const set = new Set(); + for (const el of Object.values(elementMap)) { + if (["resistor", "capacitor", "inductor", "vcvs", "vcis"].includes(el.type) && el.sympyName) { + set.add(el.sympyName); + } + } + return [...set]; +} + +async function solveWithSymPy(matrixStr, mnaMatrix, symbolNames, resIndex, resIndex2, componentValuesSolved, pyodide) { // const pyodide = await initPyodideAndSympy(); - const symbols = `${[...Object.keys(elementMap), "s"].join(", ")} = symbols("${[...Object.keys(elementMap), "s"].join(" ")}", positive=True, real=True)`; + const symList = [...symbolNames, "s"]; + const symbols = `${symList.join(", ")} = symbols("${symList.join(" ")}", positive=True, real=True)`; const matrixStrPow = matrixStr.replaceAll("^", "**"); const sympyString = ` @@ -40,13 +53,17 @@ str(result_simplified), mathml(result_simplified, printer='presentation') } } +function elementById(elementMap, id) { + return elementMap[String(id)] ?? elementMap[id]; +} + // all these equations are based on // https://lpsa.swarthmore.edu/Systems/Electrical/mna/MNAAll.html export async function build_and_solve_mna(numNodes, chosenPlot, fullyConnectedComponents, componentValuesSolved, pyodide) { var i, vinNode, iinNode; //Are we plotting current or voltage? - const plottingI = fullyConnectedComponents[chosenPlot[0]].type === "iprobe"; + const plottingI = elementById(fullyConnectedComponents, chosenPlot[0]).type === "iprobe"; const elementMap = fullyConnectedComponents; const numIprb = Object.values(elementMap).filter((el) => el.type === "iprobe").length; @@ -58,15 +75,17 @@ export async function build_and_solve_mna(numNodes, chosenPlot, fullyConnectedCo // console.log("nodeArray 2", nodeArray); // console.log("elementMap", elementMap); - if ("vin" in elementMap) { - vinNode = elementMap["vin"].ports[0]; - } else if ("iin" in elementMap) { - iinNode = elementMap["iin"].ports[0]; + const vinEl = Object.values(elementMap).find((e) => e.type === "vin"); + const iinEl = Object.values(elementMap).find((e) => e.type === "iin"); + if (vinEl) { + vinNode = vinEl.ports[0]; + } else if (iinEl) { + iinNode = iinEl.ports[0]; } else { console.log("no iin or vin"); return; } - const iinOrVin = "vin" in elementMap ? "vin" : "iin"; + const iinOrVin = vinEl ? "vin" : "iin"; const extraRow = iinOrVin == "vin" ? 1 : 0; var mnaMatrix = new Array(numNodes + extraRow + numActives + numIprb); @@ -74,12 +93,12 @@ export async function build_and_solve_mna(numNodes, chosenPlot, fullyConnectedCo // Step 1 - loop thru elementMap and start adding things to the MNA var laplaceElement; - for (const name in elementMap) { - const el = elementMap[name]; + for (const el of Object.values(elementMap)) { if (["inductor", "capacitor", "resistor"].includes(el.type)) { - if (el.type == "resistor") laplaceElement = name; - else if (el.type == "inductor") laplaceElement = `(s*${name})`; - else laplaceElement = `1/(s*${name})`; + const sym = el.sympyName; + if (el.type == "resistor") laplaceElement = sym; + else if (el.type == "inductor") laplaceElement = `(s*${sym})`; + else laplaceElement = `1/(s*${sym})`; //2.1 in the diagonal is the sum of all impedances connected to that node for (const p of el.ports) if (p !== null) mnaMatrix[p][p] += `+${laplaceElement}^(-1)`; @@ -112,26 +131,26 @@ export async function build_and_solve_mna(numNodes, chosenPlot, fullyConnectedCo // current probes are implemented as 0V voltage sources, because the i thru voltage sources ends up in the resulting matric var opAmpCounter = 0; var iprbCounter = 0; - for (const name in elementMap) { - const el = elementMap[name]; + for (const el of Object.values(elementMap)) { + const sym = el.sympyName; if (el.type === "opamp") { if (el.ports[0] != null) mnaMatrix[numNodes + extraRow + opAmpCounter][el.ports[0]] = "1"; if (el.ports[1] != null) mnaMatrix[numNodes + extraRow + opAmpCounter][el.ports[1]] = "-1"; if (el.ports[2] != null) mnaMatrix[el.ports[2]][numNodes + extraRow + opAmpCounter] = "1"; opAmpCounter++; } else if (el.type === "vcvs") { - if (el.ports[0] != null) mnaMatrix[numNodes + extraRow + opAmpCounter][el.ports[0]] = `+${name}`; - if (el.ports[1] != null) mnaMatrix[numNodes + extraRow + opAmpCounter][el.ports[1]] = `-${name}`; + if (el.ports[0] != null) mnaMatrix[numNodes + extraRow + opAmpCounter][el.ports[0]] = `+${sym}`; + if (el.ports[1] != null) mnaMatrix[numNodes + extraRow + opAmpCounter][el.ports[1]] = `-${sym}`; if (el.ports[2] != null) mnaMatrix[numNodes + extraRow + opAmpCounter][el.ports[2]] += `+1`; if (el.ports[3] != null) mnaMatrix[numNodes + extraRow + opAmpCounter][el.ports[3]] += `-1`; if (el.ports[2] != null) mnaMatrix[el.ports[2]][numNodes + extraRow + opAmpCounter] = "1"; if (el.ports[3] != null) mnaMatrix[el.ports[3]][numNodes + extraRow + opAmpCounter] = "-1"; opAmpCounter++; } else if (el.type === "vcis") { - if (el.ports[2] != null && el.ports[0] != null) mnaMatrix[el.ports[2]][el.ports[0]] += `-${name}`; - if (el.ports[2] != null && el.ports[1] != null) mnaMatrix[el.ports[2]][el.ports[1]] += `+${name}`; - if (el.ports[3] != null && el.ports[0] != null) mnaMatrix[el.ports[3]][el.ports[0]] += `+${name}`; - if (el.ports[3] != null && el.ports[1] != null) mnaMatrix[el.ports[3]][el.ports[1]] += `-${name}`; + if (el.ports[2] != null && el.ports[0] != null) mnaMatrix[el.ports[2]][el.ports[0]] += `-${sym}`; + if (el.ports[2] != null && el.ports[1] != null) mnaMatrix[el.ports[2]][el.ports[1]] += `+${sym}`; + if (el.ports[3] != null && el.ports[0] != null) mnaMatrix[el.ports[3]][el.ports[0]] += `+${sym}`; + if (el.ports[3] != null && el.ports[1] != null) mnaMatrix[el.ports[3]][el.ports[1]] += `-${sym}`; } else if (el.type === "iprobe") { if (el.ports[0] != null) mnaMatrix[numNodes + extraRow + numActives + iprbCounter][el.ports[0]] = "1"; if (el.ports[0] != null) mnaMatrix[el.ports[0]][numNodes + extraRow + numActives + iprbCounter] = "1"; @@ -147,25 +166,27 @@ export async function build_and_solve_mna(numNodes, chosenPlot, fullyConnectedCo } const nerdStr = nerdStrArr.join(","); + const symbolNames = collectMnaSymbolNames(elementMap); + var resIndex = []; var resIndex2 = []; if (plottingI) { if (iinOrVin == "vin") resIndex.push(mnaMatrix.length, numNodes + 1); else resIndex.push(mnaMatrix.length, iinNode + 1); } else { - const voutNode = elementMap[chosenPlot[0]].ports[0]; + const voutNode = elementById(elementMap, chosenPlot[0]).ports[0]; if (iinOrVin == "vin") resIndex.push(voutNode + 1, numNodes + 1); else resIndex.push(voutNode + 1, iinNode + 1); if (chosenPlot.length == 2) { //are we plotting V or deltaV? - const voutNode2 = elementMap[chosenPlot[1]].ports[0]; + const voutNode2 = elementById(elementMap, chosenPlot[1]).ports[0]; if (iinOrVin == "vin") resIndex2.push(voutNode2 + 1, numNodes + 1); else resIndex2.push(voutNode2 + 1, iinNode + 1); } } var textResult, mathml; - [textResult, mathml] = await solveWithSymPy(nerdStr, mnaMatrix, elementMap, resIndex, resIndex2, componentValuesSolved, pyodide); + [textResult, mathml] = await solveWithSymPy(nerdStr, mnaMatrix, symbolNames, resIndex, resIndex2, componentValuesSolved, pyodide); return [textResult, mathml]; } @@ -197,9 +218,15 @@ export async function new_calculate_tf(pyodide, fRange, numSteps, componentValue try { // Use sympy to calculate magnitudes and phases for all frequencies and numeric representation // Optimized: Use lambdify to create a fast numeric function instead of slow evalf() calls + // Substitute via symbol names: a Python dict `{C0: 1}` treats C0 as a variable (NameError if + // that symbol was never created in a prior solve). JSON + free_symbols only touches symbols + // that actually appear in result_simplified (e.g. omits disconnected parts still in UI values). + const valuesJson = JSON.stringify(componentValuesSolved); const sympyString = ` - -result_numeric = result_simplified.subs(${JSON.stringify(componentValuesSolved).replaceAll('"', "")}) +import json +_name_vals = json.loads(${JSON.stringify(valuesJson)}) +_subs = {sym: _name_vals[sym.name] for sym in result_simplified.free_symbols if sym.name in _name_vals} +result_numeric = result_simplified.subs(_subs) result_numeric_simplified = simplify(result_numeric) # Calculate numeric MathML and text representation diff --git a/src/sympyValues.js b/src/sympyValues.js new file mode 100644 index 0000000..5c4858d --- /dev/null +++ b/src/sympyValues.js @@ -0,0 +1,44 @@ +import { units } from "./common.js"; +import { typesWithValueUnit } from "./componentNaming.js"; + +/** Groups sympy names to sorted numeric shape ids (value-bearing types only). */ +export function sympyNameGroupsForValueTypes(componentMap) { + const valueTypes = new Set(typesWithValueUnit()); + const bySym = {}; + for (const id of Object.keys(componentMap)) { + const el = componentMap[id]; + if (!el || !valueTypes.has(el.type)) continue; + const n = el.sympyName; + if (!bySym[n]) bySym[n] = []; + bySym[n].push(Number(id)); + } + for (const k of Object.keys(bySym)) { + bySym[k].sort((a, b) => a - b); + } + return bySym; +} + +/** True if this id should show value/unit (canonical row for shared sympy name). */ +export function isCanonicalValueRow(id, componentMap, groups) { + const el = componentMap[String(id)]; + if (!el) return true; + const valueTypes = new Set(typesWithValueUnit()); + if (!valueTypes.has(el.type)) return true; + const g = groups[el.sympyName]; + if (!g || g.length <= 1) return true; + return Number(id) === g[0]; +} + +/** One numeric value per SymPy symbol for subs() / numeric TF. */ +export function buildComponentValuesForSympy(componentValues, componentMap) { + const groups = sympyNameGroupsForValueTypes(componentMap); + const solved = {}; + for (const sym of Object.keys(groups)) { + const canon = String(groups[sym][0]); + const row = componentValues[canon]; + if (row) { + solved[sym] = row.value * units[row.type][row.unit]; + } + } + return solved; +} diff --git a/src/visiojs_to_matrix.js b/src/visiojs_to_matrix.js index 3947e60..3c260ad 100644 --- a/src/visiojs_to_matrix.js +++ b/src/visiojs_to_matrix.js @@ -1,11 +1,14 @@ -function elementMapper(s) { +import { commitLabelForType } from "./componentNaming.js"; + +function elementMapper(s, shapeIndex) { const shapeType = s.image.split(".")[0]; - const name = s.label ? s.label.text : ""; - if (s.image == "vin.svg") return { type: "vin", name: "vin" }; - if (s.image == "iin.svg") return { type: "iin", name: "iin" }; + if (s.image == "vin.svg") return { type: "vin", id: shapeIndex, sympyName: "vin" }; + if (s.image == "iin.svg") return { type: "iin", id: shapeIndex, sympyName: "iin" }; // else if (s.image == "vout.svg") return { type: "vprobe", name: "X0" }; - else if (s.image == "gnd.svg") return { type: "gnd", name: "gnd" }; - else return { type: shapeType, name: name }; + else if (s.image == "gnd.svg") return { type: "gnd", id: shapeIndex, sympyName: "gnd" }; + const rawLabel = s.label ? s.label.text : ""; + const sympyName = commitLabelForType(shapeType, rawLabel, shapeIndex); + return { type: shapeType, id: shapeIndex, sympyName }; } //convert from visiojs json to array of nodes which are connected to vin @@ -16,12 +19,18 @@ export function createNodeMap(newState, addShapes) { const fullyConnectedComponents = {}; var nodeMap = []; - newState.shapes.map((s) => { + newState.shapes.forEach((s, shapeIndex) => { if (s === null) return; //skip deleted shapes if (!("connectors" in s)) return; //skip shapes without connectors - var z = elementMapper(s); + var z = elementMapper(s, shapeIndex); + const key = String(z.id); //use portConnections because a cap can have 2 wires to 1 port. We need to check every port is connected... - components[z.name] = { type: z.type, portConnections: Array(s.connectors.length).fill(false) }; + components[key] = { + type: z.type, + sympyName: z.sympyName, + id: z.id, + portConnections: Array(s.connectors.length).fill(false), + }; }); // const numConnections = Object.fromEntries(components.map(s=>s.name)); @@ -33,13 +42,16 @@ export function createNodeMap(newState, addShapes) { var crushedNodes = []; for (const w of newState.wires) { if (w == null) continue; //skip deleted wires - const startEl = elementMapper(newState.shapes[w.start.shapeID]); - const endEl = elementMapper(newState.shapes[w.end.shapeID]); + const startShape = newState.shapes[w.start.shapeID]; + const endShape = newState.shapes[w.end.shapeID]; + if (startShape == null || endShape == null) continue; + const startEl = elementMapper(startShape, w.start.shapeID); + const endEl = elementMapper(endShape, w.end.shapeID); start = { ...startEl, port: w.start.connectorID }; end = { ...endEl, port: w.end.connectorID }; - components[startEl.name].portConnections[w.start.connectorID] = true; - components[endEl.name].portConnections[w.end.connectorID] = true; + components[String(startEl.id)].portConnections[w.start.connectorID] = true; + components[String(endEl.id)].portConnections[w.end.connectorID] = true; crushedNodes.push([start, end]); // if (startEl.type == "gnd" || endEl.type == "gnd") continue; //don't add ground nodes to the node map @@ -75,7 +87,7 @@ export function createNodeMap(newState, addShapes) { const node = crushedNodes[nodeIndex]; for (const conn2_index in node) { const conn2 = node[conn2_index]; - if (conn.name == conn2.name && conn.port == conn2.port) { + if (conn.id === conn2.id && conn.port === conn2.port) { // console.log("found a match", conn, conn2, "in node", node); //merge the nodes // if (nodeIndex != index) { @@ -92,15 +104,12 @@ export function createNodeMap(newState, addShapes) { } } while (wasChanged); - //delete the node containing gnd - outerLoop: for (const index in crushedNodes) { - const node = crushedNodes[index]; - for (const conn of node) { - if (conn.type == "gnd") { - // console.log("found gnd node", node, "at index", index); - crushedNodes.splice(index, 1); - break outerLoop; //break to avoid checking the rest of the connections - } + //delete any node containing gnd (was single-gnd removal; now all gnd supernodes) + for (let gi = crushedNodes.length - 1; gi >= 0; gi--) { + const node = crushedNodes[gi]; + if (node.some((conn) => conn.type == "gnd")) { + // console.log("found gnd node", node, "at index", gi); + crushedNodes.splice(gi, 1); } } @@ -111,7 +120,7 @@ export function createNodeMap(newState, addShapes) { for (let j = i + 1; j < crushedNodes[n].length; j++) { const conn2 = crushedNodes[n][j]; // console.log("conns", conn, conn2); - if (conn.name == conn2.name && conn.port == conn2.port) { + if (conn.id === conn2.id && conn.port === conn2.port) { // console.log("found a duplicate", conn, "in node", crushedNodes[n]); crushedNodes[n].splice(j, 1); //remove the duplicate j--; //decrement j to account for the removed element @@ -126,7 +135,7 @@ export function createNodeMap(newState, addShapes) { for (const [key, value] of Object.entries(components)) { if (value.portConnections.includes(false)) { for (const node in nodeMapPre) { - nodeMapPre[node] = nodeMapPre[node].filter((c) => c.name != key); + nodeMapPre[node] = nodeMapPre[node].filter((c) => String(c.id) != key); } } } @@ -144,7 +153,7 @@ export function createNodeMap(newState, addShapes) { // console.error("No vin or vout node found in the node map"); // } else { var connected_nodes = [vin_node]; - var connected_elements = nodeMapPre[vin_node].map((c) => c.name); + var connected_elements = nodeMapPre[vin_node].map((c) => c.id); var pushed; for (var i = 0; i < connected_elements.length; i++) { for (const node in nodeMapPre) { @@ -152,10 +161,10 @@ export function createNodeMap(newState, addShapes) { if (connected_nodes.includes(node)) continue; //skip already connected nodes pushed = false; for (const conn of nodeMapPre[node]) { - if (connected_elements.includes(conn.name)) { + if (connected_elements.includes(conn.id)) { if (!pushed) connected_nodes.push(node); pushed = true; - connected_elements.push(...nodeMapPre[node].map((c) => c.name).filter((c) => !connected_elements.includes(c))); + connected_elements.push(...nodeMapPre[node].map((c) => c.id).filter((c) => !connected_elements.includes(c))); // break; //break to avoid checking the rest of the connections } } @@ -174,14 +183,17 @@ export function createNodeMap(newState, addShapes) { nodeMap.forEach((node, i) => { // for (j = 0; j < nodeArray[i].length; j++) { node.forEach((comp) => { - // element = comp.name; - if (!(comp.name in fullyConnectedComponents)) { - fullyConnectedComponents[comp.name] = { + // element = comp.sympyName; + const elKey = String(comp.id); + if (!(elKey in fullyConnectedComponents)) { + fullyConnectedComponents[elKey] = { + id: comp.id, + sympyName: comp.sympyName, ports: Array(addShapes[comp.type].connectors.length).fill(null), type: comp.type, }; } - fullyConnectedComponents[comp.name].ports[comp.port] = i; + fullyConnectedComponents[elKey].ports[comp.port] = i; }); }); } diff --git a/tests/circuit.test.js b/tests/circuit.test.js index e0f4588..019d03b 100644 --- a/tests/circuit.test.js +++ b/tests/circuit.test.js @@ -7,33 +7,18 @@ const pyodide = await initPyodideAndSympy(); test("voltage in current probe - 1", async () => { const components = { - Y0: { - ports: [0, 1], - type: "iprobe", - }, - vin: { - ports: [0], - type: "vin", - }, - L0: { - ports: [0, 2], - type: "inductor", - }, - R0: { - ports: [1, 2], - type: "resistor", - }, - C0: { - ports: [2, null], - type: "capacitor", - }, + 1: { id: 1, ports: [0, 1], type: "iprobe", sympyName: "Y0" }, + 2: { id: 2, ports: [0], type: "vin", sympyName: "vin" }, + 3: { id: 3, ports: [0, 2], type: "inductor", sympyName: "L0" }, + 4: { id: 4, ports: [1, 2], type: "resistor", sympyName: "R0" }, + 5: { id: 5, ports: [2, null], type: "capacitor", sympyName: "C0" }, }; const values = { L0: 0.000001, R0: 10000, C0: 1.0000000000000002e-14, }; - const [textResult, _mathml] = await build_and_solve_mna(3, ["Y0"], components, values, pyodide); + const [textResult, _mathml] = await build_and_solve_mna(3, ["1"], components, values, pyodide); const { numericText } = await new_calculate_tf(pyodide, { fmin: 1, fmax: 1000 }, 10, values, () => {}); expect(textResult).toEqual("C0*L0*s**2/(C0*L0*R0*s**2 + L0*s + R0)"); // expect(_mathml).toEqual(null); @@ -41,35 +26,41 @@ test("voltage in current probe - 1", async () => { expect(numericText).toEqual("1.0e-20*s^2/(1.0e-16*s^2 + 1.0e-6*s + 10000)"); }); +/** Disconnected extra capacitor on schematic: values include C1 but MNA/symbol solve only has C0,L0,R0 — must not NameError on subs. */ +test("numeric TF ignores extra component values not in symbolic result", async () => { + const components = { + 1: { id: 1, ports: [0, 1], type: "iprobe", sympyName: "Y0" }, + 2: { id: 2, ports: [0], type: "vin", sympyName: "vin" }, + 3: { id: 3, ports: [0, 2], type: "inductor", sympyName: "L0" }, + 4: { id: 4, ports: [1, 2], type: "resistor", sympyName: "R0" }, + 5: { id: 5, ports: [2, null], type: "capacitor", sympyName: "C0" }, + }; + const values = { + L0: 0.000001, + R0: 10000, + C0: 1.0000000000000002e-14, + C1: 1e-12, + }; + const [textResult, _mathml] = await build_and_solve_mna(3, ["1"], components, values, pyodide); + const { numericText } = await new_calculate_tf(pyodide, { fmin: 1, fmax: 1000 }, 10, values, () => {}); + expect(textResult).toEqual("C0*L0*s**2/(C0*L0*R0*s**2 + L0*s + R0)"); + expect(numericText).toEqual("1.0e-20*s^2/(1.0e-16*s^2 + 1.0e-6*s + 10000)"); +}); + test("voltage in current probe - 2", async () => { const components = { - R0: { - ports: [0, 1], - type: "resistor", - }, - vin: { - ports: [0], - type: "vin", - }, - L0: { - ports: [0, 2], - type: "inductor", - }, - Y0: { - ports: [1, 2], - type: "iprobe", - }, - C0: { - ports: [2, null], - type: "capacitor", - }, + 1: { id: 1, ports: [0, 1], type: "resistor", sympyName: "R0" }, + 2: { id: 2, ports: [0], type: "vin", sympyName: "vin" }, + 3: { id: 3, ports: [0, 2], type: "inductor", sympyName: "L0" }, + 4: { id: 4, ports: [1, 2], type: "iprobe", sympyName: "Y0" }, + 5: { id: 5, ports: [2, null], type: "capacitor", sympyName: "C0" }, }; const values = { L0: 0.000001, R0: 10000, C0: 1.0000000000000002e-14, }; - const [textResult, _mathml] = await build_and_solve_mna(3, ["Y0"], components, values, pyodide); + const [textResult, _mathml] = await build_and_solve_mna(3, ["4"], components, values, pyodide); const { numericText } = await new_calculate_tf(pyodide, { fmin: 1, fmax: 1000 }, 10, values, () => {}); expect(textResult).toEqual("C0*L0*s**2/(C0*L0*R0*s**2 + L0*s + R0)"); // expect(_mathml).toEqual(null); @@ -78,33 +69,18 @@ test("voltage in current probe - 2", async () => { }); test("voltage in current probe - 3", async () => { const components = { - R0: { - ports: [0, 1], - type: "resistor", - }, - vin: { - ports: [0], - type: "vin", - }, - L0: { - ports: [0, 2], - type: "inductor", - }, - Y0: { - ports: [2, null], - type: "iprobe", - }, - C0: { - ports: [2, null], - type: "capacitor", - }, + 1: { id: 1, ports: [0, 1], type: "resistor", sympyName: "R0" }, + 2: { id: 2, ports: [0], type: "vin", sympyName: "vin" }, + 3: { id: 3, ports: [0, 2], type: "inductor", sympyName: "L0" }, + 4: { id: 4, ports: [2, null], type: "iprobe", sympyName: "Y0" }, + 5: { id: 5, ports: [2, null], type: "capacitor", sympyName: "C0" }, }; const values = { L0: 0.000001, R0: 10000, C0: 1e-14, }; - const [textResult, _mathml] = await build_and_solve_mna(3, ["Y0"], components, values, pyodide); + const [textResult, _mathml] = await build_and_solve_mna(3, ["4"], components, values, pyodide); const { numericText } = await new_calculate_tf(pyodide, { fmin: 1, fmax: 1000 }, 10, values, () => {}); expect(textResult).toEqual("1/(L0*s)"); expect(numericText).toEqual("1000000.0/s"); @@ -112,33 +88,18 @@ test("voltage in current probe - 3", async () => { test("current in current probe - 2", async () => { const components = { - L0: { - ports: [0, 2], - type: "inductor", - }, - iin: { - ports: [0], - type: "iin", - }, - Y0: { - ports: [0, 1], - type: "iprobe", - }, - R0: { - ports: [1, 2], - type: "resistor", - }, - C0: { - ports: [2, null], - type: "capacitor", - }, + 1: { id: 1, ports: [0, 2], type: "inductor", sympyName: "L0" }, + 2: { id: 2, ports: [0], type: "iin", sympyName: "iin" }, + 3: { id: 3, ports: [0, 1], type: "iprobe", sympyName: "Y0" }, + 4: { id: 4, ports: [1, 2], type: "resistor", sympyName: "R0" }, + 5: { id: 5, ports: [2, null], type: "capacitor", sympyName: "C0" }, }; const values = { L0: 0.000001, R0: 10000, C0: 1.0000000000000002e-14, }; - const [textResult, _mathml] = await build_and_solve_mna(3, ["Y0"], components, values, pyodide); + const [textResult, _mathml] = await build_and_solve_mna(3, ["3"], components, values, pyodide); const { numericText } = await new_calculate_tf(pyodide, { fmin: 1, fmax: 1000 }, 10, values, () => {}); expect(textResult).toEqual("L0*s/(L0*s + R0)"); // expect(_mathml).toEqual(null); @@ -148,28 +109,16 @@ test("current in current probe - 2", async () => { test("current in current probe VCIS - 3", async () => { const components = { - G0: { - ports: [null, 0, 1, null], - type: "vcis", - }, - R0: { - ports: [0, null], - type: "resistor", - }, - Y0: { - ports: [0, 1], - type: "iprobe", - }, - iin: { - ports: [0], - type: "iin", - }, + 1: { id: 1, ports: [null, 0, 1, null], type: "vcis", sympyName: "G0" }, + 2: { id: 2, ports: [0, null], type: "resistor", sympyName: "R0" }, + 3: { id: 3, ports: [0, 1], type: "iprobe", sympyName: "Y0" }, + 4: { id: 4, ports: [0], type: "iin", sympyName: "iin" }, }; const values = { G0: 0.001, R0: 1000000, }; - const [textResult, _mathml] = await build_and_solve_mna(2, ["Y0"], components, values, pyodide); + const [textResult, _mathml] = await build_and_solve_mna(2, ["3"], components, values, pyodide); const { numericText } = await new_calculate_tf(pyodide, { fmin: 1, fmax: 1000 }, 10, values, () => {}); expect(textResult).toEqual("G0*R0/(G0*R0 + 1)"); // expect(_mathml).toEqual(null); @@ -179,30 +128,12 @@ test("current in current probe VCIS - 3", async () => { test("voltage in voltage probe VCVS", async () => { const components = { - C0: { - ports: [1, 0], - type: "capacitor", - }, - L0: { - ports: [0, 1], - type: "inductor", - }, - vin: { - ports: [0], - type: "vin", - }, - A0: { - ports: [null, 1, 2, null], - type: "vcvs", - }, - R0: { - ports: [1, null], - type: "resistor", - }, - X0: { - ports: [2], - type: "vprobe", - }, + 1: { id: 1, ports: [1, 0], type: "capacitor", sympyName: "C0" }, + 2: { id: 2, ports: [0, 1], type: "inductor", sympyName: "L0" }, + 3: { id: 3, ports: [0], type: "vin", sympyName: "vin" }, + 4: { id: 4, ports: [null, 1, 2, null], type: "vcvs", sympyName: "A0" }, + 5: { id: 5, ports: [1, null], type: "resistor", sympyName: "R0" }, + 6: { id: 6, ports: [2], type: "vprobe", sympyName: "X0" }, }; const values = { C0: 1e-12, @@ -210,7 +141,7 @@ test("voltage in voltage probe VCVS", async () => { A0: 100, R0: 1000000, }; - const [textResult, _mathml] = await build_and_solve_mna(3, ["X0"], components, values, pyodide); + const [textResult, _mathml] = await build_and_solve_mna(3, ["6"], components, values, pyodide); const { numericText } = await new_calculate_tf(pyodide, { fmin: 1, fmax: 1000 }, 10, values, () => {}); expect(textResult).toEqual("A0*R0*(C0*L0*s**2 + 1)/(C0*L0*R0*s**2 + L0*s + R0)"); // expect(_mathml).toEqual(null);