MicroPython and full Python
The console has two Pythons. MicroPython is already in the page and starts in milliseconds; it runs most scripts. Full Python (Pyodide, CPython compiled to WebAssembly) runs what MicroPython cannot, such as numpy, scipy and matplotlib, and is downloaded only when a script needs it and you agree. Both call the same engine.
The two runtimes#
| MicroPython | Full Python | |
|---|---|---|
| What it is | MicroPython 1.24.1, a small Python 3, compiled to WebAssembly | Pyodide 314.0.7: CPython 3.14.2 compiled to WebAssembly |
| Download | none extra: part of the page, loaded when the console first opens | 12.9 MB, plus the packages a script imports, after you agree (sizes) |
import cufsm_rs gives | cufsm_rs lite: the package's names, signatures and defaults, written for MicroPython | the published package's own Python code (cufsm-rs-py 0.1.0), unchanged, with its compiled part replaced by the page's engine |
| Arrays | Array: nested lists with numpy's shape and indexing | numpy ndarray |
cufsm_rs.plot | drawn by the page (SVG in the page's colours), returns a Figure | matplotlib, returns the Axes |
| Other modules | json, math, cmath, time, sys, array, collections, random, struct, re, io, itertools, functools, heapq, copy, operator, string, and the examples' dsm (MicroPython's versions: smaller than CPython's) | the standard library, numpy, scipy and matplotlib (with the packages matplotlib uses), and dsm |
| Start-up | a fresh interpreter per run, about 5 ms | a few seconds, once per session; then fresh globals per run |
What runs where#
All mechanics run in the Rust engine, in every mode. Section properties, stresses, first yield, the
eigenvalue solves, load factors, modes, signature-curve minima, the cFSM classification and the section templates all
come from cufsm.wasm, the same engine the analysis on screen uses. Python only organises the inputs,
packages the results and plots them.
What the Python layers do, exactly:
- Organise the inputs. Build
Modeltables, check their shapes, expandm_allinto one term list per length, fill a template's defaults. - Package the results. Stack each length's load factors and modes into arrays (NaN where fewer modes were
found), split a mode vector into
u,v,wandtheta, and evaluate a mode's longitudinal shape functions along the member (ModeShape.at), as the published package does. - Plot. The figures draw numbers the script already holds.
Your own script's arithmetic is yours: the examples' Direct Strength Method equations, for instance, are Python in
dsm.py, applied to buckling loads from the engine.
How the console picks one#
MicroPython first, cheapest test first. Full Python is used only when one of these says MicroPython cannot run the script.
- The import scan. The console reads the script's
importandfrom … importlines (text in strings and comments is skipped) and checks each top-level module against the list in the table above. One that is not on it (numpy,scipy,matplotlib,pandas, …) sends the script to full Python. - The compile check. MicroPython compiles the script without running it. A
SyntaxErrormeans syntax MicroPython does not have (some f-string forms,match, …), and the script goes to full Python. A typo is aSyntaxErrortoo, so a typo can bring up the download prompt (what to do). - The runtime fallback. The script runs on MicroPython. If it stops on something MicroPython
lacks, it runs again from the start on full Python. That is:
- an
ImportError(an import the scan could not see, such as one inside a function); - the lite package's own refusal, which names the full runtime (an
Arrayasked to do numpy arithmetic,ax=on a plot function); - a
NameErroron one of CPython's built-in names that MicroPython does not have; - an
AttributeErroron a module or a built-in type (a function MicroPython'smathorstrlacks); - a
TypeErrorsaying a built-in takes no such keyword (json.dumps(x, indent=2)).
ValueError, aNameErroron your own name) is not a reason to switch: it is shown as MicroPython's error. - an
Once full Python is loaded in the session, a script that needs it runs on it straight away. A script that MicroPython can run still runs on MicroPython.
The download prompt#
Nothing is downloaded without your agreement. When a script needs full Python and some of it is not yet on the device, the console stops and asks. The prompt says why (for example It imports numpy, which the console's MicroPython does not run), how much this script would download now, and the parts:
| Part | Size | When |
|---|---|---|
| Python 3.14.2 (Pyodide 314.0.7) | 12.9 MB | always, the first time |
| numpy | 2.8 MB | always: the cufsm_rs package needs it |
| matplotlib, with the 11 packages it uses | 9.8 MB | a script that imports matplotlib or uses cufsm_rs.plot |
| scipy | 13.4 MB | a script that imports scipy |
So a numpy script downloads 15.7 MB the first time, and one with numpy and matplotlib 25.6 MB; everything the console hosts is 38.9 MB. (MB here is 1,048,576 bytes, as the prompt counts.) Only what is not already on the device is counted, so a later script that adds scipy asks for 13.4 MB. Full Python is fetched from this site, the same place as the app, not from a third party: the official Pyodide 314.0.7 release files, pinned by size and SHA-256 when the site is built. Other packages of the Pyodide release are not hosted; a script that imports one is told so in the prompt, and its import fails on full Python too.
- Load full Python and run downloads the parts and runs the script on full Python. Starting it takes a few seconds; the status line says what is loading.
- Not now shows the script's MicroPython run instead: it stops where it needs full Python, with a note and the line marked.
- Always load it on this device without asking, ticked before Load, skips the prompt from then on, on this device.
Always, Remove and offline#
What full Python downloads is kept on the device, in a store of its own beside the app's offline copy. It stays when the app updates. After a full Python run, Remove full Python from this device appears under the output: it deletes that copy and the "always" choice, and the next script that needs full Python asks again.
Offline, full Python works with what is already on the device: a numpy and matplotlib script that has run once online runs again offline, with the same output. A script that needs a package not yet downloaded (scipy, say) needs the network for it. MicroPython needs nothing extra: it is part of the app's offline copy.
The runtime badge#
Every run says which Python ran it, in the badge above the output:
- Ran on: MicroPython;
- Ran on: full Python (Pyodide), with a line saying why, for example Full Python ran this script: it imports numpy. or Full Python ran this script: MicroPython stopped at line 12 (AttributeError: …).;
- after Not now: Ran on: MicroPython, with Not run on full Python (you chose Not now): … This is MicroPython's run.
One-way compatibility#
A script that runs on the console's MicroPython runs unchanged on full Python, and on CPython after
pip install cufsm-rs-py (with [plot] if it plots, and dsm.py beside it if it
imports dsm). The reverse does not hold: a numpy or scipy script does not run on MicroPython. To write a
script that runs everywhere:
- index results as
a[i][j], nota[i, j]ora[:, k]; - use loops or list comprehensions instead of whole-array arithmetic (
[2 * v for v in r.curve], not2 * r.curve); - use
len(a),list(a)ora.tolist(), nota.min()ora.max():min(r.curve)works on both; - call the plot functions without
ax=, and do not use what they return; - keep to the modules in MicroPython's list.
To tell the runtimes apart in a script: the lite package has cufsm_rs.__lite__ = True, and the
published package has no such attribute (getattr(cufsm_rs, "__lite__", False)).
Why results are Array objects in MicroPython#
MicroPython has no numpy, so where the package returns a numpy array the lite package returns an
Array: nested lists with the same shape and the same indexing. a[i] (a row, itself an
Array, or a number), a[i][j], negative indices, slices, len(a), iteration,
a.shape, a.ndim, a.tolist() and list(a) all work as on the
package's arrays, and a[i] = v or a[i][j] = v writes through, as numpy's views do.
It is deliberately not a list. On a list, a * 2 repeats the list and a + b
joins two lists, where numpy multiplies and adds: a script written for numpy would give a different answer without
any error. So an Array refuses whole-array arithmetic, comparisons, a[i, j] and the array
methods, with an error that names the full runtime; and that error is one the console's
fallback acts on, so such a script moves to full Python rather than failing.
import cufsm_rs as fsm
r = fsm.signature(fsm.stress(fsm.lipped_c(150, 64, 15, 1.5), P=1.0), [50.0, 100.0, 200.0])
print(r.load_factors.shape) # (3, 1)
print(r.load_factors[1][0]) # a number: works on both runtimes
print(min(r.curve)) # works on both
print([2 * v for v in r.curve]) # works on both
# r.curve * 2 on MicroPython: NotImplementedError naming the full runtime (numpy multiplies)
The same numbers#
Both runtimes call the same cufsm.wasm, and numbers cross between Python and the engine bit for bit,
so MicroPython and full Python get the same results to the last bit. The parity suite runs the five MicroPython
examples on both and finds no difference. What can differ is the printing: MicroPython's float formatting is
not correctly rounded, so a printed number can differ in its last digits (FAQ).
Against pip install cufsm-rs-py on your own machine, the console's results agree to a relative
1e-9 in the cases the parity suite checks (cufsm-rs-py 0.1.0; every example, the README quickstart, and several
hundred individual calls). The exceptions are known and come from the maths library, not the engine's code: the
browser's engine uses Rust's own sine, cosine and arctangent, and the native package uses the operating system's.
A last-bit difference there is amplified where the problem is ill-conditioned: load factors and modes at
half-wavelengths of very many times the widest strip, and the cFSM classification of an unsymmetric section. There
the two can differ by up to about 1e-4 relative (at a half-wavelength of a thousand times the widest strip); elsewhere the
difference is much smaller.