A native Windows desktop build of EngineeringPaper.xyz using PyWebView + native Python instead of Pyodide/WebAssembly. Calculations run via native Python with SymPy — faster startup, faster computation (3-5x faster), and a smaller footprint.
A web app for engineering calculations with:
- Automatic unit conversion and dimensional analysis
- Plotting and data tables
- Systems of equations
- Documentation cells with rich text
EngineeringPapyr replaces that entire stack:
| Aspect | Original Standalone | EngineeringPapyr |
|---|---|---|
| Window | System browser (Chrome/Firefox) | Native window (Edge WebView2) |
| Python runtime | Pyodide (WASM, ~92MB) | Native Python |
| Computation engine | SymPy via Pyodide | SymPy via native Python |
| SymPy startup | ~10-15s (WASM load + init) | ~2-5s (native import) |
| Frontend | Svelte + KaTeX (same) | Svelte + KaTeX (same) |
| Frontend assets | ~165MB (includes pyodide/) | ~73MB (no pyodide/) |
| Code completion | Jedi via Pyodide Worker | Jedi via native Python |
| Packaging | Node.js exe via pkg | Embedded Python 3.12 + launcher exe |
All features are preserved: math cells, documentation cells, system solve cells, EVA/RSS analysis cells, code cells, plot cells, fluid cells, data tables, DOCX export (native pandoc via pypandoc), and PDF export (print dialog). Code cells can now import any pip-installed Python package on the computer.
Additional features maintained from EngineeringPaperStandalone
Math cells have an optional annotation column to the right for units descriptions, notes, or labels (e.g., "velocity", "kg/m^3").
- Click a math cell to reveal the annotation input
- Saves automatically and persists with the sheet
- Included in Markdown/DOCX export as
*[annotation]*
Finds worst-case min/max of an output expression by evaluating all 2^n combinations of input parameter bounds, plus sensitivity analysis. Supports up to 25 parameters (2^25 = 33,554,432 combinations) — made practical by using lambdify to convert SymPy expressions into fast NumPy functions, evaluating millions of combinations in seconds instead of hours.
- Define parameters on the sheet (e.g.,
V = 10 [V],R = 1000 [Ω],I = V / R =) - Insert an EVA cell
- Set the Query field to the expression to evaluate (e.g.,
I=) - Add parameter rows with Parameter name, Min, and Max values2
How it works:
- Evaluates all 2^n combinations of min/max bounds (max 25 parameters)
- Shows nominal value plus extreme min and max
- Sensitivity analysis: varies each parameter while holding others at midpoint, reports percentage contribution (sorted highest to lowest)
Statistical tolerance analysis cell that computes the RSS error envelope. Unlike EVA's worst-case (all tolerances at extremes simultaneously), RSS assumes parameter variations are independent and combines them as root-sum-of-squares.
- Define parameters on the sheet
- Insert an RSS cell
- Set the Query field to the expression to evaluate
- Add parameter rows with Parameter name, Min, Nominal, and Max values
How it works:
- Evaluates the query at all-nominal to get the baseline output
- For each parameter, computes the output deviation when that parameter moves to its worst-case limit (all others held at nominal)
- RSS total = sqrt(delta_1^2 + delta_2^2 + ... + delta_n^2)
- RSS Min = nominal - RSS total, RSS Max = nominal + RSS total
- Sensitivity: reports % of RSS variance (delta_i^2 / sum of all delta^2) per parameter, sorted highest first
Why % of RSS variance (not arithmetic %):
In RSS, errors combine as variances (squares). A source contributing 50% of RSS variance contributes sqrt(0.5) = 71% of the RSS voltage. Ranking by variance contribution correctly identifies which tolerance to tighten for maximum RSS reduction.
When RSS is valid:
- Error sources are independent (separate physical components or mechanisms)
- Error sources are uncorrelated (no shared cause — e.g., two resistors on different packages)
- Transfer function is approximately linear over the tolerance range
When RSS is NOT valid (use EVA instead):
- Parameters are correlated (e.g., resistors from the same reel track together)
- A single root cause drives multiple parameters (e.g., a supply rail feeding multiple stages)
- The transfer function is highly nonlinear over the tolerance range
The included Example.epxyz file demonstrates both EVA and RSS analysis on an op-amp non-inverting amplifier with divided feedback. It models V_OUT as a function of resistor tolerances (R_lower, R_upper), bias current (I_B), offset current (I_OS), and offset voltage (V_OS), showing worst-case bounds, statistical bounds, and per-parameter sensitivity.
Open it from the app via the file open button or drag and drop.
The templates/ folder contains ready-to-use .epxyz design worksheets:
- Buck Converter — Step-down converter power stage
- Boost Converter — Step-up converter power stage
- Buck-Boost Converter — Inverting converter power stage
- Power Supply Topologies — 20 topologies side-by-side (duty cycle, FET stress, diode stress) with a topology selection guide and control methods reference
- ADC Resolution — An ADC TUE resolution example
Open any template from the app, modify the input parameters, and all derived values update automatically.
A built-in Code Cell function for snapping calculated values to standard component series (0.1% resistors, 1% resistors, capacitors, inductors).
Insert a code cell and place this function inside to use it (SymPy Mode is only used if evaluating symbolic arguments, not numeric).
Code Cell Function Definition:
SelectStd([any], [any]) = [any]
Code area:
from standard_parts import (
std_res_01, std_res_1, std_cap, std_ind,
select_nearest, select_up, select_down
)
# Mode: 1=RES 0.1%, 2=RES 1%, 3=CAP, 4=IND
# Negative for next value DOWN, e.g. -1 = RES 0.1% round down
# Add 10 for next value UP, e.g. 11 = RES 0.1% round up
_SERIES = {1: std_res_01, 2: std_res_1, 3: std_cap, 4: std_ind}
def calculate(target, mode):
mode = int(mode)
if mode > 10:
series = _SERIES[mode - 10]
return select_up(target, series)
elif mode < 0:
series = _SERIES[-mode]
return select_down(target, series)
else:
series = _SERIES[mode]
return select_nearest(target, series)Then in math cells:
R_std = SelectStd(4870, 1) = ← nearest 0.1% resistor
R_up = SelectStd(4870, 11) = ← next 0.1% resistor up
R_down = SelectStd(4870, -1) = ← next 0.1% resistor down
C_std = SelectStd(0.0000047, 3) = ← nearest standard capacitor
A built-in Code Cell function for checking component stresses against derating rules with automatic SMD package selection. Provide operating conditions (current for resistors, voltage for capacitors) and the module calculates stress, picks the smallest passing package, and outputs a CSV stress report.
Derating defaults: 80% voltage, 60% power. Resistor voltage rating uses the minimum of the package rating or the power-limited voltage (sqrt(P_rated * R)). To customize derating, set the module-level constants before calling resistor()/capacitor():
import part_stress
part_stress.VOLTAGE_DERATING = 0.70 # 70% voltage derating (default 0.80)
part_stress.POWER_DERATING = 0.50 # 50% power derating (default 0.60)All inputs must be in SI base units (Amps, Volts, Ohms, Farads). Math cells with units (e.g. 0.42 [mA]) auto-convert to SI before reaching the code cell. If entering raw numbers without units, use SI values directly (e.g. 0.42e-3 for 0.42 mA).
Code Cell Function Definition:
EPSA(I_{R1}, I_{R2}, V_{Cin}) = [text]
Code area:
from part_stress import resistor, capacitor, stress_report
def calculate(i_r1, i_r2, v_cin):
return stress_report(
resistor("R1", 10e3, "1%", i_r1),
resistor("R2", 4.7e3, "1%", i_r2),
capacitor("C_IN", 100e-9, "10%", v_cin),
)Then in math cells, define the operating conditions:
I_{R1} = 0.42 [mA]
I_{R2} = 5 [mA]
V_{Cin} = 12 [V]
The output is a CSV report grouped by component type, one row per part:
Resistors
Ref,Package,Value(ohm),Tolerance,Current(mA),V_rated(V),V_derated(V),V_actual(V),V_stress,V_status,P_rated(mW),P_derated(mW),P_actual(mW),P_stress,P_status
R1,0201,10000,1%,0.42,15,12,4.2,0.35,PASS,50,30,1.764,0.059,PASS
R2,1206,4700,1%,5,34.28,27.42,23.5,0.857,PASS,250,150,117.5,0.783,PASS
Capacitors
Ref,Package,Value(pF),Tolerance,V_rated(V),V_derated(V),V_actual(V),V_stress,V_status
C_IN,0402,100000,10%,16,12.8,12,0.938,PASS
To force a specific package instead of auto-selecting: resistor("R1", 10e3, "1%", i_r1, package="0805").
The function scales to any number of components — expand the code cell signature and calculate() arguments to match your design. For example, a design with 5 resistors and 3 capacitors:
Code Cell Function Definition:
EPSA(I_{R1}, I_{R2}, I_{R3}, I_{R4}, I_{R5}, V_{C1}, V_{C2}, V_{C3}) = [text]
Code area:
from part_stress import resistor, capacitor, stress_report
def calculate(i_r1, i_r2, i_r3, i_r4, i_r5, v_c1, v_c2, v_c3):
return stress_report(
resistor("R1", 10e3, "1%", i_r1),
resistor("R2", 4.7e3, "1%", i_r2),
resistor("R3", 1e3, "5%", i_r3),
resistor("R4", 100e3, "1%", i_r4),
resistor("R5", 220, "1%", i_r5),
capacitor("C1", 100e-9, "10%", v_c1),
capacitor("C2", 10e-6, "20%", v_c2),
capacitor("C3", 1e-6, "10%", v_c3),
)- Node.js
- Git for Windows (includes Git Bash, required for building npm dependencies)
- Python 3.10 to 3.12 with pip (best compatibility with scientific packages)
- Edge WebView2 runtime (pre-installed on Windows 11)
cd C:\<PathToFolder>\EngineeringPapyr
py -3.12 -m pip install -r requirements.txtWindows users: Run npm install from Git Bash, not PowerShell or cmd. Some dependencies require bash to build.
cd C:\<PathToFolder>\EngineeringPapyr\frontend
npm install
npm run build:nativeIf you get a cross-env not found error: npm install cross-env
Output goes to frontend/public/.
To clean build artifacts before rebuilding:
cd C:\<PathToFolder>\EngineeringPapyr\frontend
rm -rf public/buildTo clear the WebView2 browser cache (e.g., if the app shows stale images or assets after an update), close the app and delete:
%APPDATA%\pywebview\EBWebView
This folder is recreated automatically on next launch.
Running directly from Python gives code cells full access to your Python environment — any package you pip install is immediately available for import.
py -3.12 python/main.pyOr double-click Run.bat from the repo root.
Builds a self-contained directory with an embedded Python 3.12 distribution, all pip dependencies, and a small launcher exe. Code cells have full access to the embedded Python environment — any package installed into it is available for import.
py -3.12 build.pyOutput: dist/EngineeringPapyr/ directory and dist/EngineeringPapyr-vX.X.X.zip
The build script:
- Builds the frontend (
npm run build:native) - Downloads and sets up the Python 3.12 embeddable distribution
- Installs all pip dependencies from
requirements.txtinto the embedded Python - Copies app source, frontend, and data files
- Compiles a small launcher exe via PyInstaller
- Creates a zip for distribution
Run dist/EngineeringPapyr/EngineeringPapyr.exe to launch the app. No system Python installation is required.
To install additional Python packages into the embedded environment after building:
dist\EngineeringPapyr\python-3.12\python.exe -m pip install <package>The package is immediately available for import in code cells on the next app launch. No rebuild is required.
For iterating on frontend changes:
- Terminal 1:
cd frontend && npm run dev:native(watches + rebuilds on save) - Terminal 2:
cd .. && py -3.12 python/main.py(launch app, restart manually after frontend rebuild)
PyWebView window (Edge WebView2)
|
|-- Loads Svelte frontend from local files (frontend/public/)
|-- JS calls window.pywebview.api.solve_sheet(json)
|-- JS calls window.pywebview.api.export_docx(json)
|
v
Native Python (python/api.py)
|-- solve_sheet() -> dimensional_analysis.py (SymPy)
|-- get_code_context() -> jedi_code_analysis.py (Jedi)
|-- export_docx() -> pypandoc (native pandoc)
|-- get_python_info() -> importlib.metadata
|-- LRU cache (100 entries, replaces QuickLRU in JS)
The JS-Python boundary is 100% JSON strings in both directions. PyWebView runs API methods in background threads, so long computations don't freeze the UI.
| File | Purpose |
|---|---|
python/main.py |
PyWebView entry point, creates window |
python/api.py |
JS API bridge (solve_sheet, get_code_context, export_docx, get_python_info) |
python/dimensional_analysis.py |
Core computation engine (SymPy, ~4800 lines) |
python/jedi_code_analysis.py |
Code cell autocomplete via Jedi |
frontend/src/App.svelte |
Main app (calls window.pywebview.api instead of Web Workers) |
frontend/src/jediWrapper.ts |
Jedi bridge (PyWebView API instead of Worker) |
frontend/rollup.config.js |
Build config (no pyodide/jedi worker entries) |
launcher.py |
Tiny launcher script, compiled into the launcher exe |
pyinstaller_launcher.spec |
PyInstaller config for the launcher exe only |
build.py |
Build orchestrator (frontend + embedded Python + launcher) |
- SymPy first import takes 2-5 seconds — the app shows "Loading Python..." during this, normal behavior
- CoolProp fails to install — skip it initially (
pip installthe rest manually), only needed for fluid cells npm installslow — expected, the mathlive/plotly GitHub dependencies take time- Python 3.13/3.14 — may have issues with scientific packages; use 3.10-3.12 for best compatibility
MIT license, same as original.
See the original EngineeringPaper.xyz project for license information.
