# Kinetic plasma on a tensor tree: problem-script specification > This page runs kinetic plasma simulations in the browser with the ttn.cpp tree tensor network (TTN) solver, compiled to WebAssembly in single precision. > A problem is an ES module that imports from "ttn", exports numeric `parameters`, and default-exports a function that returns `{ title, model, run, plot, slice, ... }`. > Paste a module into **Edit script** and press **Apply script**, or load one of the examples listed at the end. > This file is the complete reference for writing such modules; it is written so that a language model can produce a working script from it. ## Script contract ```js import { periodic, cartesian, species, maxwellian, normalized, maxwell, vlasov, run, ttn, automatic, fixedStep } from "ttn"; export const parameters = { nx: 32, nv: 64, dt: 0.01, steps: 500, capacity: 12 }; // numbers only; each becomes an input export const collisions = { model: "off", frequency: 2e-3 }; // optional: enables the page's collision control export default function ({ nx, nv, dt, steps, capacity }) { const x = periodic("x", nx, [0, 4 * Math.PI]), vx = periodic("vx", nv, [-6, 6]); const space = cartesian(x), velocity = cartesian(vx); const electrons = species("electrons", { charge: -1, mass: 1, velocity, collisions, initial: normalized(maxwellian(velocity), space.volume) }); const plasma = vlasov({ space, species: [electrons], fields: maxwell({ cSquared: 16 }) }); return { title: "Maxwellian", model: plasma, run: run(plasma).represent(electrons.f, ttn({ rank: automatic({ capacity }) })).time(fixedStep({ dt, steps })), plot: electrons.f, slice: [x, vx], stepsPerFrame: 2, }; } ``` - The page calls the default function with the script's `parameters`. Its parameter and collision controls rewrite the literal values in the `parameters` and `collisions` exports and rerun the script, so the text always shows the run. - `collisions` is `{ model: "off" | "dougherty" | "bgk", frequency, bgk? }`; `bgk` holds `bgk()` options and applies only while the model is `"bgk"`. - Return fields: - `title` (string), `model` (the object returned by `vlasov()` or `{ name, states, groups, observables }`), `run` (a `RunSpec`). - `plot`: a state or observable to draw as a heatmap. An observable needs `plotAxes`, its axes in index order. - `slice: [horizontalAxis, verticalAxis]`: the two plotted axes; other axes are cut at their midpoints. - Optional: `stepsPerFrame` (positive integer), `note` (string), `treeState` (state whose tree and ranks are drawn), `diagnostics` (object of role → observable; the default uses `model.mass` and `model.energy`), `charts` (`{ title, note, primary: { label, series: [{ role, label }] }, secondary: {...}, metric: { role, label } }`), `view: { kind: "annulus", radial, angle, innerRadius, label }` for a 2π-periodic angle axis. - Errors are thrown as `TtnError` with a path such as `species electrons.velocity`; the page shows them in the status line. ## Equation layer (`ttn`) Axes and frames: - `periodic(name, cells, [lower, upper], { component? })`: uniform periodic grid, points `lower + j h`. - `uniform(name, cells, [lower, upper])`: cell-centred grid, points `lower + (j + 1/2) h` (use for a magnetic moment on `[0, mu_max]`). - `cartesian(...axes)` or `cartesian({ x: axis, z: axis })`: a frame; `frame.axes`, `frame.size`, `frame.volume`. - `components`: the 3-component vector index used by fields such as `E` and `B`. States, expressions and equations: - Start an explicit equation model with `beginModel()` or `new Program()` inside the default function; `vlasov()` creates its own model. - `state(name, indices, { initial, representation? })`; `state.expression` or the state itself in arithmetic. - Arithmetic: `add(...)`, `sub(a, b)`, `mul(...)`, `neg(a)`, `div(a, number)`, `pow(a, integer)`, `dot(a, b)`, `cross(a, b)`, and the method forms `a.add(b)`, `a.mul(b)`, ... - Calculus: `diff(f, axis, { derivative: "spectral" | "moment-compatible" })`, `grad(f, axes)`, `curl(F, axes)`, `integrate(f, axes)`, `coordinates(axes)`, `component(F, "x")`, `inverseLaplacian(f)`. - Linear maps: `linearAxis(f, axis, rowMajorMatrix)`, `fourierMultiplier(f, { axes, indices, symbol: (kx, ky, ...batch) => number })`, `spectralDamping(f, axis, { rate, cutoff })`, `screenedPoisson({ source, reaction, axes, tolerance })`. - Static data: `coefficient(indices, (x, y, ...) => number)` (a fixed table usable in expressions), `sampled(indices, fn)` (dense initial values). - `ddt(state).eq(rhs)`, `group(name, ...equations)`, `observable(name, expression)`. Initial values (separable sums of products): - `profile(axis, fn)`, `samples(axis, values)`, `gaussian(axis, mean, sigma)`, `cosine(axis, k, { amplitude, offset, phase })`, `sine(...)`. - `product(...)`, `sum(...)`, `normalized(value, integral)`, `vector({ x, y, z })`, `zero()`, `checkpoint({ tree, ranks, cores })`. Collisions: - `dougherty(f, velocityAxes, frequency, { magneticMoment: { axis, field }, mass })` returns a right-hand-side term (conservative Dougherty / Lenard–Bernstein drag and diffusion). Velocity axes must be periodic with at least three nodes; a magnetic-moment axis must be `uniform` on `[0, mu_max]`; `field` is the magnetic field strength as an expression or `coefficient(...)`. - `bgk(name, f, velocityAxes, frequency, { magneticMoment, mass, relativeTolerance, momentTolerance, ... })` returns a relaxation group; advance it with `exactRelaxation`, usually as `strang(advance(collisions, exactRelaxation), kinetic)`. BGK needs a full (not delta-f) distribution and a static field strength. Its default tolerances (`relativeTolerance: 1e-5`, `momentTolerance: 1e-6`) suit float32. - With `vlasov()`, set `collisions: { model, frequency, bgk? }` on a species instead; the model adds the Dougherty term or the BGK half-steps itself. - Dougherty damps fine velocity filaments fastest. A weak frequency near `0.5 k h_v^3 / T` (wavenumber `k`, velocity spacing `h_v`, temperature `T`) removes grid-scale filaments with little effect on resolved physics. ## Physics layer - `species(name, { charge, mass, velocity: cartesian(...), initial, derivative?, terms?: (f, context) => expression, collisions? })`; `species.f` is its distribution after `vlasov()`. - `maxwellian(velocity, { density, drift, thermal })`: `thermal` is the standard deviation per axis. - Field models: `maxwell({ epsilon0, cSquared, electric, magnetic })`, `electrostatic({ form: "ampere" | "poisson", epsilon0, electric })`, `kineticAlfven({ perpendicular: [x, y], parallel: z, electrostatic, magnetic, tolerance })`. - `vlasov({ space, species, fields })` returns `{ name, states, groups, groupsByRole, scheme, mass, energy: { kinetic, electric, magnetic, field, total }, observables, fields }`. ## Numerics layer - `treeSampled([x, y, z, v], (x, y, z, v) => f0)`: construct a TTN initial state directly by on-demand native hierarchical cross on its tree, without forming the full grid. Requires TTN representation; provide a physically normalized function if needed. `sampled()` instead stores dense initial values. - `run(model).represent(state, ttn({ tree, rank }) | dense())`: `tree` is `"automatic"` or nested pairs of axes such as `[[x, y], [z, v]]`. - Rank policies: `automatic({ capacity })`, `adaptive({ capacity, relativeTolerance, initial, leafCapacity })`, `fixed({ rank, leafRank })`. The default capacity is 96; browser examples use 10–64. - `.transport(state, map)` with `maps.streaming({ space, velocity })`, `maps.characteristics({ space, velocity, refinement })`, `maps.gyromotion({ axes, frequency, acceleration })`, `maps.affine({ axes, generator, acceleration })`, `maps.shears({ periodic, shears })`, `maps.coordinates({ periodic, forward, inverse })`, `maps.compose(inner, outer)`. A map chooses moving coordinates; it does not add forces. - `.streaming(state, false)` disables automatic free-streaming extraction. - `.scheme(...)` with `advance(group, method)`, `strang(a, b)` and `compose(a, b)`. Methods: `lie`, `strang`, `rk4`, `psAbug`, `bugMidpoint`, `sgm`, `exactRelaxation`, `coupledMidpoint`, `pfg({ maximumCoreCoefficients, coreFlow })`. Without `.scheme`, the model's default scheme is used. - `.time(fixedStep({ dt, steps }) | fixedStep({ dt, finalTime }) | adaptiveStep({ initialDt, finalTime, tolerance, minimumDt, error }))`. - `.observe(resident(observable) | onRequest(state))`; the page adds what it plots and its diagnostics itself. ## Browser constraints - Single precision (float32). Tolerances below about `1e-6` relative are not meaningful. - Use explicit or mildly implicit integrators for low-stiffness collisions; stiff collisions need the native float64 runtime. - Large grids run, but each step must finish within a frame budget; prefer total phase-space sizes up to about `10^6` points and modest rank capacities. ## Notebook-style problem documentation The page first renders special JavaScript block comments as Markdown text cells. Begin a block with `/* @markdown` on its own line, write headings, prose, lists and LaTeX (`$...$` inline or `$$...$$` for display), and close it with `*/`. These remain legal comments in executable JavaScript. Put the governing equations and initial conditions here, next to the actual state/field definitions. The viewer can open the JavaScript input cell, edit the same source and run it again. HTML in documentation comments is displayed as text, not trusted markup. ## Run in place (chat artifact or any web page) `https://cdn.jsdelivr.net/npm/@tensortree/plasma/ttn.mjs` is one ES module with the WebAssembly runtime embedded (npm package [`@tensortree/plasma`](https://www.npmjs.com/package/@tensortree/plasma); jsDelivr serves its files). The same file is at `https://unpkg.com/@tensortree/plasma/ttn.mjs` and `https://itsuwari.github.io/vlasov/ttn.mjs`; use the jsDelivr URL in chat artifacts, which allow npm CDNs. Import it, call `await T.load()` once, then use the same API as a script, with `T.` in front of each name. `T.compile(run)` returns a simulation: `advance(n)`, `retreat(n)`, `time`, `steps`, `done`, `value(observable)`, `tensor(target)` (`{ indices, shape, data }`, last index fastest) and `dispose()`. Observe every state or observable you read with `.observe(...)` before compiling. The sandbox must allow scripts from the chosen host and WebAssembly compilation. A React artifact that animates Landau damping with D3 colors: ```jsx import { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; const RUNTIME = "https://cdn.jsdelivr.net/npm/@tensortree/plasma/ttn.mjs"; export default function Landau() { const canvas = useRef(null); const [status, setStatus] = useState("loading runtime"); useEffect(() => { let sim, frame, stopped = false; (async () => { const T = await import(/* @vite-ignore */ RUNTIME); await T.load(); const x = T.periodic("x", 48, [0, 4 * Math.PI]), vx = T.periodic("vx", 64, [-6, 6]); const space = T.cartesian(x), velocity = T.cartesian(vx); const electrons = T.species("electrons", { charge: -1, mass: 1, velocity, initial: T.normalized(T.maxwellian(velocity, { density: T.cosine(x, 0.5, { offset: 1, amplitude: 0.04 }) }), space.volume) }); const plasma = T.vlasov({ space, species: [electrons], fields: T.maxwell({ cSquared: 16, electric: T.vector({ x: T.product(-0.08, T.sine(x, 0.5)) }) }) }); sim = T.compile(T.run(plasma).represent(electrons.f, T.ttn({ rank: T.automatic({ capacity: 10 }) })) .time(T.fixedStep({ dt: 0.01, steps: 2000 })).observe(electrons.f)); const draw = () => { if (stopped) return; sim.advance(5); const { shape: [nx, nv], data } = sim.tensor(electrons.f); const color = d3.scaleSequential(d3.interpolateViridis).domain(d3.extent(data)); const context = canvas.current.getContext("2d"), image = context.createImageData(nx, nv); for (let i = 0; i < nx; ++i) for (let j = 0; j < nv; ++j) { const c = d3.rgb(color(data[i * nv + j])); image.data.set([c.r, c.g, c.b, 255], 4 * ((nv - 1 - j) * nx + i)); } context.putImageData(image, 0, 0); setStatus(`t = ${sim.time.toFixed(2)}`); if (!sim.done) frame = requestAnimationFrame(draw); }; draw(); })().catch((error) => setStatus(error.message)); return () => { stopped = true; cancelAnimationFrame(frame); sim?.dispose(); }; }, []); return (

{status}

); } ``` In a plain HTML page use `