Note
Go to the end to download the full example code.
Work with configuration files¶
NeoFOAM configs are BaseConfig (Pydantic) subclasses tagged
with an @IOStrategy decorator that binds a file and a format
(YAML / JSON / OpenFOAM dictionary) to the class. The class then
gains .load() / .save() / .collect_errors() methods that
read and write that file with full type validation.
This page walks through the four things you typically do with configs — declare, load, validate, and share a file between multiple configs via subdicts. The YAML fixtures shipped alongside this example are staged into a temporary case directory and loaded with the real on-disk reader; the file contents shown below are read straight from those fixtures by Sphinx.
Set up a throwaway case directory and locate the fixtures¶
Every example below stages real YAML files from
examples/how-to/*.yaml into the same temporary directory. The
real solver hands you a case_dir argument; here we make our
own. HERE resolves to the directory containing this script so
every fixture stays addressable, including when sphinx-gallery
exec()``s the script with no ``__file__.
import inspect
import shutil
import tempfile
from pathlib import Path
from neofoam.io import BaseConfig, IOStrategy, YAML
def _here() -> None:
"""Marker used to locate this script on disk via inspect.getfile()."""
HERE = Path(inspect.getfile(_here)).resolve().parent
case_dir = Path(tempfile.mkdtemp(prefix="neofoam_howto_"))
print("case_dir =", case_dir)
case_dir = /tmp/neofoam_howto_xy84ustr
Declare a config class¶
@IOStrategy(YAML("...yaml")) binds the class to a file and a
format in one decorator. The class body is a plain Pydantic model:
field names, types, and default values become the schema you’ll
validate against. The decorator attaches an io_config class var
the framework reads at load() time.
@IOStrategy(YAML("solver_config.yaml"))
class SolverConfig(BaseConfig):
name: str
dt: float
end_time: float
write_interval: int = 10
# Show what the binding looks like.
print("file:", SolverConfig.io_config.file)
print("reader:", type(SolverConfig.io_config.reader).__name__)
print("default path under case_dir:", SolverConfig.get_default_path(case_dir))
file: solver_config.yaml
reader: YAMLStrategy
default path under case_dir: /tmp/neofoam_howto_xy84ustr/solver_config.yaml
Read a config from disk¶
In a real run the file already exists in the case directory — the
user wrote it, or it shipped with the tutorial. The class method
BaseConfig.load() takes a case_dir and resolves the
filename via io_config.file, so you never spell the path twice.
The fixture used here lives at examples/how-to/solver_config.yaml:
name: pisoFoam
dt: 1.0e-3
end_time: 1.0
write_interval: 20
shutil.copy(HERE / "solver_config.yaml", case_dir / "solver_config.yaml")
cfg = SolverConfig.load(case_dir=case_dir)
print("loaded:", cfg)
print("cfg.dt =", cfg.dt, "(type:", type(cfg.dt).__name__ + ")")
loaded: name='pisoFoam' dt=0.001 end_time=1.0 write_interval=20
cfg.dt = 0.001 (type: float)
Write changes back to disk¶
Once loaded, the instance behaves like any Pydantic model —
mutate fields in memory, then BaseConfig.save() writes the
YAML back to the same path the loader read from.
cfg.write_interval = 50
cfg.save(case_dir=case_dir)
print("file after save:")
print((case_dir / "solver_config.yaml").read_text())
file after save:
name: pisoFoam
dt: 0.001
end_time: 1.0
write_interval: 50
Loading bad data raises with everything wrong at once¶
Pydantic batches errors per call — one ValidationError reports
every field that’s wrong, not just the first. Useful when a user
mistypes three keys in one file.
The intentionally-broken fixture lives at
examples/how-to/solver_config_bad.yaml:
name: 42 # should be a string
dt: "not-a-float" # should be a float
end_time: -1.0 # would be caught by a validator (not added here)
shutil.copy(HERE / "solver_config_bad.yaml", case_dir / "solver_config.yaml")
try:
SolverConfig.load(case_dir=case_dir)
except Exception as exc:
print(type(exc).__name__, "raised — listing every problem:")
for err in exc.errors():
print(f" {'.'.join(map(str, err['loc'])):12s} {err['msg']}")
ValidationError raised — listing every problem:
name Input should be a valid string
dt Input should be a valid number, unable to parse string as a number
Validate without raising via collect_errors¶
Use BaseConfig.collect_errors() when you want a flat list of
problems with file/subdict context — typical inside a CLI that
shows the user every error before exiting. Returns [] on
success. The framework’s bulk validator
(neofoam.io.validate_models()) is built on top of this.
errors = SolverConfig.collect_errors(case_dir=case_dir)
print(f"{len(errors)} error(s):")
for e in errors:
field = ".".join(map(str, e.field)) if e.field else "<file>"
print(f" {field:12s} {e.error_type}: {e.message[:60]}")
2 error(s):
name string_type: Input should be a valid string
dt float_parsing: Input should be a valid number, unable to parse string as a
Other formats¶
Swap the strategy in the decorator to read the same data shape from a different format — no other changes needed. The three strategies that ship today:
YAML()— human-readable, supports subdicts, round-trips floats safely.JSON()— strict, machine-friendly, also supports subdicts.OF()— OpenFOAM dictionary format, lets you point at an existing case’ssystem/fvSolutionorconstant/transportPropertiesdirectly.
A typical migration looks like changing YAML("foo.yaml") to
OF("foo") and pointing the case at the OpenFOAM-format file.
See also¶
neofoam.framework.dependency_resolver —
BaseConfigparameters in operations are pulled fromruntime.configvia the same resolver shown in Inject dependencies into operations.Register a new model — the LOAD stage’s job is to instantiate one of these configs per model and return it inside a
LoadResult.
Total running time of the script: (0 minutes 0.008 seconds)