|
@@ -244,6 +244,102 @@ def api_tests() -> None:
|
|
|
check("csv: 400 when no results",
|
|
check("csv: 400 when no results",
|
|
|
client.get(f"/api/plans/{empty_pk}/export-csv").status_code == 400)
|
|
client.get(f"/api/plans/{empty_pk}/export-csv").status_code == 400)
|
|
|
|
|
|
|
|
|
|
+ # 5) Raw full-fidelity columns: attach a raw archive to row 1 of the
|
|
|
|
|
+ # thermal plan and verify all fields (with units) appear in the export.
|
|
|
|
|
+ with SessionLocal() as db:
|
|
|
|
|
+ r1 = (db.query(SimulationResult)
|
|
|
|
|
+ .filter(SimulationResult.plan_id == plan_pk,
|
|
|
|
|
+ SimulationResult.run_index == 1).first())
|
|
|
|
|
+ r1.set_raw([
|
|
|
|
|
+ {"section": "\u9a71\u52a8", "name": "\u76f4\u6d41\u6bcd\u7ebf\u7535\u538b",
|
|
|
|
|
+ "value": 48.0, "unit": "Volts"},
|
|
|
|
|
+ {"section": "\u7535\u78c1", "name": "\u8f6c\u77e9\u5bc6\u5ea6",
|
|
|
|
|
+ "value": 23.333, "unit": "kNm/m3"},
|
|
|
|
|
+ {"section": "Thermal-\u6e29\u5ea6", "name": "T[\u7ed5\u7ec4\u5e73\u5747]",
|
|
|
|
|
+ "value": 52.591, "unit": "\u00b0C"},
|
|
|
|
|
+ {"section": "Thermal-\u70ed\u963b", "name": "Rt[\u6c14\u9699]",
|
|
|
|
|
+ "value": 5.076, "unit": "\u00b0C/W"},
|
|
|
|
|
+ ])
|
|
|
|
|
+ db.commit()
|
|
|
|
|
+
|
|
|
|
|
+ resp4 = client.get(f"/api/plans/{plan_pk}/export-xlsx")
|
|
|
|
|
+ check("raw: export 200", resp4.status_code == 200)
|
|
|
|
|
+ out4 = Path(tempfile.mkdtemp()) / "raw_export.xlsx"
|
|
|
|
|
+ out4.write_bytes(resp4.content)
|
|
|
|
|
+ import openpyxl as _oxl
|
|
|
|
|
+ ws4 = _oxl.load_workbook(out4).active
|
|
|
|
|
+ h4 = [ws4.cell(2, c).value for c in range(1, ws4.max_column + 1)]
|
|
|
|
|
+ c4 = []
|
|
|
|
|
+ prev = None
|
|
|
|
|
+ for c in range(1, ws4.max_column + 1):
|
|
|
|
|
+ v = ws4.cell(1, c).value
|
|
|
|
|
+ if v and v != prev:
|
|
|
|
|
+ c4.append(v); prev = v
|
|
|
|
|
+ check("raw: EM raw header with unit",
|
|
|
|
|
+ "\u76f4\u6d41\u6bcd\u7ebf\u7535\u538b[Volts]" in h4)
|
|
|
|
|
+ check("raw: thermal raw header with unit",
|
|
|
|
|
+ "T[\u7ed5\u7ec4\u5e73\u5747][\u00b0C]" in h4)
|
|
|
|
|
+ check("raw: thermal category translated",
|
|
|
|
|
+ "Thermal-\u6e29\u5ea6".replace("Thermal-", "\u70ed\u4eff\u771f-") in c4
|
|
|
|
|
+ or "\u70ed\u4eff\u771f-\u6e29\u5ea6" in c4)
|
|
|
|
|
+ r3v = [ws4.cell(3, c).value for c in range(1, ws4.max_column + 1)]
|
|
|
|
|
+ check("raw: raw value in data row",
|
|
|
|
|
+ 52.591 in r3v and 48.0 in r3v)
|
|
|
|
|
+
|
|
|
|
|
+ # parse_export_full unit test on the real coupled-run export files.
|
|
|
|
|
+ from afmcore.metrics import parse_export_full
|
|
|
|
|
+ em_csv = REPO_ROOT / "output" / "thermal_validation_20260904_124142" / "raw" / "emagnetic.csv"
|
|
|
|
|
+ th_csv = REPO_ROOT / "output" / "thermal_validation_20260904_124142" / "raw" / "thermal_steadystate.csv"
|
|
|
|
|
+ if em_csv.exists() and th_csv.exists():
|
|
|
|
|
+ em_rows = parse_export_full(em_csv)
|
|
|
|
|
+ th_rows = parse_export_full(th_csv)
|
|
|
|
|
+ check("full: EM export parsed", len(em_rows) > 200,
|
|
|
|
|
+ f"got {len(em_rows)}")
|
|
|
|
|
+ check("full: thermal export parsed", len(th_rows) > 500,
|
|
|
|
|
+ f"got {len(th_rows)}")
|
|
|
|
|
+ check("full: units captured",
|
|
|
|
|
+ any(r["unit"] == "\u00b0C" for r in th_rows))
|
|
|
|
|
+ check("full: sections captured",
|
|
|
|
|
+ any(r["section"] == "\u6e29\u5ea6" for r in th_rows))
|
|
|
|
|
+ else:
|
|
|
|
|
+ check("full: real export files present", False,
|
|
|
|
|
+ "thermal_validation_20260904_124142 raw files missing")
|
|
|
|
|
+
|
|
|
|
|
+ # INF / NaN safety: non-finite floats must not enter raw rows as floats
|
|
|
|
|
+ # (they break JSON reporting: "Out of range float values are not JSON
|
|
|
|
|
+ # compliant" - observed live on 2026-09-04 task e53ff277).
|
|
|
|
|
+ import math as _math
|
|
|
|
|
+ tmp_csv = Path(tempfile.mkdtemp()) / "inf_test.csv"
|
|
|
|
|
+ tmp_csv.write_text(
|
|
|
|
|
+ "\u7535\u78c1\n"
|
|
|
|
|
+ "\u6052\u8f6c\u77e9\u8f6c\u901f\u9650\u5236;INF;rpm\n"
|
|
|
|
|
+ "\u5e73\u5747\u8f6c\u77e9;0.5;Nm\n"
|
|
|
|
|
+ "Node to Node Thermal Resistances\n"
|
|
|
|
|
+ "0;Ambient;2\n"
|
|
|
|
|
+ "\u6e29\u5ea6\n"
|
|
|
|
|
+ "T[\u7ed5\u7ec4];52.5;\u00b0C\n",
|
|
|
|
|
+ encoding="utf-8",
|
|
|
|
|
+ )
|
|
|
|
|
+ full_rows = parse_export_full(tmp_csv)
|
|
|
|
|
+ inf_row = [r for r in full_rows if r["name"] == "\u6052\u8f6c\u77e9\u8f6c\u901f\u9650\u5236"]
|
|
|
|
|
+ check("full: INF kept as string",
|
|
|
|
|
+ len(inf_row) == 1 and inf_row[0]["value"] == "INF")
|
|
|
|
|
+ check("full: no non-finite floats",
|
|
|
|
|
+ all(not (isinstance(r["value"], float) and not _math.isfinite(r["value"]))
|
|
|
|
|
+ for r in full_rows))
|
|
|
|
|
+ check("full: node-to-node matrix skipped",
|
|
|
|
|
+ all(r["section"] != "Node to Node Thermal Resistances" for r in full_rows))
|
|
|
|
|
+ import json as _json
|
|
|
|
|
+ _json.dumps(full_rows) # must not raise
|
|
|
|
|
+ check("full: raw rows JSON-serializable", True)
|
|
|
|
|
+
|
|
|
|
|
+ # Normalized parser must also skip INF (not extract it as a metric).
|
|
|
|
|
+ from afmcore.metrics import parse_export as _pe
|
|
|
|
|
+ parsed = _pe(tmp_csv)
|
|
|
|
|
+ flat_vals = [v for sec in parsed.values() for v in sec.values()]
|
|
|
|
|
+ check("full: parse_export skips INF",
|
|
|
|
|
+ all(_math.isfinite(v) for v in flat_vals))
|
|
|
|
|
+
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if __name__ == "__main__":
|
|
|
unit_tests()
|
|
unit_tests()
|