1"""A Layer X run, exported (DATA-CONTRACT 4: ``GET /api/layerx/runs/{id}/export/{fmt}``).
3* :func:`to_csv` -- every per-step signal in the result, one row per twin step (``series.t``), one
4 column per signal, headed ``name [unit]``. The series, the delivered (replay) columns on the firing
5 steps, the replay's own points on their steps, the full network record and every diagnostics
6 column that follows the series. Floats are written with ``repr``, so they read back bit for bit.
7* :func:`to_parquet` -- the same table as Parquet (pyarrow), each field's unit in its metadata.
8* :func:`fea_bundle` -- a zip for structural and thermal analysis: ``pc_t.csv``, ``thrust_t.csv``,
9 ``heatflux_xt.csv`` (from the ``axial`` sidecar, when given), ``loads.json`` with the peak loads,
10 and ``README.txt`` with units and basis.
12Nothing is interpolated onto the twin's grid. A value sampled at a replay point is written on that
13point's step and left empty elsewhere: a table must not imply a resolution the model did not have.
14A block on its own clock that does not land on the twin's steps is not written to the table;
15:func:`columns` returns what was left out, and the Parquet file's metadata lists it
19from __future__
import annotations
27from typing
import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
29PSI = 6894.757293168361
32_SUFFIX_UNITS: Tuple[Tuple[str, str], ...] = (
34 (
"_MW_m2",
"MW/m^2"), (
"_kg_m3",
"kg/m^3"), (
"_m_s2",
"m/s^2"), (
"_mm_s",
"mm/s"), (
"_m_s",
"m/s"),
36 (
"_psia",
"psia"), (
"_psig",
"psig"), (
"_psi",
"psi"), (
"_pa",
"Pa"), (
"_Pa",
"Pa"), (
"_K",
"K"),
37 (
"_kg",
"kg"), (
"_N",
"N"), (
"_Ns",
"N*s"), (
"_hz",
"Hz"), (
"_Hz",
"Hz"), (
"_mm",
"mm"), (
"_m2",
"m^2"),
38 (
"_m3",
"m^3"), (
"_um",
"um"), (
"_ms",
"ms"), (
"_deg",
"deg"), (
"_cal",
"cal"), (
"_pct",
"%"), (
"_J",
"J"),
39 (
"_W",
"W"), (
"_L",
"L"), (
"_s",
"s"), (
"_m",
"m"), (
"_g",
"g0"),
42 "t":
"s",
"dt":
"s",
"mdot":
"kg/s",
"mdot_O":
"kg/s",
"mdot_F":
"kg/s",
"mdot_ox":
"kg/s",
43 "mdot_fuel":
"kg/s",
"capacity_mdot":
"kg/s",
"cstar":
"m/s",
"cstar_ideal":
"m/s",
"cstar_actual":
"m/s",
44 "isp":
"s",
"Isp":
"s",
"thrust":
"N",
"p":
"psia",
"T":
"K",
45 "v_ox":
"m/s",
"v_fuel":
"m/s",
"omega":
"rad/s",
"frequency":
"Hz",
"firing":
"bool",
"converged":
"bool",
46 "extrapolated":
"bool",
"choked":
"bool",
"wide_open":
"bool",
"summerfield":
"bool",
"schmucker":
"bool",
47 "replay_point":
"bool",
"in_start":
"bool",
52 """The unit a Layer X key carries, from the repo's naming convention (``*_psia``, ``*_K``, ...);
53 ``1`` for a key without a unit suffix, which in this result is a ratio or a fraction (ΔP/Pc,
54 O/F, ε, η, margins). Instrument and network columns pass their own unit explicitly."""
55 leaf = name.rsplit(
".", 1)[-1]
56 if leaf
in _NAME_UNITS:
57 return _NAME_UNITS[leaf]
58 for suffix, unit
in _SUFFIX_UNITS:
59 if leaf.endswith(suffix):
65 if isinstance(v, bool):
67 if isinstance(v, (int, float)):
69 return f
if math.isfinite(f)
else None
74 return isinstance(v, list)
and len(v) == n
and all(x
is None or isinstance(x, (int, float, bool))
for x
in v)
77def _walk(prefix: str, block: Any, n: int, out: List[Tuple[str, List[Any], Optional[str]]],
78 skipped: List[str], depth: int = 0) ->
None:
79 """Every per-step list of length ``n`` under ``block``, as ``(dotted name, values, unit)``."""
82 if isinstance(block, Mapping):
83 for key
in sorted(block, key=str):
84 if key
in (
"model",
"t",
"index"):
86 _walk(f
"{prefix}.{key}" if prefix
else str(key), block[key], n, out, skipped, depth + 1)
87 elif isinstance(block, list)
and block
and all(isinstance(x, Mapping)
for x
in block):
88 for k, item
in enumerate(block):
89 tag = str(item.get(
"id")
or item.get(
"line")
or item.get(
"tank")
or k)
90 _walk(f
"{prefix}.{tag}", item, n, out, skipped, depth + 1)
92 out.append((prefix, [
_cell(v)
for v
in block],
None))
93 elif isinstance(block, list)
and block
and all(x
is None or isinstance(x, (int, float, bool))
for x
in block):
94 skipped.append(prefix)
97def _place(n: int, rows: Sequence[int], values: Sequence[Any]) -> List[Any]:
98 col: List[Any] = [
None] * n
99 for i, v
in zip(rows, values):
105def _rows_of(times: Sequence[Any], t: Sequence[float], tol: float = 1e-6) -> Optional[List[int]]:
106 """The series step of each time, when every time is one of the series' steps; else None."""
110 if not isinstance(x, (int, float)):
112 while j < len(t)
and t[j] < x - tol:
114 if j >= len(t)
or abs(t[j] - x) > tol:
120def columns(result: Mapping[str, Any]) -> Tuple[List[Tuple[str, str, List[Any]]], List[str]]:
121 """``([(name, unit, values)], skipped)``: the export table, one entry per column, every column as
122 long as ``series.t``; and the dotted names of per-point blocks that were not on the twin's steps."""
123 series = result.get(
"series")
or {}
124 t = [float(v)
for v
in (series.get(
"t")
or [])]
128 cols: List[Tuple[str, str, List[Any]]] = []
129 skipped: List[str] = []
131 seen: Dict[str, int] = {}
133 def add(name: str, values: List[Any], unit: Optional[str] =
None) ->
None:
136 seen[name] = seen.get(name, 0) + 1
137 cols.append((name
if seen[name] == 1
else f
"{name}#{seen[name]}", unit
or unit_of(name), values))
139 add(
"t", [
_cell(v)
for v
in t],
"s")
140 core = (
"dt",
"firing",
"converged",
"copv_psia",
"copv_mass_kg",
"copv_wall_K")
143 add(key, [
_cell(v)
for v
in series[key]])
144 for reg, block
in sorted((series.get(
"regulators")
or {}).items()):
145 for key
in sorted(block):
147 add(f
"regulators.{reg}.{key}", [
_cell(v)
for v
in block[key]])
148 for iid, inst
in sorted((series.get(
"instruments")
or {}).items()):
149 if isinstance(inst, Mapping)
and _is_column(inst.get(
"values"), n):
150 tag = inst.get(
"tag")
or iid
151 add(f
"instruments.{iid}.{tag}", [
_cell(v)
for v
in inst[
"values"]], str(inst.get(
"unit")
or "1"))
152 for group
in (
"ox",
"fuel",
"chamber"):
153 found: List[Tuple[str, List[Any], Optional[str]]] = []
154 _walk(group, series.get(group)
or {}, n, found, skipped)
155 for name, values, unit
in found:
156 add(name, values, unit)
157 rest = [k
for k
in sorted(series)
if k
not in (
"t",
"regulators",
"instruments",
"ox",
"fuel",
"chamber") + core]
160 _walk(key, series[key], n, found, skipped)
161 for name, values, unit
in found:
162 add(name, values, unit)
165 firing = [i
for i, f
in enumerate(series.get(
"firing")
or [])
if f]
166 dv = result.get(
"delivered")
167 if isinstance(dv, Mapping)
and isinstance(dv.get(
"t"), list):
168 rows =
_rows_of(dv[
"t"], t)
or (firing
if len(firing) == len(dv[
"t"])
else None)
170 for key
in sorted(dv):
171 if key
in (
"t",
"summary")
or not _is_column(dv[key], len(rows)):
173 add(f
"delivered.{key}",
_place(n, rows, dv[key]))
175 skipped.append(
"delivered")
176 rp = result.get(
"replay")
177 if isinstance(rp, Mapping)
and rp.get(
"available")
and isinstance(rp.get(
"index"), list):
178 rows = [int(i)
for i
in rp[
"index"]]
179 for key
in sorted(rp):
180 if key
in (
"t",
"index",
"available")
or not _is_column(rp[key], len(rows)):
182 add(f
"replay.{key}",
_place(n, rows, rp[key]))
185 net = result.get(
"network")
186 if isinstance(net, Mapping):
187 for nid, node
in sorted((net.get(
"nodes")
or {}).items()):
188 for key
in (
"p_psia",
"T_K"):
189 if isinstance(node, Mapping)
and _is_column(node.get(key), n):
190 add(f
"network.nodes.{nid}.{key}", [
_cell(v)
for v
in node[key]])
191 for bid, br
in sorted((net.get(
"branches")
or {}).items()):
192 for key
in (
"mdot",
"dp_psi",
"state"):
193 if isinstance(br, Mapping)
and _is_column(br.get(key), n):
194 add(f
"network.branches.{bid}.{key}", [
_cell(v)
for v
in br[key]],
195 "kg/s" if key ==
"mdot" else "psi" if key ==
"dp_psi" else "1")
199 diag = result.get(
"diagnostics")
200 if isinstance(diag, Mapping):
201 for name
in sorted(diag):
203 if name ==
"vv" or not isinstance(block, (Mapping, list)):
205 _diagnostic(f
"diagnostics.{name}", block, t, add, skipped)
206 flight = result.get(
"flight")
207 if isinstance(flight, Mapping)
and isinstance(flight.get(
"stability"), Mapping):
208 _diagnostic(
"flight.stability", flight[
"stability"], t, add, skipped)
213 for key
in (
"timeseries",
"engine_check",
"cross_check",
"feed_fit"):
214 if isinstance(result.get(key), Mapping):
216 return cols, sorted(set(skipped))
221NOT_HISTORIES = frozenset({
"model",
"nyquist",
"tau_sweep",
"acoustic",
"contour",
"frames",
"soak",
"inputs",
222 "worst",
"settled_min",
"other_basis",
"check",
"feed",
"start",
"summary"})
225def _diagnostic(prefix: str, block: Any, t: Sequence[float], add: Any, skipped: List[str],
226 rows: Optional[List[int]] =
None, depth: int = 0) ->
None:
227 """Write the histories under a diagnostics block. A block carrying its own ``index`` (series
228 steps) or ``t`` (times that are series steps) places its lists on those steps, and its children
229 inherit that clock; a list on no known clock is listed in ``skipped``."""
233 if isinstance(block, list):
234 if block
and all(isinstance(x, Mapping)
for x
in block):
235 for k, item
in enumerate(block):
236 tag = str(item.get(
"id")
or item.get(
"line")
or item.get(
"tank")
or k)
237 _diagnostic(f
"{prefix}.{tag}", item, t, add, skipped, rows, depth + 1)
239 if not isinstance(block, Mapping):
241 own_t, index = block.get(
"t"), block.get(
"index")
242 if isinstance(index, list)
and index
and all(isinstance(i, int)
and not isinstance(i, bool)
for i
in index):
244 elif isinstance(own_t, list)
and own_t:
248 skipped.append(prefix)
250 for key
in sorted(block, key=str):
251 if key
in (
"t",
"index")
or key
in NOT_HISTORIES:
254 name = f
"{prefix}.{key}"
255 if isinstance(v, Mapping)
or (isinstance(v, list)
and v
and isinstance(v[0], Mapping)):
256 _diagnostic(name, v, t, add, skipped, rows, depth + 1)
258 if not isinstance(v, list)
or not v:
260 if isinstance(v[0], list):
263 if not all(x
is None or isinstance(x, (int, float, bool))
for x
in v):
265 if rows
is not None and len(v) == len(rows):
266 add(name,
_place(n, rows, v))
267 elif rows
is None and len(v) == n:
268 add(name, [
_cell(x)
for x
in v])
277 return f
"{name} [{unit}]"
280def to_csv(result: Mapping[str, Any]) -> str:
281 """Every per-step signal as CSV: a header row of ``name [unit]``, then one row per twin step.
282 Booleans are 1/0, undefined cells empty, floats ``repr`` (exact)."""
285 w = csv.writer(buf, lineterminator=
"\n")
286 w.writerow([
_header(name, unit)
for name, unit, _
in cols])
287 n = len(cols[0][2])
if cols
else 0
289 w.writerow([
"" if c[2][i]
is None else repr(c[2][i])
if isinstance(c[2][i], float)
else str(c[2][i])
291 return buf.getvalue()
294HEADER_RE = re.compile(
r"^(?P<name>.*) \[(?P<unit>[^\]]*)\]$")
297def read_csv(text: str) -> Dict[str, Tuple[str, List[Optional[float]]]]:
298 """:func:`to_csv`'s output back as ``{name: (unit, values)}``: the round trip the tests use and a
299 reader for anyone scripting against an export."""
300 rows = list(csv.reader(io.StringIO(text)))
303 heads = [HEADER_RE.match(h)
for h
in rows[0]]
304 out: Dict[str, Tuple[str, List[Optional[float]]]] = {}
305 for j, m
in enumerate(heads):
306 name, unit = (m.group(
"name"), m.group(
"unit"))
if m
else (rows[0][j],
"")
307 out[name] = (unit, [float(r[j])
if r[j] !=
"" else None for r
in rows[1:]])
312 """The CSV's table as Parquet: one float64 column per signal (booleans as 0/1), named as the CSV
313 names it without the unit; each field's ``unit`` in its metadata, and the run's identity and the
314 skipped blocks in the file's."""
316 import pyarrow.parquet
as pq
318 cols, skipped =
columns(result)
319 fields, arrays = [], []
320 for name, unit, values
in cols:
321 fields.append(pa.field(name, pa.float64(), nullable=
True, metadata={
"unit": unit}))
322 arrays.append(pa.array([
None if v
is None else float(v)
for v
in values], type=pa.float64()))
323 prov = result.get(
"provenance")
or {}
325 "layerx.export":
"series table (engine/layerx/export.py)",
326 "config_sha256": str(prov.get(
"config_sha256")
or ""),
327 "drawing_sha256": str((prov.get(
"drawing")
or {}).get(
"sha256")
or ""),
328 "columns_skipped": json.dumps(skipped),
330 table = pa.Table.from_arrays(arrays, schema=pa.schema(fields, metadata=meta))
332 pq.write_table(table, sink)
333 return sink.getvalue()
339def _engine_history(result: Mapping[str, Any], key_delivered: str, key_twin: str) -> Tuple[List[float], List[Optional[float]], str]:
340 """``(t, values, source)`` over the firing steps: EngineDesign's delivered values when the run
341 has them (the eroding engine), else the twin's own chamber."""
342 series = result.get(
"series")
or {}
343 t = series.get(
"t")
or []
344 fire = [i
for i, f
in enumerate(series.get(
"firing")
or [])
if f]
345 dv = result.get(
"delivered")
346 if isinstance(dv, Mapping)
and isinstance(dv.get(key_delivered), list)
and len(dv[key_delivered]) == len(dv.get(
"t")
or []):
347 return ([float(v)
for v
in dv[
"t"]], [
_cell(v)
for v
in dv[key_delivered]],
348 "EngineDesign time-varying solve (erosion replay), interpolated between its replay points onto the "
349 "twin's firing steps")
350 col = (series.get(
"chamber")
or {}).get(key_twin)
or []
351 return ([float(t[i])
for i
in fire], [
_cell(col[i])
if i < len(col)
else None for i
in fire],
352 "the twin's engine card at the as-built throat (no erosion replay in this run)")
355def _peak(t: Sequence[float], v: Sequence[Optional[float]]) -> Optional[Dict[str, float]]:
356 pairs = [(x, tt)
for tt, x
in zip(t, v)
if x
is not None]
360 return {
"value": x,
"t": tt}
363def _csv(rows: Iterable[Sequence[Any]]) -> str:
365 w = csv.writer(buf, lineterminator=
"\n")
367 w.writerow([
"" if v
is None else repr(v)
if isinstance(v, float)
else v
for v
in r])
368 return buf.getvalue()
371def _heatflux_rows(side: Mapping[str, Any], qkey: str, tkey: Optional[str]) -> Optional[List[List[Any]]]:
372 x, t, q = side.get(
"x_mm"), side.get(
"t"), side.get(qkey)
373 if not (isinstance(x, list)
and isinstance(t, list)
and isinstance(q, list)
and len(q) == len(t)):
375 temp = side.get(tkey)
if tkey
else None
376 rows: List[List[Any]] = []
377 for k, tt
in enumerate(t):
378 row_q = q[k]
if isinstance(q[k], list)
else []
379 row_T = temp[k]
if isinstance(temp, list)
and k < len(temp)
and isinstance(temp[k], list)
else []
380 for j, xx
in enumerate(x):
381 rows.append([
_cell(tt),
_cell(xx),
_cell(row_q[j])
if j < len(row_q)
else None,
382 _cell(row_T[j])
if j < len(row_T)
else None])
386def fea_bundle(result: Mapping[str, Any], sidecar: Optional[Mapping[str, Any]] =
None) -> bytes:
387 """The loads a structural or thermal model needs, as a zip (bytes).
389 ``sidecar`` is the run's ``axial`` sidecar (``{x_mm, t, q_MW_m2, T_wall_K}``, rows t, columns x);
390 without it there is no ``heatflux_xt.csv`` and ``loads.json`` says so.
392 series = result.get(
"series")
or {}
393 prov = result.get(
"provenance")
or {}
396 files: Dict[str, str] = {}
397 files[
"pc_t.csv"] =
_csv([[
"t [s]",
"pc_psia [psia]",
"pc_Pa [Pa]"]]
398 + [[tt, p, p * PSI
if p
is not None else None]
for tt, p
in zip(t_pc, pc)])
399 files[
"thrust_t.csv"] =
_csv([[
"t [s]",
"thrust_N [N]"]] + [[tt, f]
for tt, f
in zip(t_f, thrust)])
401 loads: Dict[str, Any] = {
402 "units": {
"t":
"s from Fire",
"pressure":
"psia (absolute) and Pa",
"thrust":
"N",
"heat_flux":
"MW/m^2",
403 "temperature":
"K",
"x":
"mm along the chamber axis from the injector face"},
404 "chamber_pressure": {
"peak":
_peak(t_pc, pc),
"source": pc_src,
405 "basis":
"injector-end chamber pressure, the highest static pressure the chamber wall sees"},
406 "thrust": {
"peak":
_peak(t_f, thrust),
"source": f_src},
407 "run": {
"config_sha256": prov.get(
"config_sha256"),
"drawing": (prov.get(
"drawing")
or {}).get(
"name"),
408 "drawing_sha256": (prov.get(
"drawing")
or {}).get(
"sha256"),
"converged": result.get(
"converged"),
409 "test_mode": result.get(
"test_mode")},
411 if loads[
"chamber_pressure"][
"peak"]:
412 loads[
"chamber_pressure"][
"peak"][
"value_Pa"] = loads[
"chamber_pressure"][
"peak"][
"value"] * PSI
414 t_all = [float(v)
for v
in (series.get(
"t")
or [])]
415 for side
in (
"ox",
"fuel"):
416 col = [
_cell(v)
for v
in ((series.get(side)
or {}).get(
"tank_psia")
or [])]
417 pk =
_peak(t_all, col)
419 tanks[side] = {**pk,
"unit":
"psia",
"basis":
"ullage pressure over the recorded lead-in and the burn"}
421 loads[
"tank_pressure"] = tanks
422 bottle =
_peak(t_all, [
_cell(v)
for v
in (series.get(
"copv_psia")
or [])])
424 loads[
"bottle_pressure"] = {**bottle,
"unit":
"psia"}
425 wh = ((result.get(
"diagnostics")
or {}).get(
"water_hammer")
or [])
426 if isinstance(wh, list)
and wh:
427 loads[
"water_hammer"] = [{k: w.get(k)
for k
in (
"line",
"side",
"closure_s",
"peak_psia",
"rating_psia")}
428 for w
in wh
if isinstance(w, Mapping)]
430 heat_rows =
_heatflux_rows(sidecar,
"q_MW_m2",
"T_wall_K")
if isinstance(sidecar, Mapping)
else None
431 if heat_rows
is not None:
432 files[
"heatflux_xt.csv"] =
_csv([[
"t [s]",
"x_mm [mm]",
"q_MW_m2 [MW/m^2]",
"T_wall_K [K]"]] + heat_rows)
433 live = [r
for r
in heat_rows
if r[2]
is not None]
435 top =
max(live, key=
lambda r: r[2])
436 loads[
"heat_flux"] = {
"peak": {
"value": top[2],
"t": top[0],
"x_mm": top[1]},
437 "basis": str(sidecar.get(
"basis")
or "the run's axial sidecar")}
438 hot = [r
for r
in heat_rows
if r[3]
is not None]
440 top =
max(hot, key=
lambda r: r[3])
441 loads[
"wall_temperature"] = {
"peak": {
"value": top[3],
"t": top[0],
"x_mm": top[1]}}
442 prof = sidecar.get(
"profile")
443 if isinstance(prof, Mapping):
444 prof_rows =
_heatflux_rows({**prof,
"t": sidecar.get(
"t")},
"q_MW_m2",
None)
445 if prof_rows
is not None:
446 files[
"heatflux_profile_xt.csv"] =
_csv([[
"t [s]",
"x_mm [mm]",
"q_MW_m2 [MW/m^2]",
"T_wall_K [K]"]] + prof_rows)
448 loads[
"heat_flux"] = {
"available":
False,
"error":
"no axial sidecar for this run"}
450 files[
"loads.json"] = json.dumps(loads, indent=2, sort_keys=
True, default=
lambda o:
None)
451 files[
"README.txt"] =
_readme(files, pc_src, f_src, sidecar)
453 with zipfile.ZipFile(sink,
"w", compression=zipfile.ZIP_DEFLATED)
as z:
454 for name
in sorted(files):
455 info = zipfile.ZipInfo(name, date_time=(2026, 1, 1, 0, 0, 0))
456 info.compress_type = zipfile.ZIP_DEFLATED
457 z.writestr(info, files[name])
458 return sink.getvalue()
461def _readme(files: Mapping[str, str], pc_src: str, f_src: str, sidecar: Optional[Mapping[str, Any]]) -> str:
463 "Layer X FEA load bundle",
465 "Time is seconds from the Fire command. Pressures are absolute (psia, with Pa alongside where an FEA",
466 "deck wants SI); 1 psi = 6894.757293168361 Pa. Thrust in N. Heat flux in MW/m^2, temperatures in K,",
467 "axial position x in mm from the injector face along the chamber axis.",
469 "pc_t.csv chamber pressure at the injector end, on the twin's firing steps. Source: " + pc_src +
".",
470 " The injector-end pressure is ~0.4 % above the nozzle stagnation pressure (Rayleigh loss);",
471 " it is the highest static pressure the chamber wall sees.",
472 "thrust_t.csv thrust on the same steps. Source: " + f_src +
".",
473 " Values between replay points are linear interpolations of EngineDesign solves spaced",
474 " 0.10-0.15 s apart: they do not resolve the start or shutdown transient, which is not modelled.",
476 if "heatflux_xt.csv" in files:
478 "heatflux_xt.csv long format, one row per (t, x): net gas-side heat flux at each wall station and",
479 " that station's surface temperature, at the erosion replay's points. Basis: "
480 + str((sidecar
or {}).get(
"basis")
or "the run's axial sidecar") +
".",
482 if "heatflux_profile_xt.csv" in files:
484 "heatflux_profile_xt.csv the chamber solve's whole-contour flux at one quasi-steady liner surface",
485 " temperature: where the gas loads the wall, not the transient wall (no T_wall column).",
488 "loads.json peak loads (value and time) with their basis: chamber pressure, thrust, tank and bottle",
489 " pressures, heat flux and wall temperature when the sidecar is present, water hammer when",
490 " diagnosed, and the run's config and drawing hashes.",
492 "These are model outputs. Every input behind them carries its provenance in the run record; read it",
493 "before using a number for a margin.",
495 return "\n".join(lines) +
"\n"
List[Any] _place(int n, Sequence[int] rows, Sequence[Any] values)
bytes to_parquet(Mapping[str, Any] result)
None _walk(str prefix, Any block, int n, List[Tuple[str, List[Any], Optional[str]]] out, List[str] skipped, int depth=0)
Optional[List[int]] _rows_of(Sequence[Any] times, Sequence[float] t, float tol=1e-6)
str to_csv(Mapping[str, Any] result)
Optional[Dict[str, float]] _peak(Sequence[float] t, Sequence[Optional[float]] v)
Tuple[List[Tuple[str, str, List[Any]]], List[str]] columns(Mapping[str, Any] result)
bytes fea_bundle(Mapping[str, Any] result, Optional[Mapping[str, Any]] sidecar=None)
bool _is_column(Any v, int n)
Optional[List[List[Any]]] _heatflux_rows(Mapping[str, Any] side, str qkey, Optional[str] tkey)
str _csv(Iterable[Sequence[Any]] rows)
Dict[str, Tuple[str, List[Optional[float]]]] read_csv(str text)
str _header(str name, str unit)
Tuple[List[float], List[Optional[float]], str] _engine_history(Mapping[str, Any] result, str key_delivered, str key_twin)
None _diagnostic(str prefix, Any block, Sequence[float] t, Any add, List[str] skipped, Optional[List[int]] rows=None, int depth=0)
str _readme(Mapping[str, str] files, str pc_src, str f_src, Optional[Mapping[str, Any]] sidecar)