from __future__ import annotations import json import math from pathlib import Path import numpy as np import pyarrow.parquet as pq import pytest PARQUET_PATH = Path(__file__).parent.parent / "data" / "trajectories.parquet" JSONL_PATH = Path(__file__).parent.parent / "data" / "synthetic_dataset_v2.jsonl" def verify_drydown(record: dict) -> bool: """ Check that a trajectory represents a physically plausible dry-down: - non-empty trajectory with a finite time span - final time is near the 8-hour horizon (or film is depleted earlier) - no negative or NaN mole fractions in the trajectory """ traj = record.get("trajectory", []) if not traj: return False if not all(math.isfinite(step["t_s"]) for step in traj): return False if traj[0]["t_s"] != pytest.approx(0.0, abs=1.0): return False if traj[-1]["t_s"] < 1000.0: return False for step in traj: x_vals = list(step.get("x_liquid", {}).values()) if any(v < -1e-6 or not math.isfinite(v) for v in x_vals): return False return True def verify_stability(record: dict) -> bool: """ Check thermodynamic/ODE stability: - status is 'passed' or 'depleted' - depletion rates are in [0, 1] - no NaN/Inf in depletion rates """ status = record.get("status", "") if status not in ("passed", "depleted"): return False rates = record.get("depletion_rates", {}) if not rates: return False for v in rates.values(): if not math.isfinite(v) or v < -1e-3 or v > 1.0 + 1e-3: return False return True @pytest.fixture(scope="module") def parquet_records(): if not PARQUET_PATH.exists(): pytest.skip(f"Parquet dataset artifact not found: {PARQUET_PATH}") table = pq.read_table(PARQUET_PATH) rows = table.to_pydict() records = [] for i in range(table.num_rows): records.append({k: json.loads(rows[k][i]) if k in ("formula", "depletion_rates", "trajectory") else rows[k][i] for k in rows}) return records def test_parquet_exists_and_has_records(): if not PARQUET_PATH.exists(): pytest.skip(f"Parquet dataset artifact not found: {PARQUET_PATH}") table = pq.read_table(PARQUET_PATH) assert table.num_rows >= 5000 def test_parquet_genre_stratification(parquet_records): strategies = {} for rec in parquet_records: strategies[rec["generation_strategy"]] = strategies.get(rec["generation_strategy"], 0) + 1 expected_genres = {"citrus_cologne", "fougere", "floral_woody", "amber_oriental", "wildcard"} assert expected_genres.issubset(strategies.keys()) for genre in expected_genres: assert strategies[genre] >= 900, f"Genre {genre} under-represented: {strategies[genre]}" def test_verify_drydown_all_records(parquet_records): failures = [] for rec in parquet_records: if not verify_drydown(rec): failures.append(rec["formula_id"]) assert not failures, f"verify_drydown failed for {len(failures)} records: {failures[:10]}" def test_verify_stability_all_records(parquet_records): failures = [] for rec in parquet_records: if not verify_stability(rec): failures.append(rec["formula_id"]) assert not failures, f"verify_stability failed for {len(failures)} records: {failures[:10]}"