Files
bjkim1 21ebb16fb7 SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음)
- README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리
- 패치내역을 docs/로 이동, _worklog.md 추가
- 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
2026-10-30 11:00:46 +09:00

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,
},
}