Skip to content

Configuration

The configuration layer maps a TOML (or YAML) file onto a tree of validated Pydantic models. StellarConfig is the root; each section of the config file maps to one of its sub-models.

Model hierarchy

StellarConfig
├── model      → ModelConfig
├── chemistry  → ChemistryConfig
├── grid       → GridConfig
├── parameters → ParameterConfig
└── plot       → PlotConfig

StellarConfig

StellarConfig pydantic-model

Bases: BaseModel

Top-level configuration for stellar evolution

Fields:

Validators:

  • validate_all
Source code in src/keppy/models.py
class StellarConfig(BaseModel):
    """Top-level configuration for stellar evolution"""

    # fmt: off
    model:      ModelConfig      = Field(default_factory=ModelConfig)
    chemistry:  ChemistryConfig  = Field(default_factory=ChemistryConfig)
    grid:       GridConfig       = Field(default_factory=GridConfig)
    parameters: ParameterConfig  = Field(default_factory=ParameterConfig)
    plot:       PlotConfig       = Field(default_factory=PlotConfig)
    # fmt: on

    output_dir: Optional[str | Path] = "./output"

    @cached_property
    def lane_emden(self) -> LaneEmdenGrid:
        """The Lane-Emden grid for this config, built once on first access.

        Pure function of ``model.mass``, ``chemistry.mu`` and the ``grid``
        overrides, so it is (re)built lazily after ``override()``/reload rather
        than eagerly during validation. ``massunit`` is excluded from the grid
        kwargs (it is a post-grid scaling applied when emitting cards).
        """
        return LaneEmdenGrid(
            mass=self.model.mass,
            mu=self.chemistry.mu,
            **self.grid.model_dump(exclude_none=True),
        )

    @model_validator(mode="after")
    def validate_all(self):
        # scale the composition to Z if requested
        composition = self.chemistry.composition
        if self.chemistry.scale_to_Z:
            if composition.metallicity != self.model.metallicity:
                logger.info(
                    "Scaling to requested Z of {} from {}".format(
                        self.model.metallicity, composition.metallicity
                    )
                )
                self.chemistry.composition = composition.scale_metallicity(
                    self.model.metallicity
                )
            else:
                logger.info("Scaling of Z requested but not necessary")

            # The scaled composition differs from the preset file in KEPLER_DATA,
            # so use a custom burngen name that encodes the target metallicity.
            # Custom compositions already carry a user-supplied burngen name.
            if self.chemistry.preset is not None:
                self.chemistry.burngen = (
                    f"{self.chemistry.preset}z{self.model.metallicity:.4f}bg"
                )

        # optional helium scaling (preserves metals, draws from/adds to hydrogen).
        # Applied after Z scaling so metals are already set to the target.
        if self.chemistry.initial_Y is not None:
            logger.info(
                "Scaling to requested Y of {}".format(self.chemistry.initial_Y)
            )
            self.chemistry.composition = self.chemistry.composition.scale_helium(
                self.chemistry.initial_Y
            )

        # plotting settings parse
        self.parameters.parameters.update({"ipixtype": self.plot.ipixtype})
        self.parameters.parameters.update({"irtype": self.plot.xaxis})
        self.parameters.parameters.update({"npixedit": self.plot.interval})
        self.parameters.parameters.update(
            {"iwinsize": f"{self.plot.window_width:04d}{self.plot.window_height:04d}"}
        )

        # alex's sensible defaults from kepgen
        if self.model.aw != 0:
            #'swich on rotational mixing processes (nangmix)
            self.parameters.parameters.update(
                {
                    k: v
                    for k, v in [
                        ("nangmix", 1),
                        # efficiencies, these number seem arbitray but they're alex's defaults
                        ("angfau", 5.00e-2),
                        ("angfc", 3.33e-2),
                        ("angfjc", 1.0),
                        ("angrcrit", 2.5e3),  # critical Reynolds num
                        ("angric", 0.25),
                        ("angfjdsi", 1.0),
                        ("angfjshi", 1.0),
                        ("angfjssi", 0.9),
                        ("angfjez", 0.9),
                        ("angfjgsf", 0.9),
                        # smoothing of gradients and time derivative
                        ("angsmt", 0.2),
                        ("nangsmg", 2),
                        ("angsml", 1.0e-3),
                        ("angsmm", 1.0e-3),
                        # surface hydrostatic equilibrium above ~0.1Msun?
                        ("hstatym", 1.0e32),
                    ]
                }
            )

        # TODO: vertical velocity fields
        self.model.u = 0

        # adaptive
        if self.chemistry.adaptive:
            self.parameters.parameters.update({"nadapb": 1})

        # NOTE: the Lane-Emden grid is built lazily via the `lane_emden`
        # cached_property, not here, so validation stays cheap and override()/
        # to_toml() do not re-run the integration on every reconstruction.

        # lburn options
        if self.chemistry.use_burn:
            self.parameters.parameters.update(
                {
                    # SWITCH ON FULL IMPLICIT BURN NETWORK
                    "lburn": 1,
                }
            )
        return self

    def override(self, **sections) -> "StellarConfig":
        """Return a new, fully re-validated config with section fields changed.

        Intended for a "load a template, then tweak it" workflow::

            cfg = load_config("template.toml")
            cfg = cfg.override(
                model={"mass": 5.0, "metallicity": 0.001},
                chemistry={"preset": "sollo09"},
            )

        Each keyword names a config section (``model``, ``chemistry``,
        ``grid``, ``parameters``, ``plot``) and maps to a dict of field
        overrides that are shallow-merged over the existing values. Scalar
        top-level fields (e.g. ``output_dir``) may be passed directly.

        The whole validation pipeline re-runs on the merged data, so the
        Lane-Emden grid is rebuilt and the composition re-scaled. The original
        config is left untouched; a new instance is returned.

        Notes
        -----
        * ``preset`` and ``custom`` are mutually exclusive: supplying one
          clears the other.
        * Changing ``preset``/``custom``/``name`` resets ``burngen`` (unless you
          override it explicitly) so it is regenerated for the new composition.
        """
        data = self.model_dump()

        for key, value in sections.items():
            if key not in data:
                valid = ", ".join(k for k, v in data.items() if isinstance(v, dict))
                raise ValueError(
                    f"Unknown config section '{key}'. Valid sections: {valid}."
                )

            # scalar top-level field (e.g. output_dir): replace outright
            if not isinstance(data[key], dict) or not isinstance(value, dict):
                data[key] = value
                continue

            merged = {**data[key], **value}

            if key == "chemistry":
                # preset/custom are mutually exclusive
                if value.get("preset") is not None:
                    merged["custom"] = None
                if value.get("custom") is not None:
                    merged["preset"] = None
                # regenerate burngen for the new composition unless pinned
                if "burngen" not in value and (
                    {"preset", "custom", "name"} & value.keys()
                ):
                    merged["burngen"] = None

            data[key] = merged

        return StellarConfig(**data)

    def to_toml(self, path: str | Path | None = None) -> str:
        """Serialise this config back to TOML, round-tripping ``load_config``.

        Only input-level fields are emitted (e.g. ``mass``, ``preset``,
        ``scale_to_Z``, ``initial_Y``, parameter overrides). Derived values --
        the resolved composition, the computed plot parameters, the Lane-Emden
        grid -- are *not* written; they are reproduced when the file is reloaded.

        Parameters
        ----------
        path:
            If given, the TOML is also written to this path.

        Returns
        -------
        str
            The TOML document text.
        """

        def _order(d: dict) -> dict:
            # TOML requires scalar keys before sub-tables, at every level
            scalars = {k: v for k, v in d.items() if not isinstance(v, dict)}
            tables = {k: _order(v) for k, v in d.items() if isinstance(v, dict)}
            return {**scalars, **tables}

        # exclude_none: TOML has no null; drop unset optionals (preset/custom/...)
        data = _order(self.model_dump(exclude_none=True))
        text = tomlkit.dumps(data)

        if path is not None:
            Path(path).write_text(text)
        return text

lane_emden cached property

lane_emden: LaneEmdenGrid

The Lane-Emden grid for this config, built once on first access.

Pure function of model.mass, chemistry.mu and the grid overrides, so it is (re)built lazily after override()/reload rather than eagerly during validation. massunit is excluded from the grid kwargs (it is a post-grid scaling applied when emitting cards).

override

override(**sections) -> StellarConfig

Return a new, fully re-validated config with section fields changed.

Intended for a "load a template, then tweak it" workflow::

cfg = load_config("template.toml")
cfg = cfg.override(
    model={"mass": 5.0, "metallicity": 0.001},
    chemistry={"preset": "sollo09"},
)

Each keyword names a config section (model, chemistry, grid, parameters, plot) and maps to a dict of field overrides that are shallow-merged over the existing values. Scalar top-level fields (e.g. output_dir) may be passed directly.

The whole validation pipeline re-runs on the merged data, so the Lane-Emden grid is rebuilt and the composition re-scaled. The original config is left untouched; a new instance is returned.

Notes
  • preset and custom are mutually exclusive: supplying one clears the other.
  • Changing preset/custom/name resets burngen (unless you override it explicitly) so it is regenerated for the new composition.
Source code in src/keppy/models.py
def override(self, **sections) -> "StellarConfig":
    """Return a new, fully re-validated config with section fields changed.

    Intended for a "load a template, then tweak it" workflow::

        cfg = load_config("template.toml")
        cfg = cfg.override(
            model={"mass": 5.0, "metallicity": 0.001},
            chemistry={"preset": "sollo09"},
        )

    Each keyword names a config section (``model``, ``chemistry``,
    ``grid``, ``parameters``, ``plot``) and maps to a dict of field
    overrides that are shallow-merged over the existing values. Scalar
    top-level fields (e.g. ``output_dir``) may be passed directly.

    The whole validation pipeline re-runs on the merged data, so the
    Lane-Emden grid is rebuilt and the composition re-scaled. The original
    config is left untouched; a new instance is returned.

    Notes
    -----
    * ``preset`` and ``custom`` are mutually exclusive: supplying one
      clears the other.
    * Changing ``preset``/``custom``/``name`` resets ``burngen`` (unless you
      override it explicitly) so it is regenerated for the new composition.
    """
    data = self.model_dump()

    for key, value in sections.items():
        if key not in data:
            valid = ", ".join(k for k, v in data.items() if isinstance(v, dict))
            raise ValueError(
                f"Unknown config section '{key}'. Valid sections: {valid}."
            )

        # scalar top-level field (e.g. output_dir): replace outright
        if not isinstance(data[key], dict) or not isinstance(value, dict):
            data[key] = value
            continue

        merged = {**data[key], **value}

        if key == "chemistry":
            # preset/custom are mutually exclusive
            if value.get("preset") is not None:
                merged["custom"] = None
            if value.get("custom") is not None:
                merged["preset"] = None
            # regenerate burngen for the new composition unless pinned
            if "burngen" not in value and (
                {"preset", "custom", "name"} & value.keys()
            ):
                merged["burngen"] = None

        data[key] = merged

    return StellarConfig(**data)

to_toml

to_toml(path: str | Path | None = None) -> str

Serialise this config back to TOML, round-tripping load_config.

Only input-level fields are emitted (e.g. mass, preset, scale_to_Z, initial_Y, parameter overrides). Derived values -- the resolved composition, the computed plot parameters, the Lane-Emden grid -- are not written; they are reproduced when the file is reloaded.

Parameters

path: If given, the TOML is also written to this path.

Returns

str The TOML document text.

Source code in src/keppy/models.py
def to_toml(self, path: str | Path | None = None) -> str:
    """Serialise this config back to TOML, round-tripping ``load_config``.

    Only input-level fields are emitted (e.g. ``mass``, ``preset``,
    ``scale_to_Z``, ``initial_Y``, parameter overrides). Derived values --
    the resolved composition, the computed plot parameters, the Lane-Emden
    grid -- are *not* written; they are reproduced when the file is reloaded.

    Parameters
    ----------
    path:
        If given, the TOML is also written to this path.

    Returns
    -------
    str
        The TOML document text.
    """

    def _order(d: dict) -> dict:
        # TOML requires scalar keys before sub-tables, at every level
        scalars = {k: v for k, v in d.items() if not isinstance(v, dict)}
        tables = {k: _order(v) for k, v in d.items() if isinstance(v, dict)}
        return {**scalars, **tables}

    # exclude_none: TOML has no null; drop unset optionals (preset/custom/...)
    data = _order(self.model_dump(exclude_none=True))
    text = tomlkit.dumps(data)

    if path is not None:
        Path(path).write_text(text)
    return text

ModelConfig

ModelConfig pydantic-model

Bases: BaseModel

Basic (higher) star configuration.

Example: ``` [model] mass: 4 metallicity: 0.014 rotation: 0.9 force_hstat: true

Fields:

Validators:

  • validate_massmass
Source code in src/keppy/models.py
class ModelConfig(BaseModel):
    """Basic (higher) star configuration.


    Example:
    ```
    [model]
    mass: 4
    metallicity: 0.014
    rotation: 0.9
    force_hstat: true
    """

    # fmt: off
    mass:             float = Field(1.0,  gt=0,         description="Initial mass in Msun")
    metallicity:      float = Field(0.02, ge=0, le=1,   description="Metal fraction Z")
    rotation:         float = Field(0.0,                description="Rotation rate parameter. >1e6: we use value directly. 0 < val < 1e6: scale with max rotation. val<0: use Z=0 fits.")
    rotation_version:   int = Field(2, ge=1, le=2, description="Algorithm to determine spin. 1: Heger 1998, 2: Woosley & Heger 2012")
    force_hstat:        bool = Field(False,  description="Whether to force the initial configuration into hydrostatic equilibrium. Can lead to noise.")
    u:                 float = 0.
    #force_dstat:        bool = Field(,  description="Algorithm to determine spin. 1: Heger 1998, 2: Woosley & Heger 2012")
    # fmt: on

    @field_validator("mass")
    @classmethod
    def validate_mass(cls, v):
        if v > 100:
            raise ValueError("Mass must be <= 100 solar masses")
        return v

    @property
    def angular(self) -> float:
        """Compute based on alex's fits."""
        angular = self.rotation
        version = self.rotation_version
        if angular > 1.0e6:
            return angular
        if version == 1:
            # old fit (Heger 1998)
            angular = abs(angular) * 2.5e53 * (self.mass / 20) ** 1.65
            logger.info("using Heger 1998")
        elif version == 2:
            # new fit (Woosley & Heger 2012)
            if self.metallicity <= 6.53160290478554e-08:
                J20 = 6.79954e52
                logger.info("using Z=0 value")
            else:
                logger.info("using Z={:g}".format(self.metallicity))
                lm = np.log10(self.metallicity)
                J20 = -0.0196226e53 * lm**2 - 0.11150409e53 * lm + 0.89179526e53
            J100 = J20 * (self.mass / 20.0) ** 1.78

            if angular < 0:
                # do metallicity scaling if it's been requested
                Jt = -J100 * (
                    +0.629960 - 1.060872130505979 * self.metallicity ** (1 / 3)
                ) ** (1.5)
            else:
                Jt = J100
            angular *= Jt
        return angular

    @property
    def aw(self) -> int:
        """Whether there is rotation or not. (?)"""
        return 1 if self.angular != 0.0 else 0

mass pydantic-field

mass: float = 1.0

Initial mass in Msun

metallicity pydantic-field

metallicity: float = 0.02

Metal fraction Z

rotation pydantic-field

rotation: float = 0.0

Rotation rate parameter. >1e6: we use value directly. 0 < val < 1e6: scale with max rotation. val<0: use Z=0 fits.

rotation_version pydantic-field

rotation_version: int = 2

Algorithm to determine spin. 1: Heger 1998, 2: Woosley & Heger 2012

force_hstat pydantic-field

force_hstat: bool = False

Whether to force the initial configuration into hydrostatic equilibrium. Can lead to noise.

angular property

angular: float

Compute based on alex's fits.

aw property

aw: int

Whether there is rotation or not. (?)


ChemistryConfig

ChemistryConfig pydantic-model

Bases: BaseModel

Chemistry (network and materials) config

Example:

[chemistry]
preset = 'as09' # asplund+09
scale_to_Z = true
coprocess = true

Fields:

Validators:

Source code in src/keppy/models.py
class ChemistryConfig(BaseModel):
    """Chemistry (network and materials) config

    Example:
    ```
    [chemistry]
    preset = 'as09' # asplund+09
    scale_to_Z = true
    coprocess = true
    ```
    """

    # MATERIALS
    # fmt: off
    name:       Literal["solcomp", "zero", "mycomp"]  = "mycomp"
    preset:     Optional[str]                         = Field(None,           description="Name of preset composition (exclusive with custom). Requires $KEPLER_DATA to be set.")
    custom:     Optional[dict[str, float]]            = Field(None,           description="Custom ion abundances (exclusive with preset)")
    scale_to_Z: bool                                  = Field(True,           description="Whether to scale the composition to the given metallicity. Setting with a preset will scale all mass fractions to this metallicity")
    initial_Y:  Optional[float]                       = Field(None, ge=0, le=1, description="Optional target initial helium mass fraction Y. When set, the composition is scaled to this Y by drawing from (or adding to) hydrogen while preserving metals, instead of using the raw preset value. Independent of and applied after scale_to_Z.")

    # network
    # TODO: docs say only APPROX @ 1 is allowed, is this true?
    #net: Literal["APPROX"] = Field("APPROX", description='Network name')
    num: Literal[1]        = Field(1,        description='Network number')
    isenet: bool           = Field(True,     description='Whether to enable ISE/NSE networks (#2 and #3)')

    # BURN opts
    coprocess: bool        = Field(False,    description='Whether to enable BURN coprocessing (mapburn only). Default OFF because `use_burn` is generally on.')
    use_burn: bool         = Field(True,     description='Whether to use BURN as the main network (lburn).')
    burngen: Optional[str] = Field(None,     description='BURN generator filepath. Automatically set to name of preset if preset is used.')

    # ADAPNET
    adaptive: bool         = Field(True,     description='Whether to use adaptive BURN network (adapnb).')
    network_z_max: int     = Field(86,     description='Maximum mass number of elements to be allowed in the network.')
    # fmt: on

    @field_validator("preset")
    @classmethod
    def validate_preset(cls, v):
        """Check that preset name is valid"""
        if v is not None and v not in Composition.list_presets():
            available = ", ".join(Composition.list_presets())
            raise ValueError(f"Unknown preset '{v}'. Available presets: {available}")
        return v

    @model_validator(mode="after")
    def validate_burngen(self):
        """Auto-set burngen name if not explicitly specified."""
        if self.burngen is None:
            if self.preset is not None:
                self.burngen = f"{self.preset}bg"
                if len(self.burngen) > 8:
                    self.burngen = f"{self.preset[3:]}bg"
            else:
                self.burngen = f"{self.name}bg"
        return self

    @model_validator(mode="after")
    def validate_exclusive(self):
        """Ensure either preset or custom is specified, not both or neither"""
        if self.preset is None and self.custom is None:
            raise ValueError(
                "Must specify either 'preset' or 'custom' composition. "
                f"Available presets: {', '.join(Composition.list_presets())}"
            )
        if self.preset is not None and self.custom is not None:
            raise ValueError(
                "Cannot specify both 'preset' and 'custom' composition. Choose one."
            )

        return self

    @property
    def composition(self) -> Composition:
        return self._composition

    @composition.setter
    def composition(self, composition: Composition) -> None:
        self._composition = composition

    @model_validator(mode="after")
    def init_composition(self):
        """Initializes composition object"""
        if self.preset is not None:
            self._composition = Composition.load_kepler_file(self.preset)
        elif self.custom is not None:
            self._composition = Composition.from_dict(self.custom)
        else:
            raise ValueError("Something is wrong. Custom and Preset are not specified.")
        return self

    @property
    def mu(self) -> float:
        """Mean molecular weight, via Composition routine"""
        return self.composition.mu

preset pydantic-field

preset: Optional[str] = None

Name of preset composition (exclusive with custom). Requires $KEPLER_DATA to be set.

custom pydantic-field

custom: Optional[dict[str, float]] = None

Custom ion abundances (exclusive with preset)

scale_to_Z pydantic-field

scale_to_Z: bool = True

Whether to scale the composition to the given metallicity. Setting with a preset will scale all mass fractions to this metallicity

initial_Y pydantic-field

initial_Y: Optional[float] = None

Optional target initial helium mass fraction Y. When set, the composition is scaled to this Y by drawing from (or adding to) hydrogen while preserving metals, instead of using the raw preset value. Independent of and applied after scale_to_Z.

num pydantic-field

num: Literal[1] = 1

Network number

isenet pydantic-field

isenet: bool = True

Whether to enable ISE/NSE networks (#2 and #3)

coprocess pydantic-field

coprocess: bool = False

Whether to enable BURN coprocessing (mapburn only). Default OFF because use_burn is generally on.

use_burn pydantic-field

use_burn: bool = True

Whether to use BURN as the main network (lburn).

burngen pydantic-field

burngen: Optional[str] = None

BURN generator filepath. Automatically set to name of preset if preset is used.

adaptive pydantic-field

adaptive: bool = True

Whether to use adaptive BURN network (adapnb).

network_z_max pydantic-field

network_z_max: int = 86

Maximum mass number of elements to be allowed in the network.

mu property

mu: float

Mean molecular weight, via Composition routine

validate_preset pydantic-validator

validate_preset(v)

Check that preset name is valid

Source code in src/keppy/models.py
@field_validator("preset")
@classmethod
def validate_preset(cls, v):
    """Check that preset name is valid"""
    if v is not None and v not in Composition.list_presets():
        available = ", ".join(Composition.list_presets())
        raise ValueError(f"Unknown preset '{v}'. Available presets: {available}")
    return v

validate_burngen pydantic-validator

validate_burngen()

Auto-set burngen name if not explicitly specified.

Source code in src/keppy/models.py
@model_validator(mode="after")
def validate_burngen(self):
    """Auto-set burngen name if not explicitly specified."""
    if self.burngen is None:
        if self.preset is not None:
            self.burngen = f"{self.preset}bg"
            if len(self.burngen) > 8:
                self.burngen = f"{self.preset[3:]}bg"
        else:
            self.burngen = f"{self.name}bg"
    return self

validate_exclusive pydantic-validator

validate_exclusive()

Ensure either preset or custom is specified, not both or neither

Source code in src/keppy/models.py
@model_validator(mode="after")
def validate_exclusive(self):
    """Ensure either preset or custom is specified, not both or neither"""
    if self.preset is None and self.custom is None:
        raise ValueError(
            "Must specify either 'preset' or 'custom' composition. "
            f"Available presets: {', '.join(Composition.list_presets())}"
        )
    if self.preset is not None and self.custom is not None:
        raise ValueError(
            "Cannot specify both 'preset' and 'custom' composition. Choose one."
        )

    return self

init_composition pydantic-validator

init_composition()

Initializes composition object

Source code in src/keppy/models.py
@model_validator(mode="after")
def init_composition(self):
    """Initializes composition object"""
    if self.preset is not None:
        self._composition = Composition.load_kepler_file(self.preset)
    elif self.custom is not None:
        self._composition = Composition.from_dict(self.custom)
    else:
        raise ValueError("Something is wrong. Custom and Preset are not specified.")
    return self

GridConfig

GridConfig pydantic-model

Bases: BaseModel

Grid setup overrides to Lane Emden solver

Fields:

  • massunit (float)
  • cutoff (Optional[float])
  • ngrid (Optional[int])
  • rho_c (Optional[float])
  • n (Optional[float])
  • Omega (Optional[float])
Source code in src/keppy/models.py
class GridConfig(BaseModel):
    """Grid setup overrides to Lane Emden solver"""

    # fmt: off
    massunit: float             = Field(1.0,  gt=0, description="Mass unit used on grid",exclude=True)
    cutoff: Optional[float]     = Field(None, gt=0, description="density cutoff")
    ngrid: Optional[int]        = Field(None, gt=0, description="number of initial grid points.")
    rho_c: Optional[float]      = Field(None, gt=0, description="central density")
    n: Optional[float]          = Field(None, gt=0, description="polytrope index")
    Omega: Optional[float]      = None # overriden by ModelConfig rotation

massunit pydantic-field

massunit: float = 1.0

Mass unit used on grid

cutoff pydantic-field

cutoff: Optional[float] = None

density cutoff

ngrid pydantic-field

ngrid: Optional[int] = None

number of initial grid points.

rho_c pydantic-field

rho_c: Optional[float] = None

central density

n pydantic-field

n: Optional[float] = None

polytrope index


ParameterConfig

ParameterConfig pydantic-model

Bases: BaseModel

Controls for Kepler's adjustable runtime parameters.

Example

[parameters]
dtmax: 3e12 # 10^5yrs in seconds
iwinsize: 10240750 # XXXX by YYYY
tnmin: 1e4  # minimum temperature for which rezoning is considered
dnmin: 1e-4 # minimum density for which rezoning is considered

Config:

  • extra: allow

Fields:

  • parameters (dict[str, float | int | str])
Source code in src/keppy/models.py
class ParameterConfig(BaseModel):
    """Controls for Kepler's adjustable runtime parameters.

    Example
    ```
    [parameters]
    dtmax: 3e12 # 10^5yrs in seconds
    iwinsize: 10240750 # XXXX by YYYY
    tnmin: 1e4  # minimum temperature for which rezoning is considered
    dnmin: 1e-4 # minimum density for which rezoning is considered
    ```
    """

    model_config = ConfigDict(extra="allow")

    parameters: dict[str, float | int | str] = Field(default_factory=dict, exclude=True)

    def model_post_init(self, __context):
        """Capture all parameters and verify"""
        extra = self.model_extra or {}
        for field_name, value in extra.items():
            # validate field_name is valid
            if field_name not in PARAMETERS.name.values:
                raise ValueError("Invalid parameter name: '{}'".format(field_name))

            if field_name not in ["parameters"]:
                # store the parameter and its spec
                self.parameters[field_name] = value

    def get_output_parameters(self) -> list[str]:
        """Get list of all parameters"""
        return list(self.parameters.keys())

    def has_parameter(self, parameter: str) -> bool:
        """Check if a parameter is changed"""
        return parameter in self.parameters

model_post_init

model_post_init(__context)

Capture all parameters and verify

Source code in src/keppy/models.py
def model_post_init(self, __context):
    """Capture all parameters and verify"""
    extra = self.model_extra or {}
    for field_name, value in extra.items():
        # validate field_name is valid
        if field_name not in PARAMETERS.name.values:
            raise ValueError("Invalid parameter name: '{}'".format(field_name))

        if field_name not in ["parameters"]:
            # store the parameter and its spec
            self.parameters[field_name] = value

get_output_parameters

get_output_parameters() -> list[str]

Get list of all parameters

Source code in src/keppy/models.py
def get_output_parameters(self) -> list[str]:
    """Get list of all parameters"""
    return list(self.parameters.keys())

has_parameter

has_parameter(parameter: str) -> bool

Check if a parameter is changed

Source code in src/keppy/models.py
def has_parameter(self, parameter: str) -> bool:
    """Check if a parameter is changed"""
    return parameter in self.parameters

PlotConfig

PlotConfig pydantic-model

Bases: BaseModel

Mongo plotting configuration.

Example: ``` [plot] window_width: 1024 window_height: 720 plot1: plot2:

Fields:

Source code in src/keppy/models.py
class PlotConfig(BaseModel):
    """Mongo plotting configuration.


    Example:
    ```
    [plot]
    window_width: 1024
    window_height: 720
    plot1:
    plot2:
    """

    # fmt: off
    window_width: int = Field(1024, ge=0, description="XWindow height")
    window_height: int = Field(720, ge=0,    description="XWindow width")
    interval: int = Field(1, ge=0,    description="Plotting interval in seconds")
    xaxis: int = Field(3, ge=1,le=32, description='xaxis type (irtype).')
    plot1: PlotMode           = Field('abundances', description='Plot mode 1')
    plot2: Optional[PlotMode] = Field(None,         description='Plot mode 2')
    plot3: Optional[PlotMode] = Field(None,         description='Plot mode 3')
    plot4: Optional[PlotMode] = Field(None,         description='Plot mode 4')
    isotopes: Optional[list[str]] = Field(default_factory=lambda: ['h1','he4','c12','o16','mg25'], description='isotopes to plot when using isotope plot')
    # fmt: on

    def get_integer_mapping(self, plot: None | int | str) -> str:
        """Map a plot mode (int code or named alias) to its integer code string."""
        if isinstance(plot, int):
            return str(plot)
        try:
            return str(PLOT_MODES.index(plot))
        except ValueError:
            raise ValueError(f"plottype specified is invalid: {plot!r}")

    @property
    def ipixtype(self) -> str:
        parts = [
            self.get_integer_mapping(p) if p is not None else ""
            for p in (self.plot1, self.plot2, self.plot3, self.plot4)
        ]
        p1, p2, p3, p4 = parts

        # set type and flag if we need to set isotopes
        ipix = f"{p1}{p2}{p3}{p4}"

        if p3 and not p4:
            ipix += "00"  # trailing 0 for 3 stacked plots

        return ipix

    @property
    def isotope_active(self) -> bool:
        return str(PLOT_MODES.index("isotope")) in self.ipixtype

window_width pydantic-field

window_width: int = 1024

XWindow height

window_height pydantic-field

window_height: int = 720

XWindow width

interval pydantic-field

interval: int = 1

Plotting interval in seconds

xaxis pydantic-field

xaxis: int = 3

xaxis type (irtype).

plot1 pydantic-field

plot1: PlotMode = 'abundances'

Plot mode 1

plot2 pydantic-field

plot2: Optional[PlotMode] = None

Plot mode 2

plot3 pydantic-field

plot3: Optional[PlotMode] = None

Plot mode 3

plot4 pydantic-field

plot4: Optional[PlotMode] = None

Plot mode 4

isotopes pydantic-field

isotopes: Optional[list[str]]

isotopes to plot when using isotope plot

get_integer_mapping

get_integer_mapping(plot: None | int | str) -> str

Map a plot mode (int code or named alias) to its integer code string.

Source code in src/keppy/models.py
def get_integer_mapping(self, plot: None | int | str) -> str:
    """Map a plot mode (int code or named alias) to its integer code string."""
    if isinstance(plot, int):
        return str(plot)
    try:
        return str(PLOT_MODES.index(plot))
    except ValueError:
        raise ValueError(f"plottype specified is invalid: {plot!r}")