navier-cfd 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
navier_cfd/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """NAVIER-CFD public API: neural and agentic workflows for computational fluid dynamics."""
2
+
3
+ from .catalogs import Catalog
4
+ from .evidence import (
5
+ ALGORITHM_VERSION,
6
+ EvidenceMatch,
7
+ EvidenceRecord,
8
+ EvidenceSummary,
9
+ load_builtin_evidence,
10
+ score_model_evidence,
11
+ )
12
+ from .recommender import Recommendation, recommend_models
13
+ from .specs import DatasetSpec, ModelSpec, TaskSpec
14
+
15
+ __all__ = [
16
+ "ALGORITHM_VERSION",
17
+ "Catalog",
18
+ "DatasetSpec",
19
+ "EvidenceMatch",
20
+ "EvidenceRecord",
21
+ "EvidenceSummary",
22
+ "ModelSpec",
23
+ "Recommendation",
24
+ "TaskSpec",
25
+ "load_builtin_evidence",
26
+ "recommend_models",
27
+ "score_model_evidence",
28
+ ]
29
+
30
+ __version__ = "0.2.0"
navier_cfd/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import app
2
+
3
+ app()
@@ -0,0 +1,3 @@
1
+ from .orchestrator import AgentOrchestrator, AgentPlan
2
+
3
+ __all__ = ["AgentOrchestrator", "AgentPlan"]
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import asdict, dataclass
6
+ from typing import Any, Protocol
7
+
8
+ from ..benchmarks.planning import build_benchmark_plan
9
+ from ..catalogs import Catalog
10
+ from ..recommender import recommend_models
11
+ from ..specs import TaskSpec
12
+
13
+
14
+ class LLMBackend(Protocol):
15
+ def __call__(self, *, system: str, user: str, tools: list[dict[str, Any]]) -> dict[str, Any]: ...
16
+
17
+
18
+ @dataclass
19
+ class AgentPlan:
20
+ interpreted_task: TaskSpec
21
+ dataset_id: str
22
+ recommended_models: list[str]
23
+ benchmark_plan: dict[str, Any]
24
+ rationale: list[str]
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ return asdict(self)
28
+
29
+
30
+ class AgentOrchestrator:
31
+ """Provider-neutral experiment planner.
32
+
33
+ The deterministic planner works offline. A caller may inject any LLM backend that accepts
34
+ the documented structured interface; no vendor SDK is required by the core package.
35
+ """
36
+
37
+ def __init__(self, catalog: Catalog | None = None, backend: LLMBackend | None = None) -> None:
38
+ self.catalog = catalog or Catalog.load_builtin()
39
+ self.backend = backend
40
+
41
+ def plan(self, prompt: str) -> AgentPlan:
42
+ task, dataset_id = self._interpret(prompt)
43
+ recommendations = recommend_models(task, self.catalog.models, top_k=8)
44
+ dataset = self.catalog.dataset(dataset_id)
45
+ benchmark = build_benchmark_plan(task, dataset, recommendations)
46
+ rationale = [
47
+ f"Dataset selected from explicit or inferred benchmark: {dataset.name}.",
48
+ "Models were hard-filtered by dimension and mesh compatibility.",
49
+ "Ranking rewards task role, physics overlap, geometry/temporal support, and safety needs.",
50
+ "The emitted plan separates interpolation, OOD, stability, physics, and cost metrics.",
51
+ ]
52
+ return AgentPlan(
53
+ interpreted_task=task,
54
+ dataset_id=dataset_id,
55
+ recommended_models=[item.model.id for item in recommendations],
56
+ benchmark_plan=benchmark.to_dict(),
57
+ rationale=rationale,
58
+ )
59
+
60
+ def _interpret(self, prompt: str) -> tuple[TaskSpec, str]:
61
+ text = prompt.lower()
62
+ dataset_id = "pdebench"
63
+ if "realpdebench" in text or "real pde" in text or "sim-to-real" in text:
64
+ dataset_id = "realpdebench"
65
+ elif "cfdbench" in text or "cavity" in text or "dam" in text:
66
+ dataset_id = "cfdbench"
67
+
68
+ dimension = 3 if re.search(r"\b3d\b|three-dimensional", text) else 2
69
+ mesh = "unstructured" if "unstructured" in text else "structured"
70
+ temporal = "autoregressive" if any(k in text for k in ["rollout", "forecast", "unsteady", "sequence"]) else "steady"
71
+ geometry = "varying" if any(k in text for k in ["geometry", "shape", "airfoil", "vehicle", "wing"]) else "fixed"
72
+ task_type = "acceleration" if any(k in text for k in ["accelerat", "precondition", "corrector", "solver-in-the-loop"]) else "surrogate"
73
+ problem = "cylinder_wake" if "cylinder" in text else "general_cfd"
74
+ memory = None
75
+ match = re.search(r"(\d+(?:\.\d+)?)\s*gb", text)
76
+ if match:
77
+ memory = float(match.group(1))
78
+
79
+ physics = ["incompressible_navier_stokes"]
80
+ if "compressible" in text or "mach" in text or "shock" in text:
81
+ physics = ["compressible_navier_stokes"]
82
+ if "multiphase" in text or "dam" in text:
83
+ physics.append("multiphase")
84
+
85
+ task = TaskSpec(
86
+ problem=problem,
87
+ task_type=task_type,
88
+ dimension=dimension,
89
+ mesh_type=mesh,
90
+ temporal_mode=temporal,
91
+ geometry_mode=geometry,
92
+ physics=tuple(physics),
93
+ requires_conservation="conserv" in text or task_type == "acceleration",
94
+ requires_uncertainty="uncertainty" in text or "safe" in text or "certif" in text,
95
+ requires_geometry_transfer="geometry" in text or "shape" in text,
96
+ requires_mesh_transfer="mesh transfer" in text or "cross-mesh" in text,
97
+ requires_long_rollout=temporal == "autoregressive",
98
+ hardware_memory_gb=memory,
99
+ notes=prompt,
100
+ )
101
+ return task, dataset_id
102
+
103
+ @staticmethod
104
+ def tool_schemas() -> list[dict[str, Any]]:
105
+ return [
106
+ {"name": "list_datasets", "description": "List registered CFD/PDE datasets."},
107
+ {"name": "inspect_dataset", "description": "Read one dataset card."},
108
+ {"name": "recommend_models", "description": "Rank compatible neural solvers."},
109
+ {"name": "build_benchmark_plan", "description": "Create splits, metrics and ablations."},
110
+ {"name": "emit_run_manifest", "description": "Serialize a reproducible experiment plan."},
111
+ ]
112
+
113
+ def plan_json(self, prompt: str) -> str:
114
+ return json.dumps(self.plan(prompt).to_dict(), indent=2)
@@ -0,0 +1,4 @@
1
+ from .metrics import compute_metric_bundle
2
+ from .planning import BenchmarkPlan, build_benchmark_plan
3
+
4
+ __all__ = ["BenchmarkPlan", "build_benchmark_plan", "compute_metric_bundle"]
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+
7
+
8
+ def rmse(pred: np.ndarray, target: np.ndarray) -> float:
9
+ return float(np.sqrt(np.mean((pred - target) ** 2)))
10
+
11
+
12
+ def mae(pred: np.ndarray, target: np.ndarray) -> float:
13
+ return float(np.mean(np.abs(pred - target)))
14
+
15
+
16
+ def relative_l2(pred: np.ndarray, target: np.ndarray, eps: float = 1e-12) -> float:
17
+ return float(np.linalg.norm(pred - target) / (np.linalg.norm(target) + eps))
18
+
19
+
20
+ def r2(pred: np.ndarray, target: np.ndarray, eps: float = 1e-12) -> float:
21
+ residual = np.sum((target - pred) ** 2)
22
+ total = np.sum((target - np.mean(target)) ** 2)
23
+ return float(1.0 - residual / (total + eps))
24
+
25
+
26
+ def spectral_relative_error(pred: np.ndarray, target: np.ndarray, eps: float = 1e-12) -> float:
27
+ axes = tuple(range(-min(2, pred.ndim), 0))
28
+ p = np.abs(np.fft.rfftn(pred, axes=axes))
29
+ t = np.abs(np.fft.rfftn(target, axes=axes))
30
+ return float(np.linalg.norm(p - t) / (np.linalg.norm(t) + eps))
31
+
32
+
33
+ def divergence_rms(velocity: np.ndarray, spacing: tuple[float, ...] | None = None) -> float:
34
+ """Approximate RMS divergence for arrays shaped (..., spatial..., components)."""
35
+ if velocity.shape[-1] not in {2, 3}:
36
+ raise ValueError("velocity must store 2 or 3 components in the final axis")
37
+ ncomp = velocity.shape[-1]
38
+ spacing = spacing or tuple(1.0 for _ in range(ncomp))
39
+ if len(spacing) != ncomp:
40
+ raise ValueError("spacing length must equal the number of velocity components")
41
+ div = 0.0
42
+ for component in range(ncomp):
43
+ axis = velocity.ndim - ncomp - 1 + component
44
+ div = div + np.gradient(velocity[..., component], spacing[component], axis=axis)
45
+ return float(np.sqrt(np.mean(np.asarray(div) ** 2)))
46
+
47
+
48
+ def compute_metric_bundle(
49
+ pred: np.ndarray,
50
+ target: np.ndarray,
51
+ *,
52
+ velocity: bool = False,
53
+ spacing: tuple[float, ...] | None = None,
54
+ ) -> dict[str, Any]:
55
+ pred = np.asarray(pred)
56
+ target = np.asarray(target)
57
+ if pred.shape != target.shape:
58
+ raise ValueError(f"shape mismatch: {pred.shape} != {target.shape}")
59
+ metrics: dict[str, Any] = {
60
+ "rmse": rmse(pred, target),
61
+ "mae": mae(pred, target),
62
+ "relative_l2": relative_l2(pred, target),
63
+ "r2": r2(pred, target),
64
+ "spectral_relative_error": spectral_relative_error(pred, target),
65
+ }
66
+ if velocity:
67
+ metrics["divergence_rms_pred"] = divergence_rms(pred, spacing)
68
+ metrics["divergence_rms_target"] = divergence_rms(target, spacing)
69
+ return metrics
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+
7
+ from ..recommender import Recommendation
8
+ from ..specs import DatasetSpec, TaskSpec
9
+
10
+
11
+ @dataclass
12
+ class BenchmarkPlan:
13
+ task: TaskSpec
14
+ dataset: DatasetSpec
15
+ models: list[str]
16
+ splits: list[str]
17
+ metrics: list[str]
18
+ ablations: list[str]
19
+ reporting: list[str]
20
+ created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
21
+
22
+ def to_dict(self) -> dict[str, Any]:
23
+ return asdict(self)
24
+
25
+
26
+ def build_benchmark_plan(
27
+ task: TaskSpec,
28
+ dataset: DatasetSpec,
29
+ recommendations: list[Recommendation],
30
+ max_models: int = 6,
31
+ ) -> BenchmarkPlan:
32
+ metrics = [
33
+ "relative_l2",
34
+ "rmse",
35
+ "mae",
36
+ "r2",
37
+ "spectral_relative_error",
38
+ "wall_clock",
39
+ "peak_memory",
40
+ ]
41
+ if task.temporal_mode in {"autoregressive", "unsteady", "sequence"}:
42
+ metrics += ["stable_horizon", "rollout_drift", "autocorrelation_error"]
43
+ if task.requires_conservation:
44
+ metrics += ["mass_balance_error", "momentum_balance_error", "divergence_rms"]
45
+ if task.requires_uncertainty:
46
+ metrics += ["coverage", "calibration_error", "risk_coverage"]
47
+
48
+ splits = ["interpolation", "parameter_ood"]
49
+ if task.requires_geometry_transfer or task.geometry_mode == "varying":
50
+ splits.append("geometry_ood")
51
+ if task.requires_mesh_transfer:
52
+ splits += ["mesh_resolution_ood", "mesh_generator_ood"]
53
+ if "real" in dataset.name.lower() or "real" in dataset.description.lower():
54
+ splits.append("sim_to_real")
55
+
56
+ return BenchmarkPlan(
57
+ task=task,
58
+ dataset=dataset,
59
+ models=[item.model.id for item in recommendations[:max_models]],
60
+ splits=splits,
61
+ metrics=list(dict.fromkeys(metrics)),
62
+ ablations=[
63
+ "remove_physics_features",
64
+ "remove_geometry_features",
65
+ "one_step_vs_unrolled_training",
66
+ "matched_parameter_and_wall_clock_budget",
67
+ ],
68
+ reporting=[
69
+ "three_or_more_random_seeds",
70
+ "confidence_intervals_over_cases",
71
+ "data_generation_cost",
72
+ "training_cost",
73
+ "break_even_query_count",
74
+ "pinned_dataset_and_model_revisions",
75
+ ],
76
+ )
navier_cfd/catalogs.py ADDED
@@ -0,0 +1,208 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Iterable
6
+ import json
7
+
8
+ from .specs import DatasetSpec, ModelSpec
9
+
10
+ _MODEL_ROWS = [
11
+ ("pinn", "Physics-Informed Neural Network", ("physics_informed", "general_pde_solver", "inverse")),
12
+ ("nsfnets", "NSFnets", ("physics_informed", "specialized")),
13
+ ("pinnsformer", "PINNsFormer", ("physics_informed", "general_pde_solver")),
14
+ ("deeponet", "DeepONet", ("surrogate", "general_pde_solver", "physics_informed")),
15
+ ("mionet", "MIONet", ("surrogate", "general_pde_solver")),
16
+ ("fourier_deeponet", "Fourier-DeepONet", ("surrogate", "specialized")),
17
+ ("nested_fourier_deeponet", "Nested Fourier-DeepONet", ("surrogate", "specialized", "particle_multiphase")),
18
+ ("fourier_mionet", "Fourier-MIONet", ("surrogate", "specialized", "particle_multiphase")),
19
+ ("fno", "Fourier Neural Operator", ("surrogate", "general_pde_solver")),
20
+ ("pino", "Physics-Informed Neural Operator", ("physics_informed", "surrogate", "general_pde_solver")),
21
+ ("geo_fno", "Geo-FNO", ("geometry", "surrogate", "general_pde_solver")),
22
+ ("gino", "Geometry-Informed Neural Operator", ("geometry", "surrogate")),
23
+ ("u_fno", "U-FNO", ("surrogate", "specialized", "particle_multiphase")),
24
+ ("f_fno", "Factorized FNO", ("surrogate", "general_pde_solver")),
25
+ ("u_no", "U-shaped Neural Operator", ("surrogate", "general_pde_solver")),
26
+ ("lsm", "Latent Spectral Model", ("surrogate", "general_pde_solver")),
27
+ ("gnot", "General Neural Operator Transformer", ("surrogate", "general_pde_solver", "geometry")),
28
+ ("galerkin_transformer", "Galerkin Transformer", ("surrogate", "general_pde_solver")),
29
+ ("mwt", "Multiwavelet Transformer", ("surrogate", "general_pde_solver")),
30
+ ("factformer", "FactFormer", ("surrogate", "general_pde_solver")),
31
+ ("ono", "Orthogonal Neural Operator", ("surrogate", "general_pde_solver")),
32
+ ("transolver", "Transolver", ("geometry", "surrogate", "general_pde_solver")),
33
+ ("upt", "Universal Physics Transformer", ("foundation", "geometry", "surrogate", "general_pde_solver")),
34
+ ("meshgraphnets", "MeshGraphNets", ("geometry", "surrogate")),
35
+ ("domino", "DoMINO", ("geometry", "surrogate", "specialized")),
36
+ ("pibert", "PIBERT", ("surrogate", "physics_informed", "specialized")),
37
+ ("fourierflow", "FourierFlow", ("surrogate", "generative", "specialized")),
38
+ ("pde_refiner", "PDE-Refiner", ("surrogate", "generative", "general_pde_solver")),
39
+ ("dpot", "DPOT", ("foundation", "surrogate", "general_pde_solver")),
40
+ ("poseidon", "Poseidon", ("foundation", "surrogate", "general_pde_solver")),
41
+ ("prose_fd", "PROSE-FD", ("foundation", "surrogate", "specialized")),
42
+ ("bcat", "BCAT", ("foundation", "surrogate", "specialized")),
43
+ ("pdeformer1", "PDEformer-1", ("foundation", "general_pde_solver")),
44
+ ("pi_mfm", "PI-MFM", ("foundation", "physics_informed", "general_pde_solver")),
45
+ ("laplace_no", "Laplace Neural Operator", ("surrogate", "general_pde_solver")),
46
+ ("state_space_no", "State-Space Neural Operator", ("surrogate", "general_pde_solver")),
47
+ ("p3d", "P3D", ("surrogate", "foundation", "specialized")),
48
+ ("aerotransformer", "AeroTransformer", ("foundation", "geometry", "specialized")),
49
+ ("tadpole", "Tadpole", ("foundation", "surrogate")),
50
+ ("solver_in_loop", "Solver-in-the-Loop", ("acceleration", "physics_informed")),
51
+ ("inc", "Indirect Neural Corrector", ("acceleration", "physics_informed")),
52
+ ("pict", "PICT", ("acceleration", "specialized", "physics_informed")),
53
+ ("diffsph", "diffSPH", ("acceleration", "particle_multiphase", "specialized")),
54
+ ("neurosem", "NeuroSEM", ("acceleration", "physics_informed", "specialized")),
55
+ ("np_newton", "Neural-Operator Preconditioned Newton", ("acceleration", "general_pde_solver")),
56
+ ("geometry_preconditioner", "Geometry-Aware Neural Preconditioner", ("acceleration", "geometry")),
57
+ ("neuraldem", "NeuralDEM", ("specialized", "particle_multiphase", "surrogate")),
58
+ ("revit", "ReViT", ("geometry", "surrogate")),
59
+ ("deepmmnet", "DeepM&Mnet", ("surrogate", "specialized", "physics_informed")),
60
+ ("conformal_deeponet", "Conformalized-DeepONet", ("uncertainty", "surrogate")),
61
+ ("tante", "TANTE", ("surrogate", "general_pde_solver")),
62
+ ("riemannonet", "RiemannONet", ("physics_informed", "specialized", "surrogate")),
63
+ ("energy_transformer", "Energy Transformer Flow Reconstruction", ("inverse", "surrogate", "specialized")),
64
+ ("fun_diff", "FunDiff", ("generative", "physics_informed", "general_pde_solver")),
65
+ ("flow_matching_pde", "Flow Matching for PDEs", ("generative", "physics_informed", "general_pde_solver")),
66
+ ]
67
+
68
+ _GEOMETRY = {"geo_fno", "gino", "gnot", "transolver", "upt", "meshgraphnets", "domino", "aerotransformer", "geometry_preconditioner", "revit"}
69
+ _THREED = {"gino", "upt", "domino", "p3d", "aerotransformer", "tadpole", "inc", "pict", "diffsph", "neurosem", "np_newton", "geometry_preconditioner", "neuraldem", "revit", "deepmmnet", "tante", "energy_transformer"}
70
+ _ACCEL = {"solver_in_loop", "inc", "pict", "diffsph", "neurosem", "np_newton", "geometry_preconditioner"}
71
+ _LONG = {"pinnsformer", "fno", "f_fno", "pibert", "fourierflow", "pde_refiner", "dpot", "poseidon", "prose_fd", "bcat", "p3d", "tadpole", "solver_in_loop", "inc", "neuraldem", "tante"}
72
+ _PARTICLE = {"diffsph", "neuraldem"}
73
+ _UQ = {"conformal_deeponet", "fun_diff", "fourierflow"}
74
+
75
+ _ARCH = {
76
+ "pinn": "coordinate network with PDE residuals",
77
+ "deeponet": "branch-trunk operator network",
78
+ "fno": "Fourier neural operator",
79
+ "pibert": "bidirectional transformer with Fourier-wavelet embeddings and physics-biased attention",
80
+ "fourierflow": "frequency-guided generative flow model",
81
+ "transolver": "physics-attention transformer on general geometries",
82
+ "upt": "latent universal physics transformer",
83
+ "gino": "graph lifting, latent FNO, and point projection",
84
+ "domino": "decomposable multiscale iterative neural operator",
85
+ "p3d": "scalable 3D global-context surrogate",
86
+ "aerotransformer": "pretrained transformer for 3D aerodynamics",
87
+ "inc": "equation-level indirect neural corrector",
88
+ "pict": "differentiable GPU multi-block PISO solver",
89
+ "diffsph": "differentiable smoothed-particle hydrodynamics",
90
+ "np_newton": "neural-operator preconditioned Newton solver",
91
+ "geometry_preconditioner": "geometry-aware neural iterative preconditioner",
92
+ }
93
+
94
+ _REF = {
95
+ "pibert": "Chakraborty, Pan & Chen, 2026",
96
+ "fno": "Li et al., ICLR 2021",
97
+ "deeponet": "Lu et al., Nature Machine Intelligence 2021",
98
+ "transolver": "Wu et al., ICML 2024",
99
+ "upt": "Alkin et al., NeurIPS 2024",
100
+ "gino": "Li et al., NeurIPS 2023",
101
+ "inc": "Wei et al., NeurIPS 2025",
102
+ "pict": "Franz et al., JCP 2025",
103
+ "diffsph": "Winchenbach & Thuerey, JCP 2026",
104
+ }
105
+
106
+ _REPO = {
107
+ "pibert": "https://github.com/Samsomyajit/pibert",
108
+ "fno": "https://github.com/neuraloperator/neuraloperator",
109
+ "geo_fno": "https://github.com/neuraloperator/Geo-FNO",
110
+ "transolver": "https://github.com/thuml/Neural-Solver-Library",
111
+ "inc": "https://github.com/tum-pbs/INC",
112
+ }
113
+
114
+
115
+ def _models() -> list[ModelSpec]:
116
+ result: list[ModelSpec] = []
117
+ for model_id, name, categories in _MODEL_ROWS:
118
+ geometry = model_id in _GEOMETRY
119
+ three_d = model_id in _THREED or model_id not in {"pinnsformer", "riemannonet"}
120
+ accel = model_id in _ACCEL
121
+ particle = model_id in _PARTICLE
122
+ mesh_types = ("particle",) if particle else (("unstructured", "point_cloud", "structured") if geometry else ("structured", "meshfree"))
123
+ result.append(ModelSpec(
124
+ id=model_id,
125
+ name=name,
126
+ categories=categories,
127
+ architecture=_ARCH.get(model_id, name + " architecture"),
128
+ tasks=(("acceleration", "corrector", "preconditioner") if accel else ("surrogate", "forecasting")),
129
+ physics=(("particle", "granular") if particle else ("general_pde", "fluid_dynamics")),
130
+ mesh_types=mesh_types,
131
+ geometry_modes=(("varying", "parameterized") if geometry else ("fixed", "parameterized")),
132
+ temporal_modes=(("autoregressive", "unsteady", "steady") if model_id in _LONG or accel else ("steady", "unsteady")),
133
+ dimensions=((1, 2, 3) if three_d else (1, 2)),
134
+ strengths=("registered in the uniform NAVIER-CFD model taxonomy",),
135
+ limitations=("verify claims on the selected CFD benchmark and discretization",),
136
+ reference=_REF.get(model_id, name),
137
+ repository=_REPO.get(model_id),
138
+ integration="external" if model_id in _REPO else "metadata",
139
+ min_memory_gb=24 if model_id in {"gino", "domino", "p3d", "aerotransformer", "neuraldem"} else 8,
140
+ framework="pytorch",
141
+ tags=tuple(filter(None, ["mesh_transfer" if geometry else "", "long_rollout" if model_id in _LONG or accel else "", "uncertainty" if model_id in _UQ else "", "conservative" if accel else "", "3d" if three_d else ""])),
142
+ ))
143
+ return result
144
+
145
+
146
+ def _datasets() -> list[DatasetSpec]:
147
+ rows = [
148
+ ("pdebench", "PDEBench", "Extensive time-dependent PDE benchmark", "AI4Science-WestlakeU/PDEBench", ("navier_stokes", "advection", "reaction_diffusion"), (1, 2, 3), ("structured",), ("fixed",), ("autoregressive",)),
149
+ ("cfdbench", "CFDBench", "CFD benchmark with boundary, property, and geometry shifts", "chen-yingfa/CFDBench", ("cavity", "tube", "dam", "cylinder"), (2,), ("structured",), ("fixed", "varying"), ("steady", "autoregressive")),
150
+ ("realpdebench", "RealPDEBench", "Paired real-world and simulated spatiotemporal systems", "AI4Science-WestlakeU/RealPDEBench", ("cylinder", "fsi", "controlled_cylinder", "foil", "combustion"), (2,), ("structured",), ("fixed", "varying"), ("autoregressive",)),
151
+ ("airfrans", "AirfRANS", "RANS airfoil geometry and operating-condition benchmark", None, ("airfoil",), (2,), ("unstructured", "point_cloud"), ("varying",), ("steady",)),
152
+ ("drivaernetpp", "DrivAerNet++", "Large multimodal vehicle CFD dataset", None, ("vehicle_aerodynamics",), (3,), ("point_cloud", "unstructured"), ("varying",), ("steady",)),
153
+ ("drivaerml", "DrivAerML", "High-fidelity road-car aerodynamic CFD", None, ("vehicle_aerodynamics",), (3,), ("unstructured",), ("varying",), ("steady", "unsteady")),
154
+ ("the_well", "The Well", "Diverse large-scale physics simulations", "polymathic-ai/the_well", ("fluid", "multiphysics"), (2, 3), ("structured",), ("fixed", "varying"), ("autoregressive",)),
155
+ ("apebench", "APEBench", "Autoregressive PDE emulator benchmark", None, ("46_pde_configurations",), (1, 2, 3), ("structured",), ("fixed",), ("autoregressive",)),
156
+ ("scalarflow", "ScalarFlow", "Real volumetric scalar transport flows", None, ("scalar_transport",), (3,), ("structured",), ("fixed",), ("autoregressive",)),
157
+ ("shapenet_car", "ShapeNet-Car", "Vehicle-shape design benchmark", None, ("vehicle_design",), (3,), ("point_cloud",), ("varying",), ("steady",)),
158
+ ("eagle", "EAGLE", "Fluid and geometry learning benchmark", None, ("fluid",), (2, 3), ("structured", "unstructured"), ("varying",), ("autoregressive",)),
159
+ ]
160
+ return [DatasetSpec(
161
+ id=i, name=n, description=d, tasks=("surrogate", "benchmark"), physics=("fluid_dynamics", "general_pde"),
162
+ dimensions=dim, mesh_types=mesh, geometry_modes=geo, temporal_modes=temp,
163
+ hf_repo_id=hf, source_url=None, license=None, size=None, scenarios=scenarios,
164
+ notes=("Pin dataset revisions and preserve official splits.",)
165
+ ) for i, n, d, hf, scenarios, dim, mesh, geo, temp in rows]
166
+
167
+
168
+ @dataclass
169
+ class Catalog:
170
+ models: list[ModelSpec]
171
+ datasets: list[DatasetSpec]
172
+
173
+ @classmethod
174
+ def load_builtin(cls) -> "Catalog":
175
+ return cls(models=_models(), datasets=_datasets())
176
+
177
+ @classmethod
178
+ def from_paths(cls, model_path: str | Path, dataset_path: str | Path) -> "Catalog":
179
+ model_data = json.loads(Path(model_path).read_text(encoding="utf-8"))
180
+ dataset_data = json.loads(Path(dataset_path).read_text(encoding="utf-8"))
181
+ return cls([ModelSpec.from_dict(x) for x in model_data], [DatasetSpec.from_dict(x) for x in dataset_data])
182
+
183
+ def model(self, model_id: str) -> ModelSpec:
184
+ for item in self.models:
185
+ if item.id == model_id:
186
+ return item
187
+ raise KeyError(f"Unknown model id: {model_id}")
188
+
189
+ def dataset(self, dataset_id: str) -> DatasetSpec:
190
+ for item in self.datasets:
191
+ if item.id == dataset_id:
192
+ return item
193
+ raise KeyError(f"Unknown dataset id: {dataset_id}")
194
+
195
+ def models_by_category(self, category: str) -> list[ModelSpec]:
196
+ return [x for x in self.models if category in x.categories]
197
+
198
+ def search_models(self, query: str) -> list[ModelSpec]:
199
+ q = query.lower()
200
+ return [x for x in self.models if q in x.name.lower() or q in x.architecture.lower() or any(q in t.lower() for t in x.tags)]
201
+
202
+ def extend_models(self, models: Iterable[ModelSpec]) -> None:
203
+ existing = {x.id for x in self.models}
204
+ for model in models:
205
+ if model.id in existing:
206
+ raise ValueError(f"Duplicate model id: {model.id}")
207
+ self.models.append(model)
208
+ existing.add(model.id)