diff --git a/.gitignore b/.gitignore index a547bf3..ab2126d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +*/__screenshots__/* \ No newline at end of file diff --git a/package.json b/package.json index d055c64..5a05983 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,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", diff --git a/src/App.jsx b/src/App.jsx index b53ec1e..bd0bc6a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import "./App.css"; import "visiojs/dist/visiojs.css"; // Import VisioJS styles // import { startupSchematic } from "./startupSchematic.js"; @@ -8,8 +8,8 @@ import { VisioJSSchematic } from "./VisioJSSchematic.jsx"; import { ComponentAdjuster } from "./ComponentAdjuster.jsx"; import { FreqAdjusters } from "./FreqAdjusters.jsx"; // import Grid from "@mui/material/Grid"; -import { units } from "./common.js"; -import { calcBilinear } from "./new_solveMNA.js"; +import { units, formatMathML } from "./common.js"; +import { calcBilinear, new_calculate_tf } from "./new_solveMNA.js"; import { NavBar } from "./NavBar.jsx"; import { ChoseTF } from "./ChoseTF.jsx"; @@ -93,58 +93,6 @@ function stateFromURL() { } const [modifiedComponents, modifiedSettings, modifiedSchematic] = stateFromURL(); -function new_calculate_tf(textResult, fRange, numSteps, components, setErrorSnackbar) { - // console.log("new_calculate_tf", { textResult, fRange, numSteps, components }); - if (textResult == "") return { freq_new: [], mag_new: [] }; - var complex_freq = textResult; - var rep; - for (const key in components) { - rep = RegExp(key, "g"); - complex_freq = complex_freq.replace(rep, components[key]); - } - - //Now only remaining variable is S, substitute that and solve. Also swap power ^ for ** - const re = /s/gi; - const reDollar = /\$/gi; - const re2 = /\^/gi; - var res = complex_freq.replace(re2, "**"); //swap ^ for ** - // const re3 = /abs/gi; //sometimes abs(C0) is left in the equation - // res = res.replace(re3, ""); - //now swap sqrt for '$' - const re3 = /sqrt/gi; //sometimes abs(C0) is left in the equation - res = res.replace(re3, "$"); //swap sqrt for $ - const re4 = /abs/gi; - res = res.replace(re4, ""); //swap Abs(...) for (...). I saw one case where Sympy left it in but it's ok to remove it like this - const re5 = /I/gi; - res = res.replace(re5, "1"); //swap I for 1. Sometimes sympy leaves in the I if the result is fully imaginary. This is dangerous and instead the numeric solving should go inside sympy - - var fstepdB_20 = Math.log10(fRange.fmax / fRange.fmin) / numSteps; - var fstep = 10 ** fstepdB_20; - var absNew, evalNew; - const freq = []; - const mag = []; - try { - for (var f = fRange.fmin; f < fRange.fmax; f = f * fstep) { - freq.push(f); - // const mathString = res.replace(re, 2 * Math.PI * f).replace(/\*\*/g, "^"); - // evalNew = evaluate(mathString); - const mathString = res.replace(re, 2 * Math.PI * f).replace(reDollar, "Math.sqrt"); - evalNew = eval(mathString); - - absNew = Math.abs(evalNew); - mag.push(20 * Math.log10(absNew)); - } - } catch (err) { - setErrorSnackbar((x) => { - if (!x) return true; - else return x; - }); - console.log("oh no", err); - } - - return { freq_new: freq, mag_new: mag }; -} - function compToURL(key, value) { return `${key}_${value.type}_${value.value}_${value.unit}`; } @@ -153,7 +101,9 @@ function App() { const [nodes, setNodes] = useState([]); const [fullyConnectedComponents, setFullyConnectedComponents] = useState({}); - const [results, setResults] = useState({ text: "", mathML: "", complexResponse: "", bilinearRaw: "", bilinearMathML: "" }); + const [results, setResults] = useState({ text: "", mathML: "", complexResponse: "", solver: null, probeName: "", drivers: [] }); + const [numericResults, setNumericResults] = useState({ numericML: "", numericText: "" }); + const [bilinearResults, setBilinearResults] = useState({ bilinearML: "", bilinearText: "" }); const [componentValues, setComponentValues] = useState(modifiedComponents); const [settings, setSettings] = useState(modifiedSettings); const [schemHistory, setSchemHistory] = useState({ pointer: 0, state: [modifiedSchematic] }); @@ -246,13 +196,53 @@ function App() { const [freq_new, setFreqNew] = useState(null); const [mag_new, setMagNew] = useState(null); + const [phase_new, setPhaseNew] = useState(null); + const debounceTimerRef = useRef(null); + useEffect(() => { - 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 { freq_new, mag_new } = new_calculate_tf(results.complexResponse, fRange, settings.resolution, componentValuesSolved2, setErrorSnackbar); - setFreqNew(freq_new); - setMagNew(mag_new); + // Clear any existing timeout + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + // Set a new timeout to debounce the calculation + debounceTimerRef.current = setTimeout(async () => { + const calculateTF = async () => { + if (!results.solver || results.text === "") { + setFreqNew([]); + setMagNew([]); + setPhaseNew([]); + setNumericResults({ numericML: "", numericText: "" }); + setBilinearResults({ bilinearML: "", bilinearText: "" }); + 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 { freq_new, mag_new, phase_new, numericML, numericText } = await new_calculate_tf( + results.solver, + fRange, + settings.resolution, + componentValuesSolved2, + setErrorSnackbar, + ); + setFreqNew(freq_new); + setMagNew(mag_new); + setPhaseNew(phase_new); + if (numericML && numericText && results.probeName && results.drivers) { + const formattedNumericML = formatMathML(numericML, results.probeName, results.drivers); + setNumericResults({ numericML: formattedNumericML, numericText: numericText }); + } + }; + calculateTF(); + }, 1000); // Wait 1000ms after the user stops typing + + // Cleanup function to clear timeout on unmount or when dependencies change + return () => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + }; }, [results, settings, componentValues]); function stateToURL() { @@ -284,9 +274,10 @@ function App() { async function handleRequestBilin() { // console.log("handleRequestBilin", calcBilinear()); const [raw, bilin] = await calcBilinear(results.solver); - // setBilinearMathML(`${bilin}`); - // setBilinearRaw(raw); - setResults({ ...results, bilinearRaw: raw, bilinearMathML: `${bilin}` }); + setBilinearResults({ + bilinearML: `${bilin}`, + bilinearText: raw, + }); } // console.log(results); @@ -334,14 +325,13 @@ function App() { {results.text != "" && ( <> - {results.numericText != null && ( - - )}
- +
- {results.bilinearMathML == "" ? ( + + + {bilinearResults.bilinearML == "" ? ( ); })} )} - {/* Enable this feature if this ticket gets solved! https://github.com/Yaffle/Expression/issues/15 */} - - - -
Chose Algebra solver
- {"These are JavaScript based solvers. It's already loaded and is fine for most cases"} - - } - > - { - setResults({ ...emptyResults }); - setLoadedPyo(null); - setAlgebraic("algebrite"); - }} - > - {loading ? : "Algebrite + Yaffle"} - -
- -
Chose Algebra solver
- { - "This is a Python based solver. Chosing this will download 10MB of files and run Python inside the browser, enabling more features such as giving a pretty numeric result, and more advanced algebraic simplification." - } - - } - > - { - setLoading(true); - setResults({ ...emptyResults }); - const pyodide = await initPyodideAndSympy(); - setLoadedPyo(pyodide); - setLoading(false); - setAlgebraic("sympy"); - }} - > - {loading ? : "SymPy"} - -
-
-
); } diff --git a/src/PlotTF.jsx b/src/PlotTF.jsx index c6ca157..bba03f0 100644 --- a/src/PlotTF.jsx +++ b/src/PlotTF.jsx @@ -1,9 +1,36 @@ import ReactECharts from "echarts-for-react"; +import CircularProgress from "@mui/material/CircularProgress"; +import Box from "@mui/material/Box"; -const MyEChartsPlot = ({ freq_new, mag_new }) => { - if (freq_new.length === 0 || mag_new.length === 0) { +const MyEChartsPlot = ({ freq_new, mag_new, phase_new, hasResults }) => { + // Show loading indicator if we have results but data is not yet calculated + // (hasResults indicates results.text exists, meaning calculation is in progress) + if (hasResults && (freq_new === null || mag_new === null || freq_new.length === 0 || mag_new.length === 0)) { + return ( + + + Calculating transfer function... + + ); + } + + // Show no data message if arrays are empty and we don't have results + if (!freq_new || !mag_new || freq_new.length === 0 || mag_new.length === 0) { return No data available for plot; } + + // Convert linear magnitude to dB + const mag_db = mag_new.map((mag) => { + if (mag > 0) { + return 20 * Math.log10(mag); + } else { + return -Infinity; + } + }); + + // Convert phase from radians to degrees + const phase_deg = phase_new && phase_new.length > 0 ? phase_new.map((phase_rad) => (phase_rad * 180) / Math.PI) : []; + const option = { tooltip: { trigger: "axis", @@ -14,11 +41,10 @@ const MyEChartsPlot = ({ freq_new, mag_new }) => { }, }, formatter: function (params) { - // params is an array of points (since trigger is 'axis') - const point = params[0]; // only one series in this case - - const freq = point.value[0]; - const amp = point.value[1]; + // params is an array of points (one for each series) + const freq = params[0].value[0]; + const amp = params[0].value[1]; + const phase = phase_deg.length > 0 ? params[1]?.value[1] : null; let freqStr = ""; if (freq >= 1e9) freqStr = (freq / 1e9).toFixed(3) + " GHz"; @@ -26,17 +52,32 @@ const MyEChartsPlot = ({ freq_new, mag_new }) => { else if (freq >= 1e3) freqStr = (freq / 1e3).toFixed(3) + " kHz"; else freqStr = freq.toFixed(2) + " Hz"; - return ` + let tooltipContent = ` Frequency: ${freqStr}
Amplitude: ${amp.toPrecision(6)} dB `; + if (phase !== null && phase !== undefined) { + tooltipContent += `
Phase: ${phase.toPrecision(6)}°`; + } + return tooltipContent; }, }, + legend: { + show: true, + data: ["Amplitude", ...(phase_deg.length > 0 ? ["Phase"] : [])], + top: "top", + right: "right", + }, xAxis: { min: freq_new[0], // set your desired min frequency (Hz) max: freq_new[freq_new.length - 1], // set your desired max frequency (Hz) type: "log", name: "freq (Hz)", + nameLocation: "middle", + nameGap: 30, + nameTextStyle: { + padding: [10, 0, 0, 0], + }, minorSplitLine: { show: true, }, @@ -47,22 +88,64 @@ const MyEChartsPlot = ({ freq_new, mag_new }) => { if (value >= 1e3) return (value / 1e3).toFixed(2) + "k"; return value.toFixed(2); }, + margin: 10, }, }, - yAxis: { - type: "value", - name: "amplitude (dB)", - }, + yAxis: [ + { + type: "value", + name: "amplitude (dB)", + position: "left", + nameLocation: "middle", + nameGap: 50, + nameTextStyle: { + padding: [0, 0, 0, 0], + }, + }, + { + type: "value", + name: "phase (°)", + position: "right", + nameLocation: "middle", + nameGap: 50, + nameTextStyle: { + padding: [0, 0, 0, 0], + }, + axisLabel: { + margin: 12, + }, + }, + ], series: [ { - data: freq_new.map((x, i) => [x, mag_new[i]]), + data: freq_new.map((x, i) => [x, mag_db[i]]), type: "line", + name: "Amplitude", + yAxisIndex: 0, // showSymbol: false, // no markers smooth: false, lineStyle: { width: 2, }, }, + ...(phase_deg.length > 0 + ? [ + { + data: freq_new.map((x, i) => [x, phase_deg[i]]), + type: "line", + name: "Phase", + yAxisIndex: 1, + smooth: false, + lineStyle: { + width: 2, + color: "red", + }, + itemStyle: { + color: "red", + }, + }, + ] + : []), ], // grid: { // top: 10, diff --git a/src/ReleaseNotes.jsx b/src/ReleaseNotes.jsx index 4332130..410c762 100644 --- a/src/ReleaseNotes.jsx +++ b/src/ReleaseNotes.jsx @@ -68,6 +68,21 @@ function ReleaseNotes() { simplify an algebraic matrix. However, SymPy can simplify better, and can swap out the algebra to make pretty numberic results + + + v2.1 + + + December 2025 + + +
    +
  • Plotting phase, before only amplitude was plotted
  • +
  • Removed Algebrite solver - now only using SymPy
  • +
  • Fixed issue where iprobe was grounded
  • +
+
+
diff --git a/src/common.js b/src/common.js index a886ce1..2464bc2 100644 --- a/src/common.js +++ b/src/common.js @@ -174,4 +174,8 @@ export const theme = createTheme({ }, }); -export const emptyResults = { text: "", mathML: "", complexResponse: "", bilinearRaw: "", bilinearMathML: "", numericML: "", numericText: "", solver: null }; +export const emptyResults = { text: "", mathML: "", complexResponse: "", solver: null, probeName: "", drivers: [] }; + +export function formatMathML(mathml, p, drivers) { + return `${p}${drivers[0] == "vin" ? "V" : "I"}in=${mathml}`; +} diff --git a/src/new_solveMNA.js b/src/new_solveMNA.js index 5b34875..5eff5cb 100644 --- a/src/new_solveMNA.js +++ b/src/new_solveMNA.js @@ -1,5 +1,3 @@ -import * as Algebrite from "algebrite"; -import simplify_algebra from "./simplify_algebra.js"; // import { loadPyodide } from "pyodide"; // import { initPyodideAndSympy } from "./pyodideLoader"; @@ -27,63 +25,24 @@ ${ : `mna_vo_vi = mna_inv[${resIndex[0] - 1}, ${resIndex[1] - 1}] - mna_inv[${resIndex2[0] - 1}, ${resIndex2[1] - 1}]` }` } -mna_vo_vi_complex = mna_vo_vi.subs(s, s*I) result_simplified = simplify(mna_vo_vi) -result_numeric = result_simplified.subs(${JSON.stringify(componentValuesSolved).replaceAll('"', "")}) -result_numeric_simplified = simplify(result_numeric) -result_numeric_complex = result_numeric_simplified.subs(s, s*I) -rx = Abs(result_numeric_complex) -str(result_simplified), mathml(result_simplified, printer='presentation'), str(rx), mathml(result_numeric_simplified, printer='presentation'), str(result_numeric_simplified) +str(result_simplified), mathml(result_simplified, printer='presentation') `; try { - const [textResult, mathml, complex_response, numericML, numericText] = await pyodide.runPythonAsync(sympyString); - const newNumeric = numericML; - const newNumericText = numericText.replaceAll("**", "^"); + const [textResult, mathml] = await pyodide.runPythonAsync(sympyString); - return [textResult, removeFenced(mathml), complex_response, removeFenced(newNumeric), newNumericText]; + return [textResult, removeFenced(mathml)]; } catch (err) { console.log("Solving MNA matrix failed with this error:", err); - return ["", "", "", "", ""]; - } -} - -async function solveWithAlgebrite(matrixStr, mnaMatrix, resIndex, resIndex2) { - try { - Algebrite.eval("clearall"); - - if (mnaMatrix.length == 1) { - Algebrite.eval(`mna_vo_vi = 1/(${mnaMatrix[0]})`); //FIXME - is this correct? When is it hit, with Iin? - } else { - Algebrite.eval(`mna = [${matrixStr}]`); - Algebrite.eval("inv_mna = inv(mna)"); - if (resIndex2.length == 0) { - Algebrite.eval(`mna_vo_vi = inv_mna[${resIndex[0]}][${resIndex[1]}]`); - } else { - Algebrite.eval(`mna_vo_vi = inv_mna[${resIndex[0]}][${resIndex[1]}] - inv_mna[${resIndex2[0]}][${resIndex2[1]}]`); - } - } - - var strOut = Algebrite.eval("mna_vo_vi").toString(); //4ms - - const [textResult, mathml] = simplify_algebra(strOut); - - Algebrite.eval("complex_response = subst(s*i,s,mna_vo_vi)"); - Algebrite.eval("abs_complex_response = abs(complex_response)"); - - const complex_response = Algebrite.eval("abs_complex_response").toString(); - - return [textResult, mathml, complex_response]; - } catch (err) { - console.log("Building MNA matrix failed with this error:", err); - return ["", "", ""]; + return ["", ""]; } } // 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, solver) { +export async function build_and_solve_mna(numNodes, chosenPlot, fullyConnectedComponents, componentValuesSolved, pyodide) { var i, vinNode, iinNode; //Are we plotting current or voltage? @@ -174,10 +133,10 @@ export async function build_and_solve_mna(numNodes, chosenPlot, fullyConnectedCo 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}`; } else if (el.type === "iprobe") { - mnaMatrix[numNodes + extraRow + numActives + iprbCounter][el.ports[0]] = "1"; - mnaMatrix[el.ports[0]][numNodes + extraRow + numActives + iprbCounter] = "1"; - mnaMatrix[numNodes + extraRow + numActives + iprbCounter][el.ports[1]] = "-1"; - mnaMatrix[el.ports[1]][numNodes + extraRow + numActives + iprbCounter] = "-1"; + 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"; + if (el.ports[1] != null) mnaMatrix[numNodes + extraRow + numActives + iprbCounter][el.ports[1]] = "-1"; + if (el.ports[1] != null) mnaMatrix[el.ports[1]][numNodes + extraRow + numActives + iprbCounter] = "-1"; iprbCounter++; } } @@ -204,36 +163,109 @@ export async function build_and_solve_mna(numNodes, chosenPlot, fullyConnectedCo else resIndex2.push(voutNode2 + 1, iinNode + 1); } } - var numericResult, textResult, mathml, complex_response, numericText; + var textResult, mathml; - if (solver === "algebrite") { - [textResult, mathml, complex_response] = await solveWithAlgebrite(nerdStr, mnaMatrix, resIndex, resIndex2); - } else { - [textResult, mathml, complex_response, numericResult, numericText] = await solveWithSymPy(nerdStr, mnaMatrix, elementMap, resIndex, resIndex2, componentValuesSolved, pyodide); - } + [textResult, mathml] = await solveWithSymPy(nerdStr, mnaMatrix, elementMap, resIndex, resIndex2, componentValuesSolved, pyodide); - return [textResult, mathml, complex_response, numericResult, numericText]; + return [textResult, mathml]; } export async function calcBilinear(solver) { - if (solver) { - const sympyString = ` + const sympyString = ` T, Z = symbols("T Z") bilinear = result_simplified.subs(s,(2/T)*(Z-1)/(Z+1)) bilinear_simp = simplify(bilinear) str(bilinear_simp), mathml(bilinear_simp, printer='presentation') `; - const [res, mathml] = await solver.runPythonAsync(sympyString); - // console.log("bilinear transform", res, mathml); + const [res, mathml] = await solver.runPythonAsync(sympyString); + // console.log("bilinear transform", res, mathml); - return [res, removeFenced(mathml)]; - } else { - Algebrite.eval("bilinear = subst((2/T)*(Z-1)/(Z+1),s,mna_vo_vi)"); - try { - return simplify_algebra(Algebrite.eval("bilinear").toString()); - } catch (err) { - console.log(err); - return ["", "Having trouble calculating bilinear transform"]; + return [res, removeFenced(mathml)]; +} + +export async function new_calculate_tf(pyodide, fRange, numSteps, componentValuesSolved, setErrorSnackbar) { + if (!pyodide) return { freq_new: [], mag_new: [], phase_new: [], numericML: "", numericText: "" }; + + // Generate frequency array (logarithmic steps) + var fstepdB_20 = Math.log10(fRange.fmax / fRange.fmin) / numSteps; + var fstep = 10 ** fstepdB_20; + const freq = []; + for (var f = fRange.fmin; f < fRange.fmax; f = f * fstep) { + freq.push(f); + } + + 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 + const sympyString = ` + +result_numeric = result_simplified.subs(${JSON.stringify(componentValuesSolved).replaceAll('"', "")}) +result_numeric_simplified = simplify(result_numeric) + +# Calculate numeric MathML and text representation +numeric_mathml = mathml(result_numeric_simplified, printer='presentation') +numeric_text = str(result_numeric_simplified) + +# Create a lambdified numeric function for fast evaluation +# This converts the symbolic expression to a numeric function that can be evaluated much faster +numeric_func = lambdify(s, result_numeric_simplified) + +# Evaluate for all frequencies using the fast numeric function +freq_array = ${JSON.stringify(freq)} +mag_array = [] +phase_array = [] + +for f in freq_array: + s_val = 2 * pi * f * 1j # Use Python's 1j for complex number + result_complex = numeric_func(s_val) + # Convert to plain Python complex to avoid SymPy/Pyodide overhead + result_complex = complex(result_complex) + # Use manual magnitude calculation which is faster than abs() on Pyodide proxies + mag_val = (result_complex.real**2 + result_complex.imag**2)**0.5 + # Use Python's built-in cmath.phase() for fast numeric calculation + phase_val = cmath.phase(result_complex) + mag_array.append(mag_val) + phase_array.append(phase_val) + +# Convert to plain Python floats to avoid Pyodide proxy issues +# Ensure all values are plain floats (not sympy objects) +mag_list = [float(x) for x in mag_array] +phase_list = [float(x) for x in phase_array] + +(numeric_mathml, numeric_text, mag_list, phase_list) +`; + const result = await pyodide.runPythonAsync(sympyString); + // Pyodide returns a tuple as a proxy object - access elements by index + if (!result || result.length !== 4) { + throw new Error(`Unexpected result from Python: length=${result?.length}`); + } + const numericML = result[0]; + const numericText = result[1]; + const mag = result[2]; + const phase = result[3]; + + // Convert Pyodide arrays to JavaScript arrays - extract values immediately to avoid proxy exhaustion + const magArray = []; + const phaseArray = []; + for (let i = 0; i < mag.length; i++) { + magArray.push(Number(mag[i])); } + for (let i = 0; i < phase.length; i++) { + phaseArray.push(Number(phase[i])); + } + return { + freq_new: freq, + mag_new: magArray, + phase_new: phaseArray, + numericML: removeFenced(String(numericML)), + numericText: String(numericText).replaceAll("**", "^"), + }; + } catch (err) { + setErrorSnackbar((x) => { + if (!x) return true; + else return x; + }); + console.log("Error calculating transfer function:", err); + return { freq_new: [], mag_new: [], phase_new: [], numericML: "", numericText: "" }; } } diff --git a/src/pyodideLoader.js b/src/pyodideLoader.js index 39ff792..703468f 100644 --- a/src/pyodideLoader.js +++ b/src/pyodideLoader.js @@ -13,8 +13,10 @@ export async function initPyodideAndSympy() { // await micropip.install('sympy'); await pyodide.loadPackage("sympy"); var initStr = ` -from sympy import Matrix, symbols, simplify, Abs, I, fraction, solve -from sympy.printing.mathml import mathml`; +from sympy import Matrix, symbols, simplify, Abs, I, fraction, solve, arg, pi, lambdify +from sympy.printing.mathml import mathml +import math +import cmath`; await pyodide.runPythonAsync(initStr); return pyodide; } diff --git a/src/simplify_algebra.js b/src/simplify_algebra.js deleted file mode 100644 index 67f0f4d..0000000 --- a/src/simplify_algebra.js +++ /dev/null @@ -1,23 +0,0 @@ -// import Expression from "../js/expression/Expression.js"; -// import "../js/expression/complex.js"; -// import ExpressionParser from "../js/expression/ExpressionParser.js"; -// import Polynomial from "../js/expression/Polynomial.js"; -// import "../js/expression/polynomial-roots-finding.js"; -// import "../js/expression/toMathML.js"; - -// import {ExpressionParser} from './node_modules/@yaffle/expression/index.js'; -import { ExpressionParser, Polynomial, Expression } from "@yaffle/expression"; - -export default function simplify_algebra(expr) { - var z = expr.replace(/([CRL]+)([0-9]*)/g, "$1_$2"); //Swap R0 for R_0 so this new library can consume it - var matrix = ExpressionParser.parse(z); - var t = matrix.toString(); //Converts to a latex string - var t2 = t.replace(/([CRL]+)_([0-9]*)/g, "$1$2"); //Swap R_0 for R0 so this new library can consume it - - // console.log(matrix.toMathML()); - - // console.log(matrix.toMathML()) - // var zz = t.replace(/([a-zA-Z]+)_([0-9]*)/g,"$1$2") - // var matrix = ExpressionParser.parse('-1/(ab(-1/(ab)-1/(ac)-1/(bc)))-1/(ac(-1/(ab)-1/(ac)-1/(bc)))'); - return [t2, matrix.toMathML()]; -} diff --git a/tests/circuit.test.js b/tests/circuit.test.js index a32eb20..e0f4588 100644 --- a/tests/circuit.test.js +++ b/tests/circuit.test.js @@ -1,7 +1,7 @@ // const { calculateImpedance } = require('../src/impedanceFunctions.js'); import { expect, test } from "vitest"; import { initPyodideAndSympy } from "./../src/pyodideLoader.js"; -import { build_and_solve_mna } from "../src/new_solveMNA.js"; +import { build_and_solve_mna, new_calculate_tf } from "../src/new_solveMNA.js"; const pyodide = await initPyodideAndSympy(); @@ -33,18 +33,19 @@ test("voltage in current probe - 1", async () => { R0: 10000, C0: 1.0000000000000002e-14, }; - const [textResult, _mathml, complex_response, _numericResult, numericText] = await build_and_solve_mna(3, ["Y0"], components, values, pyodide, "sympy"); + const [textResult, _mathml] = await build_and_solve_mna(3, ["Y0"], 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); - expect(complex_response).toEqual("1.0e-24*s**2/sqrt(1.0e-40*s**4 - 1.0e-20*s**2 + 1)"); + // expect(complex_response).toEqual("1.0e-24*s**2/sqrt(1.0e-40*s**4 - 1.0e-20*s**2 + 1)"); expect(numericText).toEqual("1.0e-20*s^2/(1.0e-16*s^2 + 1.0e-6*s + 10000)"); }); -test("voltage in current probe (algebrite) - 1", async () => { +test("voltage in current probe - 2", async () => { const components = { - Y0: { + R0: { ports: [0, 1], - type: "iprobe", + type: "resistor", }, vin: { ports: [0], @@ -54,9 +55,9 @@ test("voltage in current probe (algebrite) - 1", async () => { ports: [0, 2], type: "inductor", }, - R0: { + Y0: { ports: [1, 2], - type: "resistor", + type: "iprobe", }, C0: { ports: [2, null], @@ -68,13 +69,14 @@ test("voltage in current probe (algebrite) - 1", async () => { R0: 10000, C0: 1.0000000000000002e-14, }; - const [textResult, _mathml, complex_response] = await build_and_solve_mna(3, ["Y0"], components, values, null, "algebrite"); - expect(textResult).toEqual("C0*L0*s^2/(R0+C0*L0*R0*s^2+L0*s)"); + const [textResult, _mathml] = await build_and_solve_mna(3, ["Y0"], 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); - expect(complex_response).toEqual("abs(C0)*abs(s)/((1-2*C0*R0^2/L0+C0^2*R0^2*s^2+R0^2/(L0^2*s^2))^(1/2))"); + // expect(complex_response).toEqual("1.0e-24*s**2/sqrt(1.0e-40*s**4 - 1.0e-20*s**2 + 1)"); + 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 () => { +test("voltage in current probe - 3", async () => { const components = { R0: { ports: [0, 1], @@ -89,7 +91,7 @@ test("voltage in current probe - 2", async () => { type: "inductor", }, Y0: { - ports: [1, 2], + ports: [2, null], type: "iprobe", }, C0: { @@ -100,13 +102,12 @@ test("voltage in current probe - 2", async () => { const values = { L0: 0.000001, R0: 10000, - C0: 1.0000000000000002e-14, + C0: 1e-14, }; - const [textResult, _mathml, complex_response, _numericResult, numericText] = await build_and_solve_mna(3, ["Y0"], components, values, pyodide, "sympy"); - expect(textResult).toEqual("C0*L0*s**2/(C0*L0*R0*s**2 + L0*s + R0)"); - // expect(_mathml).toEqual(null); - expect(complex_response).toEqual("1.0e-24*s**2/sqrt(1.0e-40*s**4 - 1.0e-20*s**2 + 1)"); - expect(numericText).toEqual("1.0e-20*s^2/(1.0e-16*s^2 + 1.0e-6*s + 10000)"); + const [textResult, _mathml] = await build_and_solve_mna(3, ["Y0"], 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"); }); test("current in current probe - 2", async () => { @@ -137,10 +138,11 @@ test("current in current probe - 2", async () => { R0: 10000, C0: 1.0000000000000002e-14, }; - const [textResult, _mathml, complex_response, _numericResult, numericText] = await build_and_solve_mna(3, ["Y0"], components, values, pyodide, "sympy"); + const [textResult, _mathml] = await build_and_solve_mna(3, ["Y0"], 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); - expect(complex_response).toEqual("1.0e-10*s/sqrt(1.0e-20*s**2 + 1)"); + // expect(complex_response).toEqual("1.0e-10*s/sqrt(1.0e-20*s**2 + 1)"); expect(numericText).toEqual("1.0e-6*s/(1.0e-6*s + 10000)"); }); @@ -167,10 +169,11 @@ test("current in current probe VCIS - 3", async () => { G0: 0.001, R0: 1000000, }; - const [textResult, _mathml, complex_response, _numericResult, numericText] = await build_and_solve_mna(2, ["Y0"], components, values, pyodide, "sympy"); + const [textResult, _mathml] = await build_and_solve_mna(2, ["Y0"], 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); - expect(complex_response).toEqual("0.999000999000999"); + // expect(complex_response).toEqual("0.999000999000999"); expect(numericText).toEqual("0.999000999000999"); }); @@ -207,9 +210,10 @@ test("voltage in voltage probe VCVS", async () => { A0: 100, R0: 1000000, }; - const [textResult, _mathml, complex_response, _numericResult, numericText] = await build_and_solve_mna(3, ["X0"], components, values, pyodide, "sympy"); + const [textResult, _mathml] = await build_and_solve_mna(3, ["X0"], 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); - expect(complex_response).toEqual("Abs(1.0e-13*s**2 - 100000000)/(1000000*sqrt(1.0e-42*s**4 - 1.999999999e-21*s**2 + 1))"); //FIXME - this seems to be a sympy bug + // expect(complex_response).toEqual("Abs(1.0e-13*s**2 - 100000000)/(1000000*sqrt(1.0e-42*s**4 - 1.999999999e-21*s**2 + 1))"); //FIXME - this seems to be a sympy bug expect(numericText).toEqual("(1.0e-13*s^2 + 100000000)/(1.0e-15*s^2 + 1.0e-9*s + 1000000)"); }); diff --git a/toDo.md b/toDo.md index e5c77d6..4ae7129 100644 --- a/toDo.md +++ b/toDo.md @@ -4,9 +4,7 @@ # visiojs todo -- delete wire, redraw wire, then undo breaks - Move the matrix solver into a web worker. So if can be killed with a timeout -- Use sympy only, move the frequency solving in there too (so it cal calcuate abs more reliably) ## New todo with visiojs @@ -30,6 +28,12 @@ - if add op amp with zero on input and LC on output, and change input to current source, it crashes +# Done Dec 2025 + +- Remove algebrite +- Move solving into Sympy, then can get mag and phase +- Use sympy only, move the frequency solving in there too (so it cal calcuate abs more reliably) + # Done October 2025 - Fix Algebrite which got broken when Sympy moved to async function diff --git a/vite.config.js b/vite.config.js index c92cd90..5abb8d7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,11 +7,11 @@ import svgr from "vite-plugin-svgr"; export default defineConfig({ test: { browser: { + enabled: true, + provider: "playwright", instances: [ { - enabled: true, browser: "chromium", // or 'firefox', 'webkit' - provider: "playwright", }, ], },