Skip to content

dough

Roll your own typed wrapper.

Warning

dough is still pre-1.0. The API is still evolving and can break in minor releases.

dough is a small framework for building typed Python wrappers around the output files of simulation codes. It ships the generic machinery โ€” file parsers, declarative output mappings, optional library converters โ€” and stays out of the way of the code-specific details. Code-specific wrappers live in their own packages (see Packages built on dough below).

๐Ÿ“ฆ Installation

Wrapper packages declare dough as a runtime dependency in their own pyproject.toml:

[project]
dependencies = [
    "dough>=0.3",
]

For ad-hoc exploration you can of course also just install it directly:

pip install dough

๐ŸŒฏ The current layers

  • Parsers โ€” turn one output file into a plain dict. One parser per file format; stateless, with a single parse(content) method.
  • Output mappings โ€” frozen dataclasses whose fields are Annotated[T, Spec(...)]. Each field declares the output's name, type, unit (in its docstring), and how to extract it from the parsed dicts via a glom Spec. One source of truth per quantity.
  • Converters โ€” optional adapters that turn base Python outputs into ase, pymatgen, or aiida-core objects. Heavy third-party imports stay lazy so wrapper packages don't pay for them at import time.

See the outputs design page for the full picture.

โœจ A minimal example

Declare the outputs as Annotated[T, Spec(...)] fields on an @output_mapping class, then bind it to a BaseOutput subclass:

from typing import Annotated
from dough.outputs import BaseOutput, output_mapping
from glom import Spec


@output_mapping
class _MyMapping:
    fermi_energy: Annotated[float, Spec("xml.output.band_structure.fermi_energy")]
    """Fermi energy in eV."""


class MyOutput(BaseOutput[_MyMapping]):
    ...

Each subclass implements from_dir (and/or from_files) to wire in its parsers. Once instantiated, the declared fields are reachable as a typed namespace:

my_out = MyOutput.from_dir("/path/to/run_dir")
my_out.outputs.fermi_energy  # -> float

โš–๏ธ Units and pint

Stack a Unit marker to label a field's physical unit:

@output_mapping
class _MyMapping:
    total_energy: Annotated[float, Spec("energy"), Unit("eV")]
    xc_functional: Annotated[str, Spec("xc")]

Users can obtain a pint Quantity when reading the output:

out.get_output("total_energy")             # 6.23
out.get_output("total_energy", to="pint")  # <Quantity(6.23, 'eV')>
out.get_output("total_energy", to="pint").to("Ha")

Non-numeric fields and fields without a Unit marker pass through unchanged under to="pint". Install with pip install dough[pint]. See the units design page for the full contract.

๐Ÿงช Testing

dough.testing ships shared pytest fixtures (json_serializer, robust_data_regression_check) used by downstream wrapper packages for regression tests. It's an opt-in plugin โ€” activate it in your top-level conftest.py with pytest_plugins = ["dough.testing.plugin"]. See the testing design page.

๐ŸฅŸ Packages built on dough

Package Code Status
qe-tools Quantum ESPRESSO alpha โ€” pw.x, dos.x outputs
strudel VASP alpha โ€” basic outputs + magnetization