From 19b3d01bd3fcd8baaa35e73de85864a073ec5342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Fri, 31 Jan 2025 14:24:34 +0100 Subject: [PATCH] calculate all voltages and currents --- src/main/angular/package-lock.json | 232 +++++++++++++++++- src/main/angular/package.json | 3 +- .../angular/src/app/editor/circuit/Circuit.ts | 156 ++++++++++++ .../angular/src/app/editor/circuit/Parts.ts | 34 ++- .../angular/src/app/editor/circuit/Wires.ts | 11 +- .../app/editor/circuit/circuit.component.less | 1 - .../app/editor/circuit/circuit.component.svg | 1 + .../app/editor/circuit/circuit.component.ts | 46 +++- .../angular/src/app/editor/colorHelpers.ts | 2 +- .../src/app/editor/junction/Junction.ts | 41 +++- .../editor/junction/junction.component.svg | 2 +- src/main/angular/src/app/editor/parts/Part.ts | 11 +- .../src/app/editor/parts/battery/Battery.ts | 23 +- .../src/app/editor/parts/light/Light.ts | 35 ++- src/main/angular/src/app/editor/wire/Wire.ts | 28 ++- .../angular/src/app/simulation/Calculation.ts | 38 --- 16 files changed, 561 insertions(+), 103 deletions(-) create mode 100644 src/main/angular/src/app/editor/circuit/Circuit.ts delete mode 100644 src/main/angular/src/app/simulation/Calculation.ts diff --git a/src/main/angular/package-lock.json b/src/main/angular/package-lock.json index dccea4a..e954f58 100644 --- a/src/main/angular/package-lock.json +++ b/src/main/angular/package-lock.json @@ -16,6 +16,7 @@ "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", "@angular/router": "^19.0.0", + "mathjs": "^14.2.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -2157,7 +2158,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "dev": true, "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -3144,6 +3144,44 @@ "tslib": "2" } }, + "node_modules/@lambdatest/node-tunnel": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@lambdatest/node-tunnel/-/node-tunnel-4.0.8.tgz", + "integrity": "sha512-IY42aDD4Ryqjug9V4wpCjckKpHjC2zrU/XhhorR5ztX088XITRFKUo8U6+gOjy/V8kAB+EgDuIXfK0izXbt9Ow==", + "license": "ISC", + "dependencies": { + "adm-zip": "^0.5.10", + "axios": "^1.6.2", + "get-port": "^1.0.0", + "https-proxy-agent": "^5.0.0", + "split": "^1.0.1" + } + }, + "node_modules/@lambdatest/node-tunnel/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@lambdatest/node-tunnel/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5294,6 +5332,15 @@ "node": ">=8.9.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -5461,6 +5508,12 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -5499,6 +5552,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-loader": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", @@ -6307,6 +6371,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -6321,6 +6397,19 @@ "dev": true, "license": "ISC" }, + "node_modules/complex.js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", + "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -6708,7 +6797,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -6722,6 +6810,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "license": "MIT" + }, "node_modules/default-browser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", @@ -6778,6 +6872,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7254,6 +7357,12 @@ "dev": true, "license": "MIT" }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "license": "MIT" + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -7681,7 +7790,6 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "dev": true, "funding": [ { "type": "individual", @@ -7715,6 +7823,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7867,6 +7989,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-1.0.0.tgz", + "integrity": "sha512-vg59F3kcXBOtcIijwtdAyCxFocyv/fVkGQvw1kVGrxFO1U4SSGkGjrbASg5DN3TVekVle/jltwOjYRnZWc1YdA==", + "license": "MIT", + "bin": { + "get-port": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -8801,6 +8935,12 @@ "dev": true, "license": "MIT" }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "license": "MIT" + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -9763,6 +9903,43 @@ "node": ">= 0.4" } }, + "node_modules/mathjs": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.2.0.tgz", + "integrity": "sha512-CcJV1cQwRSrQIAAX3sWejFPUvUsQnTZYisEEuoMBw3gMDJDQzvKQlrul/vjKAbdtW7zaDzPCl04h1sf0wh41TA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@lambdatest/node-tunnel": "^4.0.8", + "complex.js": "^2.2.5", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^5.2.1", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.2.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mathjs/node_modules/fraction.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.1.tgz", + "integrity": "sha512-Ah6t/7YCYjrPUFUFsOsRLMXAdnYM+aQwmojD2Ayb/Ezr82SwES0vuyQ8qZ3QO8n9j7W14VJuVZZet8U3bhSdQQ==", + "license": "MIT", + "engines": { + "node": ">= 12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -9874,7 +10051,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9884,7 +10060,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -10211,7 +10386,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msgpackr": { @@ -11460,6 +11634,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -11618,7 +11798,6 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, "license": "MIT" }, "node_modules/regenerator-transform": { @@ -12090,6 +12269,12 @@ } } }, + "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/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -12801,6 +12986,18 @@ "wbuf": "^1.7.3" } }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -13184,6 +13381,12 @@ "tslib": "^2" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -13191,6 +13394,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -13309,6 +13518,15 @@ "dev": true, "license": "MIT" }, + "node_modules/typed-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", diff --git a/src/main/angular/package.json b/src/main/angular/package.json index 2a2684a..703b1f4 100644 --- a/src/main/angular/package.json +++ b/src/main/angular/package.json @@ -20,7 +20,8 @@ "@angular/router": "^19.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", - "zone.js": "~0.15.0" + "zone.js": "~0.15.0", + "mathjs": "^14.2.0" }, "devDependencies": { "@angular-devkit/build-angular": "^19.0.7", diff --git a/src/main/angular/src/app/editor/circuit/Circuit.ts b/src/main/angular/src/app/editor/circuit/Circuit.ts new file mode 100644 index 0000000..1fa3b9a --- /dev/null +++ b/src/main/angular/src/app/editor/circuit/Circuit.ts @@ -0,0 +1,156 @@ +import {Part} from '../parts/Part'; +import {Battery} from '../parts/battery/Battery'; +import {Junction} from '../junction/Junction'; +import {Wire} from '../wire/Wire'; +import {lusolve} from 'mathjs'; + +export const RESISTANCE_MIN = 0.00001; + +export class Circuit { + + private readonly matrix: number[][] = []; + + private readonly currents: number[] = []; + + private potentials: number[] = []; + + constructor( + readonly parts: Part[], + readonly junctionsWithoutPivot: Junction[], + readonly wires: Wire[]) { + this.matrixInit(junctionsWithoutPivot.length); + this.wires.forEach(wire => this.addResistor(wire)); + this.parts.filter(p => p instanceof Battery).forEach(battery => this.addCurrentSource(battery)); + this.solve(); + this.wires.forEach(wire => this.useCurrent(wire)); + const minCircuitVoltage = this.potentials.reduce((a, b) => a < b ? a : b, 0); + const maxCircuitVoltage = this.potentials.reduce((a, b) => a > b ? a : b, 0); + this.junctionsWithoutPivot.forEach(junction => { + junction.minCircuitVoltage = minCircuitVoltage; + junction.maxCircuitVoltage = maxCircuitVoltage; + junction.voltage = this.getPotential(junctionsWithoutPivot.indexOf(junction)); + }); + } + + private matrixInit(size: number) { + for (let r = 0; r < size; r++) { + this.matrix.push([]); + for (let c = 0; c < size; c++) { + this.matrix[r][c] = 0; + } + this.currents[r] = 0; + } + } + + private addResistor(wire: Wire) { + const index0 = this.junctionsWithoutPivot.indexOf(wire.start); + const index1 = this.junctionsWithoutPivot.indexOf(wire.end); + const conductance = 1.0 / wire.resistance; + if (index0 >= 0) { + this.matrix[index0][index0] += conductance; + } + if (index1 >= 0) { + this.matrix[index1][index1] += conductance; + } + if (index0 >= 0 && index1 >= 0) { + this.matrix[index0][index1] -= conductance; + this.matrix[index1][index0] -= conductance; + } + } + + private useCurrent(wire: Wire) { + const indexStart = this.junctionsWithoutPivot.indexOf(wire.start); + const indexEnd = this.junctionsWithoutPivot.indexOf(wire.end); + const conductance = indexStart >= 0 && indexEnd >= 0 ? this.matrix[indexStart][indexEnd] : 1 / RESISTANCE_MIN; + const potentialStart = this.getPotential(indexStart); + const potentialEnd = this.getPotential(indexEnd); + const potentialDifference = potentialStart === null || potentialEnd === null ? 0 : potentialEnd - potentialStart; + wire.current = conductance * potentialDifference; + } + + private getPotential(index: number): number | null { + if (index < 0) { + return 0; // because this is our pivot, and it is per definition common (GND) = 0 + } + if (this.potentials === null) { + return null; + } + return this.potentials[index]; + } + + private addCurrentSource(battery: Battery) { + const indexMinus = this.junctionsWithoutPivot.indexOf(battery.minus); + const indexPlus = this.junctionsWithoutPivot.indexOf(battery.plus); + const current = battery.voltage / battery.resistance; + if (indexMinus >= 0) { + this.currents[indexMinus] -= current; + } + if (indexPlus >= 0) { + this.currents[indexPlus] += current; + } + } + + private solve() { + this.potentials = lusolve(this.matrix, this.currents).flat() as number[]; + } + + static calculate(parts: Part[]): Circuit[] { + const restParts = [...parts]; + console.debug("Recalculating circuit...") + const circuits: Circuit[] = []; + let circuitNumber = 0; + while (true) { + const battery = restParts.filter(p => p instanceof Battery)[0]; + if (!battery) { + break; + } + const circuit = this.calculateDisjunct(battery, restParts, circuitNumber++); + circuits.push(circuit); + } + if (restParts.length > 0) { + console.debug(`found ${restParts.length} not connected to any battery`); + } + return circuits; + } + + private static calculateDisjunct(battery: Battery, restParts: Part[], circuitNumber: number): Circuit { + console.debug(` Circuit #${circuitNumber}:`) + + const foundParts: Part[] = [battery]; + restParts.splice(restParts.indexOf(battery), 1); + + const pivot = battery.minus; + const foundJunctions: Junction[] = []; + const todo: Junction[] = [pivot]; + + const foundWires: Wire[] = []; + while (todo.length > 0) { + const sourceJunction: Junction = todo.splice(0, 1)[0]; + console.debug(` ${sourceJunction.fullName}`) + if (sourceJunction.wires.length === 0) { + console.debug(` NO WIRES`) + } + for (let wire of sourceJunction.wires) { + if (!foundWires.includes(wire)) { + foundWires.push(wire); + } + const destinationJunction = wire.traverse(sourceJunction); + const destinationPart = destinationJunction.part; + const newJunction = destinationJunction !== pivot && !foundJunctions.includes(destinationJunction); + console.debug(` ${newJunction ? "[NEW]" : "[___]"} ${wire}`) + if (newJunction) { + const newPart = !foundParts.includes(destinationPart); + if (newPart) { + foundParts.push(destinationPart); + restParts.splice(restParts.indexOf(battery), 1); + } + todo.push(destinationJunction); + foundJunctions.push(destinationJunction); + } + } + } + const junctionCountIncludingPivot = foundJunctions.length + 1; + console.debug(` => Circuit #${circuitNumber} (${foundParts.length} parts, ${junctionCountIncludingPivot} junctions, ${foundWires.length} wires)`); + return new Circuit(foundParts, foundJunctions, foundWires); + } +} diff --git a/src/main/angular/src/app/editor/circuit/Parts.ts b/src/main/angular/src/app/editor/circuit/Parts.ts index 437a57d..8247f8f 100644 --- a/src/main/angular/src/app/editor/circuit/Parts.ts +++ b/src/main/angular/src/app/editor/circuit/Parts.ts @@ -1,6 +1,7 @@ import {Point} from '../Point'; import {Part, RASTER} from '../parts/Part'; -import {MessageService} from '../message/message.service'; +import {Battery} from '../parts/battery/Battery'; +import {Light} from '../parts/light/Light'; export class Parts { @@ -12,12 +13,6 @@ export class Parts { dragCursor: Point | null = null; - constructor( - protected messageService: MessageService, - ) { - // - } - mouseDown(part: Part, $event: MouseEvent) { if ($event.button === 0) { this.dragStartPart = part; @@ -44,4 +39,29 @@ export class Parts { this.dragCursor = Point.fromEvent($event); } + newBattery(rasterX: number, rasterY: number, voltage: number): Battery { + return this.add(new Battery(rasterX, rasterY, this.generateName("Batterie"), voltage, 0.5)); + } + + newLight(rasterX: number, rasterY: number): Light { + return this.add(new Light(rasterX, rasterY, this.generateName("Licht"), 3, 150)); + } + + private generateName(baseName: string) { + let counter = 1; + let name: string; + while (true) { + name = `${baseName} #${counter++}`; + if (!this.list.some(p => p.name === name)) { + break + } + } + return name; + } + + private add(part: T): T { + this.list.push(part); + return part; + } + } diff --git a/src/main/angular/src/app/editor/circuit/Wires.ts b/src/main/angular/src/app/editor/circuit/Wires.ts index df4a126..5861a66 100644 --- a/src/main/angular/src/app/editor/circuit/Wires.ts +++ b/src/main/angular/src/app/editor/circuit/Wires.ts @@ -20,7 +20,8 @@ export class Wires { dragEndDuplicate: boolean = false; constructor( - protected messageService: MessageService, + protected readonly messageService: MessageService, + protected readonly recalculate: () => any, ) { // } @@ -97,11 +98,10 @@ export class Wires { this.messageService.warn("Diese Verbindung existiert bereits."); return; } - const wire = new Wire(start, end); - wire.start.wires.push(wire); - wire.end.wires.push(wire); + const wire = new Wire(start, end, 0); this.list.push(wire); - console.log("Wire connected: ", wire); + console.log(`Wire connected: ${wire}`); + this.recalculate(); } disconnect(wire: Wire) { @@ -109,6 +109,7 @@ export class Wires { wire.start.wires.splice(wire.start.wires.indexOf(wire), 1); wire.end.wires.splice(wire.end.wires.indexOf(wire), 1); console.log("Wire disconnected: ", wire); + this.recalculate(); } } diff --git a/src/main/angular/src/app/editor/circuit/circuit.component.less b/src/main/angular/src/app/editor/circuit/circuit.component.less index 83d930a..54b0239 100644 --- a/src/main/angular/src/app/editor/circuit/circuit.component.less +++ b/src/main/angular/src/app/editor/circuit/circuit.component.less @@ -9,7 +9,6 @@ .wire { stroke-width: 9px; - stroke: gray; stroke-linecap: round; pointer-events: none; } diff --git a/src/main/angular/src/app/editor/circuit/circuit.component.svg b/src/main/angular/src/app/editor/circuit/circuit.component.svg index dacfe89..6391a3c 100644 --- a/src/main/angular/src/app/editor/circuit/circuit.component.svg +++ b/src/main/angular/src/app/editor/circuit/circuit.component.svg @@ -12,6 +12,7 @@ > this.recalculate()); - this.wires = new Wires(messageService); - this.wires.connect(this.battery.minus, this.light.a); - this.wires.connect(this.battery.plus, this.light.b); + const battery0 = this.parts.newBattery(1, 6, 1.5); + const battery1 = this.parts.newBattery(5, 6, 1.5); + const light0 = this.parts.newLight(1, 1); + const light1 = this.parts.newLight(5, 1); + const light2 = this.parts.newLight(3, 11); + + this.wires.connect(light0.a, battery0.minus); + this.wires.connect(light0.b, light1.a); + + this.wires.connect(light1.b, battery1.plus); + this.wires.connect(battery0.plus, battery1.minus); + + this.wires.connect(light2.a, battery0.minus); + this.wires.connect(light2.b, battery1.plus); } mouseMove($event: MouseEvent) { @@ -49,4 +56,19 @@ export class CircuitComponent { this.parts.mouseUp($event); } + recalculate() { + Circuit.calculate(this.parts.list) + } + + voltageColor(wire: Wire) { + if (wire.start.voltage === null || wire.start.minCircuitVoltage === null || wire.start.maxCircuitVoltage === null) { + return 'gray'; + } + const ratio = (wire.start.voltage - wire.start.minCircuitVoltage) / (wire.start.maxCircuitVoltage - wire.start.minCircuitVoltage); + if (ratio < 0.5) { + return fadeColor(ratio * 2, 'blue', 'magenta'); + } + return fadeColor((ratio - 0.5) * 2, 'magenta', 'red'); + } + } diff --git a/src/main/angular/src/app/editor/colorHelpers.ts b/src/main/angular/src/app/editor/colorHelpers.ts index 48e5713..f61a28f 100644 --- a/src/main/angular/src/app/editor/colorHelpers.ts +++ b/src/main/angular/src/app/editor/colorHelpers.ts @@ -25,7 +25,7 @@ export function parseColor(color: string): { r: number; g: number; b: number } { return {r, g, b}; } -export function fadeGrayToYellow(factor: number, fromColor: any, toColor: any): string { +export function fadeColor(factor: number, fromColor: any, toColor: any): string { fromColor = parseColor(fromColor); toColor = parseColor(toColor); factor = Math.max(0, Math.min(100, factor)); diff --git a/src/main/angular/src/app/editor/junction/Junction.ts b/src/main/angular/src/app/editor/junction/Junction.ts index bb4695f..e98f9ed 100644 --- a/src/main/angular/src/app/editor/junction/Junction.ts +++ b/src/main/angular/src/app/editor/junction/Junction.ts @@ -1,10 +1,11 @@ import {Wire} from "../wire/Wire"; +import {Part} from '../parts/Part'; export const JUNCTION_RADIUS_PERCENT = 15; export class Junction { - readonly id: string = self.crypto.randomUUID(); + readonly uuid: string = self.crypto.randomUUID(); readonly percentX: number; @@ -12,29 +13,43 @@ export class Junction { readonly wires: Wire[] = []; + voltage: number | null = null; + + minCircuitVoltage: number | null = null; + + maxCircuitVoltage: number | null = null; + + get fullName(): string { + return `'${this.part.name}' '${this.name}'`; + } + constructor( + public readonly part: Part, percentX: number, percentY: number, - readonly name: string, - ) { + readonly name: string) { this.percentX = percentX - JUNCTION_RADIUS_PERCENT; this.percentY = percentY - JUNCTION_RADIUS_PERCENT; } - private boundingBox(next: (b: DOMRect) => number): number { - const b = document.getElementById(this.id)?.getBoundingClientRect(); - if (!b) { + toString(): string { + return `${this.name}`; + } + + private mapRect(next: (b: DOMRect) => number): number { + const rect = document.getElementById(this.uuid)?.getBoundingClientRect(); + if (!rect) { return 0; } - return next(b); + return next(rect); } get pixelX(): number { - return this.boundingBox(b => b.x + b.width / 2); + return this.mapRect(b => b.x + b.width / 2); } get pixelY(): number { - return this.boundingBox(b => b.y + b.height / 2); + return this.mapRect(b => b.y + b.height / 2); } get percentW(): number { @@ -45,4 +60,12 @@ export class Junction { return (JUNCTION_RADIUS_PERCENT * 2); } + sumAllBut(plus: Junction) { + return this.wires + .filter(wire => wire.traverse(this) !== plus) + .map(wire => wire.current) + .filter(current => current !== null) + .reduce((a, b) => a + b, 0); + } + } diff --git a/src/main/angular/src/app/editor/junction/junction.component.svg b/src/main/angular/src/app/editor/junction/junction.component.svg index 03f16c7..7bbd72c 100644 --- a/src/main/angular/src/app/editor/junction/junction.component.svg +++ b/src/main/angular/src/app/editor/junction/junction.component.svg @@ -11,7 +11,7 @@ (mouseup)="wires.mouseUp($event)" > this.voltageMax; } @@ -33,7 +42,17 @@ export class Light extends Part { return siPrefix(this.voltage, 'V', 2); } + get current(): number { + if (this.a.voltage === null || this.b.voltage === null) { + return 0; + } + return (this.b.voltage - this.a.voltage) / this.resistance; + } + get currentStr(): string { + if (this.current == null) { + return '--- A'; + } return siPrefix(this.current, 'A', 2); } @@ -41,7 +60,7 @@ export class Light extends Part { if (this.defect) { return "#6e4122" } - return fadeGrayToYellow(this.voltage / this.voltageMax, "#888", "#FF0"); + return fadeColor(this.voltage / this.voltageMax, "#888", "#FF0"); } } diff --git a/src/main/angular/src/app/editor/wire/Wire.ts b/src/main/angular/src/app/editor/wire/Wire.ts index 1645f6b..51f1df1 100644 --- a/src/main/angular/src/app/editor/wire/Wire.ts +++ b/src/main/angular/src/app/editor/wire/Wire.ts @@ -1,12 +1,38 @@ import {Junction} from "../junction/Junction"; +import {RESISTANCE_MIN} from '../circuit/Circuit'; export class Wire { + current: number | null = null; + constructor( readonly start: Junction, readonly end: Junction, + public resistance: number, + public name: string | null = null, ) { - // + this.start.wires.push(this); + this.end.wires.push(this); + if (this.resistance === 0) { + this.resistance = RESISTANCE_MIN; + } + } + + toString() { + if (this.start.part === this.end.part && this.name !== null) { + return `'${this.start.part}' "${this.name}"`; + } + return `${this.name !== null ? this.name + ' ' : ''}${this.start.fullName} ==> ${this.end.fullName}`; + } + + traverse(junction: Junction) { + if (junction === this.start) { + return this.end; + } + if (junction === this.end) { + return this.start; + } + throw new Error(`Wire is not connected to given Junction: wire=${this}, junction=${junction}`); } } diff --git a/src/main/angular/src/app/simulation/Calculation.ts b/src/main/angular/src/app/simulation/Calculation.ts deleted file mode 100644 index 42eaf66..0000000 --- a/src/main/angular/src/app/simulation/Calculation.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {Part} from '../editor/parts/Part'; -import {Battery} from '../editor/parts/battery/Battery'; - -export class Circuit { - - constructor( - readonly battery: Battery, - readonly parts: Part[], - ) { - // - } - - private static create2(battery: Battery, parts: Part[]): Circuit { - parts.splice(parts.indexOf(battery), 1); - - const connectedParts: Part[] = []; - for (let part of parts) { - - } - connectedParts.forEach(part => parts.splice(parts.indexOf(part), 1)); - - return new Circuit(battery, connectedParts); - } - - static create(parts: Part[]): Circuit[] { - const circuits: Circuit[] = []; - while (true) { - const battery = parts.filter(p => p instanceof Battery)[0]; - if (!battery) { - break; - } - const circuit = this.create2(battery, parts); - circuits.push(circuit); - } - return circuits; - } - -}