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(``);
- // setBilinearRaw(raw);
- setResults({ ...results, bilinearRaw: raw, bilinearMathML: `` });
+ setBilinearResults({
+ bilinearML: ``,
+ bilinearText: raw,
+ });
}
// console.log(results);
@@ -334,14 +325,13 @@ function App() {
{results.text != "" && (
<>
- {results.numericText != null && (
-
- )}
-
+
- {results.bilinearMathML == "" ? (
+
+
+ {bilinearResults.bilinearML == "" ? (
) : (
-
+
)}
>
)}
diff --git a/src/ChoseTF.jsx b/src/ChoseTF.jsx
index d719d26..cf89da2 100644
--- a/src/ChoseTF.jsx
+++ b/src/ChoseTF.jsx
@@ -1,36 +1,40 @@
import Button from "@mui/material/Button";
-import Stack from "@mui/material/Stack";
-import ToggleButton from "@mui/material/ToggleButton";
-import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { build_and_solve_mna } from "./new_solveMNA.js";
-import { styled } from "@mui/material/styles";
-import Tooltip, { tooltipClasses } from "@mui/material/Tooltip";
import CircularProgress from "@mui/material/CircularProgress";
import { initPyodideAndSympy } from "./pyodideLoader";
-import { emptyResults } from "./common.js"; // Import the emptyResults object
-
-const HtmlTooltip = styled(({ className, ...props }) => )(({ theme }) => ({
- [`& .${tooltipClasses.tooltip}`]: {
- backgroundColor: "#f5f5f9",
- color: "rgba(0, 0, 0, 0.87)",
- maxWidth: 220,
- fontSize: theme.typography.pxToRem(12),
- border: "1px solid #dadde9",
- },
-}));
-
-function formatMathML(mathml, p, drivers) {
- return ``;
-}
+import { emptyResults, formatMathML } from "./common.js"; // Import the emptyResults object
export function ChoseTF({ setResults, nodes, fullyConnectedComponents, componentValuesSolved, setUnsolveSnackbar }) {
- const [algebraic, setAlgebraic] = useState("algebrite");
const [loading, setLoading] = useState(false);
+ const [calculating, setCalculating] = useState(false);
const [loadedPyo, setLoadedPyo] = useState(null);
- // const algebraic = "algebraic";
+
+ // Auto-initialize Pyodide on component mount
+ useEffect(() => {
+ let isMounted = true;
+ const initializePyodide = async () => {
+ setLoading(true);
+ try {
+ const pyodide = await initPyodideAndSympy();
+ if (isMounted) {
+ setLoadedPyo(pyodide);
+ setLoading(false);
+ }
+ } catch (err) {
+ console.error("Failed to initialize Pyodide:", err);
+ if (isMounted) {
+ setLoading(false);
+ }
+ }
+ };
+ initializePyodide();
+ return () => {
+ isMounted = false;
+ };
+ }, []);
const probes = [];
const drivers = [];
for (const c in fullyConnectedComponents) if (["vin", "iin"].includes(fullyConnectedComponents[c].type)) drivers.push(fullyConnectedComponents[c].type);
@@ -70,117 +74,63 @@ export function ChoseTF({ setResults, nodes, fullyConnectedComponents, component
);
})}
>
)}
- {/* 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 ``;
+}
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",
},
],
},