1"""Every parameter a config carries, and what the schema says about it.
3The Parameters workspace lists these: one row per leaf of ``PintleEngineConfig``, with its current
4value, its schema default, whether it differs, its units (taken from the ``[unit]`` in its
5description) and the description itself. It walks the pydantic models, so a field added to
6``config_schemas.py`` appears here without anyone remembering to list it.
8A block that is null in the config (``chamber: null``) is one row of kind ``block``; its fields
9appear once it is set. Keys a config carries that the schema does not declare (models with
10``extra="allow"``) are rows of kind ``undeclared``: they are passed through, so they are shown.
12from __future__
import annotations
16from typing
import Any, Dict, List, Optional, Tuple, Type, Union, get_args, get_origin
18from pydantic
import BaseModel
19from pydantic_core
import PydanticUndefined
21_UNIT = re.compile(
r"\[([^\[\]]{1,28})\]")
23_NOT_UNIT = re.compile(
r"[A-Z]{4,}| — | - ")
26SECTIONS: List[Tuple[str, str]] = [
27 (
"design_requirements",
"Requirements & optimizer"),
28 (
"fluids",
"Propellants"),
29 (
"injector",
"Injector"),
30 (
"discharge",
"Discharge & Cd"),
31 (
"spray",
"Spray & vaporisation"),
32 (
"combustion",
"Combustion & CEA"),
33 (
"chamber_geometry",
"Chamber & nozzle"),
34 (
"chamber",
"Chamber (legacy)"),
35 (
"nozzle",
"Nozzle (legacy)"),
36 (
"ablative_cooling",
"Ablative liner"),
37 (
"graphite_insert",
"Graphite insert"),
38 (
"stainless_steel_case",
"Case"),
39 (
"regen_cooling",
"Regenerative cooling"),
40 (
"film_cooling",
"Film cooling"),
41 (
"stability",
"Stability"),
42 (
"feed_system",
"Feed system"),
43 (
"lox_tank",
"LOX tank"),
44 (
"fuel_tank",
"Fuel tank"),
45 (
"press_tank",
"Pressurant"),
46 (
"rocket",
"Vehicle"),
47 (
"environment",
"Environment"),
48 (
"thrust",
"Thrust profile"),
49 (
"pressure_curves",
"Pressure curves"),
51 (
"optimizer",
"Optimizer"),
52 (
"propellant_preset",
"Propellant preset"),
53 (
"design_valid_for",
"Design state"),
55_SECTION_LABEL = dict(SECTIONS)
58LEGACY_SECTIONS = frozenset({
"chamber",
"nozzle"})
61INTERNAL_SECTIONS = frozenset({
"design_valid_for"})
65 """The BaseModel classes an annotation can hold (through Optional/Union)."""
66 if isinstance(annotation, type)
and issubclass(annotation, BaseModel):
68 if get_origin(annotation)
is Union:
69 return [m
for a
in get_args(annotation)
for m
in _models_in(a)]
74 """``Dict[str, Model]`` (possibly Optional) -> Model."""
75 for a
in ([annotation] + list(get_args(annotation))
if get_origin(annotation)
is Union
else [annotation]):
76 if get_origin(a)
in (dict, Dict):
86 origin = get_origin(annotation)
88 parts = [a
for a
in get_args(annotation)
if a
is not type(
None)]
89 base =
" | ".join(
_type_name(a)
for a
in parts)
90 return base + (
" | null" if len(parts) < len(get_args(annotation))
else "")
91 if origin
is not None and getattr(origin,
"__name__",
"") ==
"Literal":
93 if str(annotation).startswith(
"typing.Literal")
or getattr(annotation,
"__origin__",
None)
is not None and "Literal" in str(annotation):
95 if origin
in (list, List):
97 if origin
in (dict, Dict):
99 return getattr(annotation,
"__name__", str(annotation))
102def _choices(annotation: Any) -> Optional[List[Any]]:
103 """Allowed values of a Literal (possibly Optional) annotation."""
104 args = get_args(annotation)
if get_origin(annotation)
is Union
else [annotation]
107 if "Literal" in str(a):
108 out.extend(get_args(a))
113 if fi.default_factory
is not None:
115 v = fi.default_factory()
118 return v.model_dump()
if isinstance(v, BaseModel)
else v
119 return None if fi.default
is PydanticUndefined
else fi.default
123 if isinstance(v, BaseModel):
124 return v.model_dump()
125 if isinstance(v, float)
and not math.isfinite(v):
131 if isinstance(value, float)
and isinstance(default, (int, float))
and default
is not None:
132 return not math.isclose(value, float(default), rel_tol=1e-12, abs_tol=0.0)
136def _leaf(path: str, section: str, fi: Any, value: Any) -> Dict[str, Any]:
137 desc = (fi.description
or "").strip()
138 m = _UNIT.search(desc)
140 required = fi.is_required()
149 "required": required,
150 "modified": (
not required)
and _differs(value, default),
151 "unit": m.group(1)
if m
and not _NOT_UNIT.search(m.group(1))
else None,
156def _walk(model: BaseModel, prefix: str, section: str, out: List[Dict[str, Any]]) ->
None:
158 for name, fi
in cls.model_fields.items():
159 path = f
"{prefix}{name}"
160 value = getattr(model, name,
None)
163 for key, sub
in (value
or {}).items():
164 if isinstance(sub, BaseModel):
165 _walk(sub, f
"{path}.{key}.", section, out)
168 if isinstance(value, BaseModel):
169 _walk(value, f
"{path}.", section, out)
171 out.append({
"path": path,
"section": section,
"kind":
"block",
"type":
"block",
172 "value":
None,
"default":
None,
"modified":
False,
"unit":
None,
173 "choices":
None,
"required":
False,
174 "description": (fi.description
or "").strip()
or "not set"})
176 out.append(
_leaf(path, section, fi, value))
177 for key, value
in (model.model_extra
or {}).items():
178 out.append({
"path": f
"{prefix}{key}",
"section": section,
"kind":
"undeclared",
179 "type": type(value).__name__,
"value":
_plain(value),
"default":
None,
180 "modified":
False,
"unit":
None,
"choices":
None,
"required":
False,
181 "description":
"not declared in the schema; passed through as written"})
185 """All parameters of ``config`` (a PintleEngineConfig), grouped by section."""
186 rows: List[Dict[str, Any]] = []
187 for name, fi
in type(config).model_fields.items():
188 value = getattr(config, name,
None)
192 for key, sub
in (value
or {}).items():
193 if isinstance(sub, BaseModel):
194 _walk(sub, f
"{name}.{key}.", section, rows)
197 if isinstance(value, BaseModel):
198 _walk(value, f
"{name}.", section, rows)
200 rows.append({
"path": name,
"section": section,
"kind":
"block",
"type":
"block",
201 "value":
None,
"default":
None,
"modified":
False,
"unit":
None,
202 "choices":
None,
"required":
False,
203 "description": (fi.description
or "").strip()
or "not set"})
205 rows.append(
_leaf(name, section, fi, value))
206 order = {s: i
for i, (s, _)
in enumerate(SECTIONS)}
207 sections = sorted({r[
"section"]
for r
in rows}, key=
lambda s: order.get(s, len(order)))
209 "sections": [{
"key": s,
"label": _SECTION_LABEL.get(s, s.replace(
"_",
" ").capitalize()),
210 "count": sum(1
for r
in rows
if r[
"section"] == s),
211 "modified": sum(1
for r
in rows
if r[
"section"] == s
and r[
"modified"]),
212 "legacy": s
in LEGACY_SECTIONS,
213 "internal": s
in INTERNAL_SECTIONS}
List[Type[BaseModel]] _models_in(Any annotation)
str _type_name(Any annotation)
Optional[List[Any]] _choices(Any annotation)
Optional[Type[BaseModel]] _dict_value_model(Any annotation)
None _walk(BaseModel model, str prefix, str section, List[Dict[str, Any]] out)
Dict[str, Any] _leaf(str path, str section, Any fi, Any value)
bool _differs(Any value, Any default)
Dict[str, Any] config_parameters(BaseModel config)