Analysis, Reports, and Plots
On this page
tako.analysis.* pulls typed values out of raw calculation results returned by tako.singlePoint/optimize/molecularDynamics/reactionPath/etc., plus two pure numeric helpers (reactionEnergy, lowerHull) for downstream thermochemistry. energyEv and reactionBarrier are convergence-guarded: they throw rather than silently reporting a number from an optimizer or NEB band that did not converge, so a script cannot accidentally report an unconverged energy as if it were final.
tako.plot.* covers two renderer families. stepSvg, heatmap, contour, scatter, and line are pure functions that return standalone, dependency-free SVG markup with no external assets, embeddable directly in a Markdown report. plotly instead returns a JSON figure spec string for the in-app interactive viewer: it renders nothing inside the script sandbox (Plotly needs a DOM the sandbox does not have), so a script writes the spec to a *.plotly.json file and the host renders it when that file is opened. heatmap and contour share a z[y][x] grid convention with row 0 plotted at the bottom.
tako.report.* assembles plain-text deliverables from data: csv serializes a column/row table, and markdown assembles a title/intro/image/sections/tables/files document. Both return plain strings; the caller writes them into the workspace with tako.write or tako.output.
tako.analysis.energyEv
tako.analysis.energyEv(result: unknown) => number
Extracts the final energy in eV from a calculation result, throwing on an engine error or a non-converged result rather than returning an untrustworthy number.
| Parameter | Type | Default | Contract |
|---|---|---|---|
result | unknown | — | A resolved result object from tako.singlePoint/optimize/molecularDynamics/phonon/vibration/stda/xrd/etc. |
Returns: number — energy in eV.
- Checked in order: result.energy_ev / energyEv / final_energy_ev / finalEnergyEv (used directly); else result.energy_hartree / energyHartree / final_energy_hartree / finalEnergyHartree (converted with *27.211386245988); else result.energy.ev; else, if result.frames is a non-empty array, recurses into the last frame.
- Throws ‘Calculation result did not include an energy in eV.’ if result is not a record or none of the above fields are present as finite numbers.
- Throws first if the record carries an engine error payload (message/detail/reason/error fields, or ok:false / success:false / status: “error” | “failed”).
- Throws when result.converged === false (message includes final_fmax_ev_per_angstrom when present) — strict equality, so a result with no converged field at all (e.g. a plain single point) is NOT treated as unconverged.
const result = await tako.optimize(structure, { calculator });
const energyEv = tako.analysis.energyEv(result);
tako.analysis.finalStructure
tako.analysis.finalStructure(input: Structure, result: unknown) => Structure
Extracts the final structure produced by a calculation result, falling back to the input structure when no final geometry is present.
| Parameter | Type | Default | Contract |
|---|---|---|---|
input | Structure | — | The structure passed into the calculation; used as the atom-shape template when the result only carries frame data, and returned unchanged as the last-resort fallback. |
result | unknown | — | A resolved calculation result. |
Returns: Structure — a deep clone (JSON round-tripped), never the same object as input.
- Checks result.final_structure / result.finalStructure / result.structure in that order for anything shaped like a Structure (has atoms/elements/elementCounts arrays) and returns a clone of the first match.
- Otherwise, if result.frames is a non-empty array, clones input as the base and overlays the last frame’s atoms onto it by index: each atom field (element/x/y/z/fx/fy/fz/layerId) that the frame atom provides overwrites the base atom’s value, and any field the frame omits keeps the base atom’s value (or a default for atoms beyond the base’s length); frame.cell replaces the base cell when present.
- If result is not record-shaped and has none of the above, returns input as-is (not a clone) — never throws.
const result = await tako.optimize(structure, { calculator });
await tako.output('output/final.extxyz', tako.analysis.finalStructure(structure, result));
tako.analysis.reactionBarrier
tako.analysis.reactionBarrier(result: unknown, options?: { allowUnconverged?: boolean }) => { barrierEv: number; reactionEnergyEv: number; saddleImage: number; energiesEv: number[]; relativeEnergiesEv: number[]; converged: boolean; residualEvPerAngstrom: number | null }
Convergence-guarded extractor for tako.reactionPath results: the barrier height, reaction energy, saddle-image index, and per-image energies.
| Parameter | Type | Default | Contract |
|---|---|---|---|
result | unknown | — | A resolved tako.reactionPath result; must carry both result.neb and result.reaction_barrier. |
options.allowUnconverged | boolean | false | When true, returns the barrier data with converged: false instead of throwing on an unconverged band. |
Returns: { barrierEv, reactionEnergyEv, saddleImage, energiesEv, relativeEnergiesEv, converged, residualEvPerAngstrom }: barrierEv (eV) — highest image energy minus the first-endpoint (reactant) energy; reactionEnergyEv (eV) — final-endpoint energy minus first-endpoint energy (positive = endothermic, same sign convention as tako.analysis.reactionEnergy); saddleImage — index of the highest-energy image used for the barrier summary; energiesEv — absolute energy of every image, eV; relativeEnergiesEv — image energies relative to the first endpoint, eV; converged — result.neb.converged === true; residualEvPerAngstrom — result.neb.residual_ev_per_angstrom, or null when absent/non-finite.
- Throws ‘Value is not a tako.reactionPath result.’ if result is not record-shaped.
- Throws first if the record carries an engine error payload (same check as tako.analysis.energyEv).
- Throws ‘Value is not a tako.reactionPath result: expected result.neb and result.reaction_barrier.’ if either object is missing — this is also what happens for a reactionInterpolateOnly result, which has no reaction_barrier at all.
- When the band did not converge (neb.converged !== true) and options.allowUnconverged is not true, throws with the residual (when available) and instructs the caller to pass { allowUnconverged: true } and to state the convergence status in any report.
- After the convergence gate, throws individually for any of reaction_barrier.barrier_ev / reaction_energy_ev / saddle_image / energies_ev / relative_energies_ev that is missing or non-finite.
const result = await tako.reactionPath(initial, { calculator, reactionFinalStructure: final });
const { barrierEv, converged } = tako.analysis.reactionBarrier(result);
tako.analysis.reactionEnergy
tako.analysis.reactionEnergy(input: { products: number[]; reactants: number[] }) => number
Computes a reaction energy in eV as the sum of product-side energies minus the sum of reactant-side energies.
| Parameter | Type | Default | Contract |
|---|---|---|---|
input.products | number[] | — | Product-side energies in eV (one entry per product species/phase). |
input.reactants | number[] | — | Reactant-side energies in eV. |
Returns: number — eV. Positive means the products are higher in energy than the reactants (endothermic); negative means exothermic.
- Plain summation with no validation: empty arrays sum to 0, and a non-finite entry propagates unchecked into the result.
- Sign convention used by Tako’s own battery-voltage scripts: voltageV = -reactionEnergyEv / transferredCharge, with the sodiated/lithiated phase as the product and the bare host + metal reference as reactants.
const reactionEnergyEv = tako.analysis.reactionEnergy({
products: [sodiatedResult.energyEv],
reactants: [hostResult.energyEv, naPerAtomEv],
});
tako.analysis.lowerHull
tako.analysis.lowerHull(points: Array<{ label: string; x: number; y: number }>) => { points: HullPoint[]; stable: HullPoint[]; rejected: HullPoint[]; segments: Array<{ from: string; to: string; dx: number; dy: number; slope: number }> }
Computes the lower convex hull of labeled (x, y) points (e.g. composition vs. energy) to identify thermodynamically stable compositions along a hull of segments.
| Parameter | Type | Default | Contract |
|---|---|---|---|
points | Array<{ label: string; x: number; y: number } & Record<string, unknown>> | — | Points to hull; each requires a non-empty label and finite x/y. Extra properties on a point pass through untouched into the returned points/stable/rejected arrays. |
Returns: { points, stable, rejected, segments }: points — the input sorted by (x, then y, then label); stable — the points on the lower hull, in x order (the stable compositions); rejected — every point not on the lower hull, restored to original input order; segments — one entry per consecutive pair of stable points: { from, to, dx, dy, slope } with slope = dy/dx (e.g. eV per ion for a composition-vs-energy hull).
- Throws ‘lowerHull points require finite x and y values.’ if any point’s x or y is not finite.
- Throws ‘lowerHull points require a label.’ if any point’s label is falsy (including an empty string).
- When two or more points share the same x (within 1e-9), only the lowest-y point at that x competes for the hull; every other same-x point goes straight to rejected.
const hull = tako.analysis.lowerHull([
{ label: 'Na4', x: 4, y: -98 }, { label: 'Na5', x: 5, y: -103 }, { label: 'Na6', x: 6, y: -104 },
]);
tako.plot.stepSvg
tako.plot.stepSvg(input: { xLabel: string; yLabel: string; yValue: number; label?: string }) => string
Renders a single flat step-line SVG for one scalar metric (e.g. a computed voltage), positioned along a small fixed-range axis.
| Parameter | Type | Default | Contract |
|---|---|---|---|
input.xLabel | string | — | X-axis caption. |
input.yLabel | string | — | Y-axis caption. |
input.yValue | number | — | The scalar value to plot. |
input.label | string | 'Value' | Caption drawn next to the value at the top-right of the plot. |
Returns: string — standalone
- The y-axis is a FIXED, hardcoded range with tick labels 2.0/3.0/4.0, not autoscaled to yValue: values below ≈1.94 clamp to the plot’s bottom edge and values above ≈4.78 clamp to its top edge instead of rescaling further. This generator is tuned for small known-range metrics like a computed voltage, not a general-purpose scalar.
- The printed title text is
${label} ${yValue.toFixed(2)}. - No input validation or throwing.
await tako.write('output/metric.svg', tako.plot.stepSvg({
xLabel: 'reaction', yLabel: 'voltage / V', yValue: voltageV, label: 'Na intercalation',
}), { mimeType: 'image/svg+xml' });
tako.plot.heatmap
tako.plot.heatmap(input: { z: number[][]; x?: number[]; y?: number[]; xLabel?: string; yLabel?: string; title?: string; colorscale?: 'viridis' | 'plasma' | 'inferno' | 'turbo'; colorbarLabel?: string; width?: number; height?: number }) => string
Renders a 2D scalar grid as a colored cell heatmap with axes and a colorbar, as a standalone SVG. The primary presenter for a 2D potential-energy surface or scan.
| Parameter | Type | Default | Contract |
|---|---|---|---|
input.z | number[][] | — | Required, non-empty rectangular grid z[row][col] (every row the same length). Row maps to y, column maps to x. |
input.x | number[] | — | Per-column axis values; length must equal z[0].length. Defaults to 0..nx-1. |
input.y | number[] | — | Per-row axis values; length must equal z.length. Defaults to 0..ny-1. |
input.xLabel | string | — | X-axis caption. |
input.yLabel | string | — | Y-axis caption. |
input.title | string | — | Plot title, also used as the SVG aria-label when set. |
input.colorscale | 'viridis' | 'plasma' | 'inferno' | 'turbo' | 'viridis' | Sequential color interpolator for cell fill and colorbar. |
input.colorbarLabel | string | — | Units/quantity caption drawn alongside the colorbar (e.g. “energy / kcal mol-1”). |
input.width | number | 720 | SVG viewBox width in px. |
input.height | number | 480 | SVG viewBox height in px. |
Returns: string — standalone
- Throws ‘tako.plot.heatmap requires a non-empty rectangular z grid’ if z is missing, empty, or ragged; throws if x/y length does not match the grid; throws if any z/x/y value is not finite.
- Row 0 is plotted at the BOTTOM of the chart (rows increase upward) — the conventional orientation for a 2D scan/PES, opposite of normal array row order.
- The color domain is the flattened z grid’s min/max (a perfectly flat grid gets a +1 domain pad so the interpolator never divides by zero).
- Axis tick count is capped at 8 per axis (always including the last index) so long axes stay legible.
await tako.write('output/pes.heatmap.svg', tako.plot.heatmap({
z: zKcalMol, x: bondLengthsAngstrom, y: anglesDegrees,
xLabel: 'O-H bond length / A', yLabel: 'H-O-H angle / deg', title: 'Water 2D PES',
}), { mimeType: 'image/svg+xml' });
tako.plot.contour
tako.plot.contour(input: { z: number[][]; x?: number[]; y?: number[]; thresholds?: number; xLabel?: string; yLabel?: string; title?: string; colorscale?: 'viridis' | 'plasma' | 'inferno' | 'turbo'; colorbarLabel?: string; width?: number; height?: number }) => string
Renders a 2D scalar grid as filled contour bands with outlines (d3-contour marching squares), as a standalone SVG.
| Parameter | Type | Default | Contract |
|---|---|---|---|
input.z | number[][] | — | Required, non-empty rectangular grid z[row][col], at least 2 rows and 2 columns. Row maps to y, column maps to x. |
input.x | number[] | — | Per-column axis values; length must equal z[0].length. Defaults to 0..nx-1. |
input.y | number[] | — | Per-row axis values; length must equal z.length. Defaults to 0..ny-1. |
input.thresholds | number | 10 | Number of contour bands (rounded to an integer). A value that is undefined or not > 0 falls back to 10. |
input.xLabel | string | — | X-axis caption. |
input.yLabel | string | — | Y-axis caption. |
input.title | string | — | Plot title, also used as the SVG aria-label when set. |
input.colorscale | 'viridis' | 'plasma' | 'inferno' | 'turbo' | 'viridis' | Sequential color interpolator for band fill and colorbar. |
input.colorbarLabel | string | — | Units/quantity caption drawn alongside the colorbar. |
input.width | number | 640 | SVG viewBox width in px. |
input.height | number | 480 | SVG viewBox height in px. |
Returns: string — standalone
- Same rectangular-grid / axis-length / finite-value checks as tako.plot.heatmap, plus ‘tako.plot.contour requires a z grid with at least 2 rows and 2 columns.’
- Same z[y][x] grid convention and row-0-at-bottom orientation as tako.plot.heatmap.
- Contour geometry is computed in grid-index space (0..nx, 0..ny) by running d3-contour on the row-major-flattened grid, then mapped onto axis pixel space — thresholds are evenly spaced levels across the value range, not caller-specified level values.
await tako.write('output/pes.contour.svg', tako.plot.contour({
z: zKcalMol, x: bondLengthsAngstrom, y: anglesDegrees, title: 'Water 2D PES',
}), { mimeType: 'image/svg+xml' });
tako.plot.scatter
tako.plot.scatter(input: { points: Array<{ x: number; y: number; label?: string }>; xLabel?: string; yLabel?: string; title?: string; width?: number; height?: number }) => string
Renders a labeled x/y point cloud (e.g. a reaction-coordinate or correlation scan) as a standalone SVG scatter plot.
| Parameter | Type | Default | Contract |
|---|---|---|---|
input.points | Array<{ x: number; y: number; label?: string }> | — | Required, non-empty array of points; label optionally annotates a point. |
input.xLabel | string | — | X-axis caption. |
input.yLabel | string | — | Y-axis caption. |
input.title | string | — | Plot title, also used as the SVG aria-label when set. |
input.width | number | 640 | SVG viewBox width in px. |
input.height | number | 480 | SVG viewBox height in px. |
Returns: string — standalone
- Throws ‘tako.plot.scatter requires a non-empty points array’ if points is missing/empty; throws if any point’s x or y is not finite.
- Axis domains are the data’s x/y extent, padded about 8% on each side (or ±1 / ±10% around a degenerate single value) so edge points are not clipped against the axes.
- Point labels are drawn as plain, unrotated text offset up-and-right of the marker; there is no collision avoidance for overlapping labels.
await tako.write('output/scan.svg', tako.plot.scatter({
points: [{ x: 1.2, y: -4.1, label: 'A' }, { x: 1.5, y: -3.8 }],
xLabel: 'distance / A', yLabel: 'energy / eV',
}), { mimeType: 'image/svg+xml' });
tako.plot.line
tako.plot.line(input: { series: Array<{ name?: string; points: Array<[number, number]> }>; xLabel?: string; yLabel?: string; title?: string; width?: number; height?: number }) => string
Renders one or more x/y series (e.g. scans along a single coordinate) as a standalone multi-series SVG line plot with a shared house color palette.
| Parameter | Type | Default | Contract |
|---|---|---|---|
input.series | Array<{ name?: string; points: Array<[number, number]> }> | — | Required; at least one series must have a non-empty points array of [x, y] pairs. name labels the series in the legend (defaults to “Series N”, 1-based). |
input.xLabel | string | — | X-axis caption. |
input.yLabel | string | — | Y-axis caption. |
input.title | string | — | Plot title, also used as the SVG aria-label when set. |
input.width | number | 640 | SVG viewBox width in px. |
input.height | number | 480 | SVG viewBox height in px. |
Returns: string — standalone
- Throws ‘tako.plot.line requires at least one series with points’ if no series has a non-empty points array; throws ‘tako.plot.line requires [x, y] pairs in series points’ if a point is not a 2-element array; throws if any x/y is not finite.
- Axis domains are computed over ALL series’ points combined (one shared pair of axes), padded the same way as tako.plot.scatter.
- Series colors cycle through the house Okabe-Ito palette (the same TAKO_PLOT_PALETTE used as tako.plot.plotly’s default colorway) by series index, wrapping after 8 series.
await tako.write('output/scan.svg', tako.plot.line({
series: [{ name: 'GFN2', points: [[0, -1.2], [0.1, -1.5], [0.2, -1.1]] }],
xLabel: 'coordinate', yLabel: 'energy / eV',
}), { mimeType: 'image/svg+xml' });
tako.plot.plotly
tako.plot.plotly(input: { data: Array<Record<string, unknown>>; layout?: Record<string, unknown>; config?: Record<string, unknown> }) => string
Builds an interactive Plotly figure spec (a standard Plotly.newPlot data/layout/config triple) as a JSON string, for writing to a *.plotly.json artifact that opens in the in-app Plotly viewer.
| Parameter | Type | Default | Contract |
|---|---|---|---|
input.data | Array<Record<string, unknown>> | — | Required, non-empty array of Plotly trace objects (type, x, y, z, colorscale, … — any valid Plotly trace shape). |
input.layout | Record<string, unknown> | — | Plotly layout object (title, axes, colorbar, scene, margin, …). |
input.config | Record<string, unknown> | — | Plotly config object (e.g. { responsive: true }). |
Returns: string — JSON-serialized { $schema: “tako-plotly-v1”, data, layout?, config? }.
- Throws ‘tako.plot.plotly requires a non-empty data array of Plotly traces.’ if data is missing or empty.
- Unlike the SVG generators, this renders NOTHING inside the script sandbox (Plotly needs a DOM). Write the returned string to a path ending in
.plotly.jsonwith{ mimeType: 'application/x-tako-plotly' }; the host recognizes a file as a Plotly artifact by either the .plotly.json extension or that mimeType, and its viewer parses and renders the spec on the main thread when the file is opened. - The viewer merges a house theme (Inter font, white background, the same 8-color Okabe-Ito colorway the SVG line generator uses, 2x-scale PNG export) UNDER whatever layout/config the script supplies — the script’s own layout/config keys always win, recursing only into nested plain objects.
- Also write a static SVG (heatmap/contour/line/scatter) alongside an interactive Plotly figure so a report renders without requiring the viewer to be opened.
await tako.write('output/pes.plotly.json', tako.plot.plotly({
data: [{ type: 'surface', x, y, z }], layout: { title: 'PES' },
}), { mimeType: 'application/x-tako-plotly' });
tako.report.csv
tako.report.csv(input: { columns: string[]; rows: Array<Array<string | number | null>> }) => string
Serializes a column/row table to CSV text.
| Parameter | Type | Default | Contract |
|---|---|---|---|
input.columns | string[] | — | Header row. |
input.rows | Array<Array<string | number | null>> | — | Data rows; a null cell becomes an empty CSV field. |
Returns: string — CSV text (one line per row, header first) with a trailing newline.
- A cell is quoted (with embedded double quotes doubled) only when it contains a comma, double quote, or newline; other cells are emitted as plain String(value).
- No validation that each row’s length matches columns.length.
await tako.write('output/results.csv', tako.report.csv({
columns: ['phase', 'energy_ev'],
rows: [['Na metal', -1.23], ['host', -45.6]],
}), { mimeType: 'text/csv' });
tako.report.markdown
tako.report.markdown(input: { title: string; intro?: string; image?: { alt: string; path: string }; sections?: Array<{ title: string; body: string }>; tables?: Array<{ title: string; columns: string[]; rows: Array<Array<string | number | null>> }>; files?: string[] }) => string
Assembles a structured Markdown report (title, intro, image, sections, tables, file list) into a single Markdown string.
| Parameter | Type | Default | Contract |
|---|---|---|---|
input.title | string | — | Rendered as # title. |
input.intro | string | — | A paragraph inserted verbatim right after the title. |
input.image | { alt: string; path: string } | — | One  image reference inserted after the intro. |
input.sections | Array<{ title: string; body: string }> | — | Each becomes a ## title heading followed by body inserted verbatim (body is itself Markdown and may contain figures, math, or fenced diagrams). |
input.tables | Array<{ title: string; columns: string[]; rows: Array<Array<string | number | null>> }> | — | Each becomes a ## title heading and a Markdown table. |
input.files | string[] | — | Rendered as a ## Files bulleted list of [file](file) links. |
Returns: string — the assembled Markdown document (all blocks joined with newlines).
- Only table cells (header and data rows) are escaped:
|becomes\|and embedded newlines become<br>. title/intro/section bodies/file paths are inserted with NO escaping, so a section body may itself contain Markdown or HTML. - Each generated table’s separator row left-aligns the first column and right-aligns every other column (
---then---:× (columns.length - 1)) — the right-alignment assumes trailing columns are numeric. - A null table cell renders as an empty string.
- intro, image, sections, tables, and files are each independently optional — omit any of them to skip that block entirely.
await tako.write('analysis.md', tako.report.markdown({
title: 'Na intercalation voltage',
intro: `Voltage: ${voltageV.toFixed(2)} V`,
files: ['output/summary.json'],
}), { mimeType: 'text/markdown' });
Return to the Tako Script API index.