- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
73 lines
3.0 KiB
Python
73 lines
3.0 KiB
Python
"""Compact wire format for /api/analyze results (``?format=columnar``).
|
|
|
|
The object format repeats every key per node/link and writes each float as
|
|
~18 characters of JSON (9.5 MB for a 2,000-node, 25-step network). Here the
|
|
time series are packed per variable into one little-endian float64 array per
|
|
element kind, base64-encoded (~10.7 bytes per value), so full double precision
|
|
is kept -- the Native-vs-WASM comparison tolerances (rel 1e-8) still apply.
|
|
|
|
Layout of each packed series: element-major, i.e. element ``i``'s values are
|
|
``data[i * steps:(i + 1) * steps]`` in the order of ``ids``.
|
|
src/api.ts (decodeColumnarResult) turns it back into the object format.
|
|
"""
|
|
import base64
|
|
import sys
|
|
from array import array
|
|
|
|
FORMAT = "spipe-columnar-1"
|
|
NODE_SERIES = ("demand", "head", "pressure", "quality")
|
|
LINK_SERIES = ("flow", "velocity", "headloss", "quality")
|
|
PUMP_FIELDS = ("energy", "efficiency", "totalKwh", "peakKw", "avgKw", "utilization")
|
|
|
|
|
|
def _pack(items: dict, ids: list, key: str, steps: int):
|
|
"""Packed base64 series, or None when any element lacks a full series
|
|
(e.g. no water-quality run) -- the decoder then leaves it empty."""
|
|
flat = array("d")
|
|
for element_id in ids:
|
|
series = items[element_id]["timeSeries"].get(key) or []
|
|
if len(series) != steps:
|
|
return None
|
|
flat.extend(series)
|
|
if sys.byteorder != "little":
|
|
flat.byteswap()
|
|
return base64.b64encode(flat.tobytes()).decode("ascii")
|
|
|
|
|
|
def to_columnar(result: dict) -> dict:
|
|
steps = len(result.get("timeSteps") or [])
|
|
nodes, links = result["nodes"], result["links"]
|
|
node_ids, link_ids = list(nodes), list(links)
|
|
|
|
# Pump series and the pump's scalar fields (initial energy/efficiency, kWh
|
|
# totals) use distinct keys -- they share names in the object format.
|
|
pumps = {
|
|
lid: {
|
|
"energySeries": links[lid]["timeSeries"].get("energy") or [],
|
|
"efficiencySeries": links[lid]["timeSeries"].get("efficiency") or [],
|
|
**{f: links[lid].get(f) for f in PUMP_FIELDS if f in links[lid]},
|
|
}
|
|
for lid in link_ids
|
|
if "totalKwh" in links[lid]
|
|
}
|
|
|
|
return {
|
|
"format": FORMAT,
|
|
**{k: v for k, v in result.items() if k not in ("nodes", "links")},
|
|
"nodes": {
|
|
"ids": node_ids,
|
|
"types": [nodes[i]["type"] for i in node_ids],
|
|
"coordinates": [nodes[i]["coordinates"] for i in node_ids],
|
|
"series": {k: _pack(nodes, node_ids, k, steps) for k in NODE_SERIES},
|
|
},
|
|
"links": {
|
|
"ids": link_ids,
|
|
"types": [links[i]["type"] for i in link_ids],
|
|
"fromNodes": [links[i]["fromNode"] for i in link_ids],
|
|
"toNodes": [links[i]["toNode"] for i in link_ids],
|
|
"vertices": [links[i]["vertices"] for i in link_ids],
|
|
"series": {k: _pack(links, link_ids, k, steps) for k in LINK_SERIES},
|
|
"pumps": pumps,
|
|
},
|
|
}
|