oderom.pages.dev computes curvature. Give it a manifold, a coordinate chart and a metric, and it returns Christoffel symbols, the Riemann and Ricci tensors, and invariant scalars — in closed form, as algebra, with no numerical approximation anywhere.
You need a browser. That is the whole list. No download, no installer, no account, no administrator rights, no Python, no LaTeX installation. It works the same in a university lab, on a borrowed laptop, and on a machine where you are not allowed to install anything.
The computation runs on your machine. The engine is compiled to WebAssembly and executes inside your own browser tab. Nothing you type is sent to a server; there is no account, no upload, and nothing to leak. The consequence is that speed depends on your machine, and that closing the tab loses whatever you have not saved.
Open the page. It arrives with an example already loaded — the Reissner–Nordström metric, a charged black hole — in four blocks, none of them executed:
manifold M dim 4
bundle TM on M dim 4
chart schw on M coords (t, r, theta, phi)
metric g on schw bundle TM {
[t,t] = -(1 - 2*M/r + Q^2/r^2),
[r,r] = 1/(1 - 2*M/r + Q^2/r^2),
[theta,theta] = r^2,
[phi,phi] = r^2 * sin(theta)^2
}
kretschmann
Click in the first block and press Shift+Enter four times, once per block. The last one prints the Kretschmann scalar:
(-96*M*Q^2*r + 48*M^2*r^2 + 56*Q^4)/r^8
On screen it arrives typeset as a real fraction with real exponents; the line above is the same result written out flat.
That is the closed form from the textbook, computed from the metric you can see on screen. Set Q = 0 in the metric, run again, and it collapses to 48M²/r⁶ — Schwarzschild.
The page is a stack of blocks. You edit a block and run it; the result appears underneath, typeset.
| Key | What it does |
|---|---|
| Shift+Enter | Run this block, move to the next one (creating one if this was the last). |
| Ctrl+Enter | Run this block and stay in it. This is the one for iterating. |
| Alt+Enter | Run this block and insert a new one right below. |
The number in brackets on the left is the execution counter.
[ ] means never run; [3] means this was the
third block you ran. It follows the order you ran things in, not the
order they sit on the page.
Nothing recomputes by itself. Opening a file runs nothing. Editing runs nothing. Clicking runs nothing. Only the three keys above run a block, and only the block you pressed them in. If a result is on screen, you asked for it.
When you edit a block that has already run, it turns stale: amber counter, a stripe, and a label above the old result. The result stays readable — it is simply marked as belonging to text that no longer exists. Every executed block below it goes stale too, because they may depend on what you changed. Nothing is recomputed and nothing is deleted; run the block again when you want it fresh.
At the bottom of the stack there is always an empty block with a dashed border. It is there to be typed in.
The dropdown at the left of the header switches the interface between Português, English, Italiano and Français. Your choice is remembered the next time you open the page.
It changes the buttons, the panels and the status bar. It does not change error messages, which stay in Portuguese: they name what you wrote wrong, and they come from the engine, not the interface. If you hit one and cannot read it, chapter 10 covers the ones you are most likely to meet.
The Gallery button opens a list of known spacetimes.
Clicking one pastes its declarations as new, editable blocks at the end
of your notebook — the same text you would have typed, never a metric
hidden behind a button. Nothing runs; the blocks arrive as
[ ], ready for you.
| Entry | What it is | What you should get |
|---|---|---|
| Schwarzschild | Vacuum, static, spherically symmetric | Rab = 0; Kretschmann = 48M²/r⁶ |
| Reissner–Nordström | Charged, spherically symmetric | R = 0 but Rab ≠ 0 |
| Kerr | Rotating black hole, Boyer–Lindquist coordinates | Rab = 0 in all ten independent components |
| FRW | Spatially flat cosmology, generic scale factor a(t) | R = 6(a″/a + a′²/a²) |
| De Sitter | Constant positive curvature | R = 12H²; Weyl identically zero |
| Anti-de Sitter | Constant negative curvature, Poincaré coordinates | R = −12H²; Rab = −3H²gab |
Every entry carries symbolic parameters — M, Q,
a, H. To fix one, edit the pasted text, or write
M := 1 in a block of its own before the metric.
Each of these was checked against a known closed form before it entered the list. They are a good way to confirm the engine is doing what you expect before you trust it on a metric of your own.
Four declarations, in this order. Each can live in its own block or share one; the notebook reads them top to bottom.
manifold M dim 4
bundle TM on M dim 4
chart schw on M coords (t, r, theta, phi)
metric g on schw bundle TM {
[t,t] = -(1 - 2*M/r),
[r,r] = 1/(1 - 2*M/r),
[theta,theta] = r^2,
[phi,phi] = r^2 * sin(theta)^2
}
| Line | What it says |
|---|---|
manifold M dim 4 | A 4-dimensional manifold called M. |
bundle TM on M dim 4 | The bundle that carries the tensor indices. |
chart schw on M coords (...) | The coordinates. Their number must equal the manifold's dimension. |
metric g on schw bundle TM { ... } | The components. Only the ones you write; everything else is zero. |
Inside the braces, one row per component: [i,j] = expression,
comma-separated, no comma after the last. The indices are coordinate
names from your chart.
| You want | You write |
|---|---|
| arithmetic | a + b, a - b, a * b, a / b; juxtaposition also multiplies (2M) |
| powers | r^2, r^-1, r^{-2} — the exponent is always a whole number |
| trigonometry | sin(theta), cos(theta) |
| hyperbolic | sinh(chi), cosh(chi) |
| exponential | exp(H*t) |
| Greek letters | spell them: theta, phi, chi. LaTeX (\theta, \frac{a}{b}) is also accepted. |
| a shorthand | f := 1 - 2*M/r in its own block, then f anywhere below |
| an unknown function | f(r), a(t) — the engine differentiates it as f'(r) without you defining it |
Those five — sin, cos, exp,
sinh, cosh — are the functions the engine
knows. Others are handled deliberately rather than silently:
f, a,
h) becomes an unknown function: opaque, but
correctly differentiated. This is how the FRW entry writes
a(t).tan, sqrt, log,
arcsin) is refused, not accepted as
unknown. This matters: before that rule existed,
tan(theta) was accepted and differentiated as an opaque
tan', never sec² — a result that looked plausible and was
wrong. A loud refusal beats a quiet wrong answer.For a metric with no metric at all — a bare connection — you can declare the Christoffel symbols directly:
connection Gamma on flat2 {
[x,y,y] = x
}
christoffel, riemann and ricci
work from that. scalar and the other invariants do not, and
say so: they need a metric to raise an index with.
A block whose first word is one of these is a question. Write it alone, on its own line, and run it.
| Write | You get |
|---|---|
christoffel | The Christoffel symbols Γabc. |
riemann | The Riemann tensor, fully covariant. |
ricci | The Ricci tensor Rab. |
scalar | The Ricci scalar R — one expression. |
kretschmann | RabcdRabcd — the invariant that stays non-zero in vacuum. |
einstein | The Einstein tensor Gab. |
weyl, weylsquare | The conformal tensor and its invariant. Undefined in 2 dimensions, and it says so. |
riccisquare, gaussbonnet | RabRab, and the Gauss–Bonnet density. |
geodesic tau | The geodesic equations, one per coordinate. The trailing word is your affine parameter's name. |
accel tau | The same equations solved for each second derivative — the form a numerical integrator eats. |
Two refinements you will want eventually:
riemann [up,down,down,down]
raises the first index. One up or down per
index, and the count must match the rank exactly.kretschmann g2. With only one, you never need to.The affine parameter for geodesic/accel may
not collide with a coordinate or with a symbol already in your metric.
geodesic r against a chart that has r is refused,
because t(r) would mean two things at once.
Long results are truncated on screen. The notebook
shows the first 20 independent components and then a line saying how
many more there are — ... and 10 more independent components
(truncated) — plus how many are identically zero. The count is
always honest; only the display is shortened.
Click any line of a result and its LaTeX goes to your clipboard — just that one equation, clean, ready to paste into a document. Not the annotation beside it, not the summary line, not the whole block. A small "copied" badge confirms it.
The Export button writes a line of code for another program. Pick a format and a query, and it inserts something like
export sympy kretschmann
into a new block. It does not run it — you run it, with Shift+Enter, like anything else. The panel shows you the exact line before you click, which is the point: the second time, you can write it yourself. Both Mathematica and SymPy are available, and export works on any of the queries in chapter 7.
Exported SymPy comes with its symbols(...) line and with
reserved-word collisions renamed, so it runs where you paste it.
Everything so far computes components: you give a metric, you get expressions in r and θ. ODEROM has a second half, and it is available here too.
simplify works on tensor expressions where the indices
are just labels — no metric, no coordinates, no components. Declare a
tensor's symmetries with head, then ask whether an
expression collapses:
manifold M dim 4
bundle TM on M dim 4
head R : TM*, TM*, TM*, TM* symmetry (1 2)- (3 4)- (1 3)(2 4)+
and then, in another block:
simplify R[a,b,c,d] + R[b,a,c,d]
0
That is antisymmetry in the first pair doing its work. Some more:
| You write | You get | Why |
|---|---|---|
simplify R[a,b,c,d] - R[c,d,a,b] | 0 | The pair-exchange symmetry. |
simplify 3 R[a,b,c,d] + -1 R[a,b,c,d] | 2 R[a,b,c,d] | Like terms collect. |
simplify R[a,b,c,d] + R[a,c,b,d] | both terms survive | Nothing declared makes them equal — and a simplifier that returned zero here would be worse than useless. |
Notation: tensors are written side by side, not multiplied with
* — g[a,b] R[b,c,d,e]. A repeated index is a
contraction. A semicolon introduces a covariant derivative:
T[a,b;c].
Multi-term identities — the Bianchi identities, metric
compatibility — must be declared, and in the notebook there is
no way to declare them yet. So a cyclic Bianchi sum will not collapse
here, even though it is zero. It collapses on the command line, where
those identities are flags. Making an .od document able to
declare an axiom is an open design question, not an oversight.
What you do get in the browser is canonicalisation under declared symmetries and like-term collection — which is most of what the verb does.
Save downloads a file. Type a name in the path field
first — that name is what the download gets, with .od
appended if you left it off. A browser page cannot write to a folder you
choose, so a download is what "save" means here.
Open uses your system's file picker. The path field is ignored for opening, deliberately: a page cannot read a file by path, only one you handed it, and pretending otherwise would be worse than the asymmetry. Cancelling the picker changes nothing and is not an error.
The file is plain text with %% on a line of its own
between blocks. It is the same format the desktop application reads and
writes, so a notebook moves between them untouched. Reopening always
comes back with every block not executed — consistent with
nothing recomputing by itself.
Closing the tab loses everything unsaved. There is no autosave and no server-side copy — that is the same property that means nothing you type ever leaves your machine. Save before you close.
While a block runs, the page keeps working: the other blocks accept focus and editing, and the running block offers Cancel. Only one block runs at a time; pressing a run key elsewhere is refused, with a flash on the status bar and on the block that is holding the engine.
There is no time limit in the browser. A calculation runs until it finishes or until you stop it. What does bound it is an expression-size ceiling (50 000 nodes), which stops a runaway before it exhausts your memory and reports rather than hanging forever.
Cancelling in the browser discards every result you have
computed so far — not just the block you cancelled. Your
text survives intact; the results do not, and every block
returns to [ ].
This is not a design choice, it is the platform: stopping WebAssembly mid-computation is only possible by terminating the worker thread it runs in, and the worker holds every result with it. Measured, not assumed: a test drives a real browser, counts four blocks with results before a cancel and zero after.
In practice: run the expensive query last, and if you expect to cancel, save first.
The language, the engine and the results are identical — the same Rust, compiled two ways. Four differences, all imposed by the browser:
| Here | On the desktop |
|---|---|
| Save downloads a file | Save writes to the path you typed |
| Open uses the file picker | Open reads the path you typed |
| Cancel discards all computed results | Cancel stops one block; other results stay |
| No time limit; a size ceiling instead | Same size ceiling |
Everything else — the keys, the block states, staleness, the gallery, Export, click-to-copy, the language selector — behaves the same in both.
The full manual (ODEROM-manual.pdf) covers what this
guide leaves out: the complete grammar, the command-line tools, the
error-message reference, the declared axioms for abstract-index work,
and nine worked examples. Read this one to get started; read that one
when you want to know exactly what the program will and will not do.
Two habits worth forming early, both of which this program is built around:
ODEROM · browser guide. Every behaviour described here was verified against the running program on 20 August 2026, including the cost of cancelling, which was measured in a real browser rather than assumed.