Skip to content

Configuration

config.yaml is turned into these dataclasses by TrainingConfig.from_dict and ModelConfig.from_architecture. The same path is used by Config() and pinnrl-train.

Config

Config(config_path=None)

Configuration for the PINNs-RL-PDE framework.

Loads device, model, training and rl from config.yaml. The model block is taken from the architecture configured for pde_type (falling back to the top-level model: block).

Source code in pinnrl/config/__init__.py
def __init__(self, config_path=None):
    if config_path is None:
        config_path = DEFAULT_CONFIG_PATH
    self.config_path = config_path
    self.model = None
    self.training = None
    self.rl = None
    self.device = torch.device("cpu")  # Default to CPU, will be overridden by config

    # Load configuration if file exists
    if os.path.exists(config_path):
        self._load_config()
        self._validate_config()

get

get(key, default=None)

Get the value for a given key or return the default value if the key doesn't exist.

Source code in pinnrl/config/__init__.py
def get(self, key: str, default: Any = None) -> Any:
    """Get the value for a given key or return the default value if the key doesn't exist."""
    return getattr(self, key, default)

__getitem__

__getitem__(key)

Get the value for a given key, also to make it subscriptable.

Source code in pinnrl/config/__init__.py
def __getitem__(self, key: str) -> Any:
    """Get the value for a given key, also to make it subscriptable."""
    return getattr(self, key)

TrainingConfig dataclass

TrainingConfig(
    num_epochs,
    batch_size,
    num_collocation_points,
    num_boundary_points,
    num_initial_points,
    learning_rate,
    weight_decay,
    gradient_clipping,
    early_stopping,
    learning_rate_scheduler,
    collocation_distribution="uniform",
    adaptive_weights=None,
    loss_weights=None,
    optimizer="adam",
    adam_lbfgs_switch_ratio=0.7,
    lbfgs=None,
    mode="forward",
    loss_function="mse",
    huber_delta=1.0,
    validation_frequency=10,
)

Training hyper-parameters and sub-configurations.

Attributes:

Name Type Description
num_epochs int

Total training epochs.

batch_size int

Mini-batch size.

num_collocation_points int

Number of interior collocation points.

num_boundary_points int

Number of boundary condition points.

num_initial_points int

Number of initial condition points.

learning_rate float

Optimizer learning rate.

weight_decay float

L2 regularisation coefficient.

gradient_clipping float

Max gradient norm for clipping (0 disables).

early_stopping EarlyStoppingConfig

Early-stopping configuration.

learning_rate_scheduler LearningRateSchedulerConfig

LR scheduler configuration.

collocation_distribution str

Collocation sampling strategy ("uniform", "stratified", "residual_based" or "adaptive").

adaptive_weights AdaptiveWeightsConfig

Adaptive loss weighting configuration.

loss_weights dict[str, float]

Static weights for residual, boundary, initial and data.

optimizer str

"adam", "lbfgs" or "adam_lbfgs".

adam_lbfgs_switch_ratio float

Fraction of num_epochs run with Adam before adam_lbfgs switches to L-BFGS.

lbfgs LBFGSConfig | None

L-BFGS settings.

mode str

"forward", "inverse", "data_only" or "data_augmented".

loss_function str

Reduction of each loss term ("mse", "mae" or "huber").

huber_delta float

Delta of the Huber loss.

validation_frequency int

Epochs between validation, logging and run checkpoints.

optimizer_config property

optimizer_config

Learning rate and weight decay, shaped like the optimizer_config YAML block.

__getitem__

__getitem__(key)

Get the value for a given key, also to make it subscriptible.

Source code in pinnrl/config/__init__.py
def __getitem__(self, key: str) -> Any:
    """Get the value for a given key, also to make it subscriptible."""
    if key == "optimizer_config":
        return self.optimizer_config
    return getattr(self, key)

get

get(key, default=None)

Get the value for a given key or return the default value if the key doesn't exist.

Source code in pinnrl/config/__init__.py
def get(self, key: str, default: Any = None) -> Any:
    """Get the value for a given key or return the default value if the key doesn't exist."""
    if key == "optimizer_config":
        return self.optimizer_config
    return getattr(self, key, default)

from_dict classmethod

from_dict(cfg)

Build from the training: block of config.yaml.

This is the one place the YAML layout is translated into a TrainingConfig; both :class:Config and pinnrl-train use it. learning_rate and weight_decay come from optimizer_config (or the top level); the scheduler from scheduler_type, warmup_epochs and reduce_lr_params; a legacy pde key in loss_weights is renamed to residual. Missing keys fall back to defaults. Float fields are coerced with float() because YAML 1.1 reads 1e-7 (no decimal point) as a string.

Source code in pinnrl/config/__init__.py
@classmethod
def from_dict(cls, cfg: dict[str, Any]) -> "TrainingConfig":
    """Build from the ``training:`` block of ``config.yaml``.

    This is the one place the YAML layout is translated into a
    ``TrainingConfig``; both :class:`Config` and ``pinnrl-train`` use it.
    ``learning_rate`` and ``weight_decay`` come from ``optimizer_config``
    (or the top level); the scheduler from ``scheduler_type``,
    ``warmup_epochs`` and ``reduce_lr_params``; a legacy ``pde`` key in
    ``loss_weights`` is renamed to ``residual``. Missing keys fall back to
    defaults. Float fields are coerced with ``float()`` because YAML 1.1
    reads ``1e-7`` (no decimal point) as a string.
    """
    optimizer_cfg = cfg.get("optimizer_config", {})
    early_stopping = cfg.get("early_stopping", {})
    reduce_lr = cfg.get("reduce_lr_params", {})
    adaptive = cfg.get("adaptive_weights", {})
    lbfgs = cfg.get("lbfgs", {})

    loss_weights = cfg.get("loss_weights")
    if loss_weights is not None and "pde" in loss_weights:
        loss_weights = dict(loss_weights)
        loss_weights["residual"] = loss_weights.pop("pde")

    return cls(
        num_epochs=cfg.get("num_epochs", 10000),
        batch_size=cfg.get("batch_size", 128),
        num_collocation_points=cfg.get("num_collocation_points", 1000),
        num_boundary_points=cfg.get("num_boundary_points", 100),
        num_initial_points=cfg.get("num_initial_points", 100),
        learning_rate=float(
            optimizer_cfg.get("learning_rate", cfg.get("learning_rate", 0.001))
        ),
        weight_decay=float(optimizer_cfg.get("weight_decay", cfg.get("weight_decay", 0.0001))),
        gradient_clipping=float(cfg.get("gradient_clipping", 1.0)),
        early_stopping=EarlyStoppingConfig(
            enabled=early_stopping.get("enabled", True),
            patience=early_stopping.get("patience", 100),
            min_delta=float(early_stopping.get("min_delta", 1e-4)),
        ),
        learning_rate_scheduler=LearningRateSchedulerConfig(
            type=cfg.get("scheduler_type", "cosine"),
            warmup_epochs=cfg.get("warmup_epochs", 0),
            min_lr=float(reduce_lr.get("min_lr", 1e-6)),
            factor=float(reduce_lr.get("factor", 0.5)),
            patience=reduce_lr.get("patience", 50),
        ),
        collocation_distribution=cfg.get("collocation_distribution", "uniform"),
        adaptive_weights=AdaptiveWeightsConfig(
            enabled=adaptive.get("enabled", False),
            strategy=adaptive.get("strategy", "rbw"),
            alpha=float(adaptive.get("alpha", 0.9)),
            eps=float(adaptive.get("eps", 1e-5)),
            initial_weights=adaptive.get("initial_weights"),
        ),
        loss_weights=loss_weights,
        optimizer=cfg.get("optimizer", "adam"),
        adam_lbfgs_switch_ratio=float(cfg.get("adam_lbfgs_switch_ratio", 0.7)),
        lbfgs=LBFGSConfig(
            **{k: _lbfgs_value(k, v) for k, v in lbfgs.items() if k in _LBFGS_FIELDS}
        ),
        mode=cfg.get("mode", "forward"),
        loss_function=cfg.get("loss_function", "mse"),
        huber_delta=float(cfg.get("huber_delta", 1.0)),
        validation_frequency=int(cfg.get("validation_frequency", 10)),
    )

ModelConfig dataclass

ModelConfig(
    input_dim,
    hidden_dim,
    output_dim,
    num_layers,
    activation,
    fourier_features=0,
    fourier_scale=1.0,
    dropout=0.0,
    layer_norm=False,
    architecture="feedforward",
)

Configuration for neural network models.

Attributes:

Name Type Description
input_dim int

Number of input features (e.g. 2 for (x, t)).

hidden_dim int

Width of hidden layers.

output_dim int

Number of output features.

num_layers int

Depth: linear layers after the feature map for Fourier (the last is the output layer), attention blocks, and the block count for ResNet / FNO when num_blocks is unset.

activation str

Activation function name.

fourier_features bool

True for the Fourier architecture (informational; no network reads it).

fourier_scale float | None

Copy of scale (informational; no network reads it).

dropout float

Dropout rate (0.0 disables).

layer_norm bool

Whether to apply layer normalisation.

architecture str

Architecture backend name.

hidden_dims list[int] | None

Hidden-layer widths for feedforward, SIREN and the autoencoder encoder; defaults to [hidden_dim] * num_layers.

omega_0 float | None

Frequency factor for SIREN networks.

num_blocks int | None

Number of residual / FNO blocks.

num_heads int | None

Number of attention heads.

latent_dim int | None

Latent dimension for the AutoEncoder.

mapping_size int

Size of the Fourier feature mapping.

scale float

Scale for Fourier feature mapping.

modes int | None

Number of Fourier modes for FNO.

Source code in pinnrl/config/__init__.py
def __init__(
    self,
    input_dim: int,
    hidden_dim: int,
    output_dim: int,
    num_layers: int,
    activation: str,
    fourier_features: int = 0,
    fourier_scale: float = 1.0,
    dropout: float = 0.0,
    layer_norm: bool = False,
    architecture: str = "feedforward",
):
    self.input_dim = input_dim
    self.hidden_dim = hidden_dim
    self.output_dim = output_dim
    self.num_layers = num_layers
    self.activation = activation
    self.fourier_features = fourier_features
    self.fourier_scale = fourier_scale
    self.dropout = dropout
    self.layer_norm = layer_norm
    self.architecture = architecture

    # Configure hidden_dims as a list based on hidden_dim when not explicitly set
    self.hidden_dims = [hidden_dim] * num_layers

    # For ResNet and FNO, set num_blocks to be the same as num_layers
    if architecture in ("resnet", "fno"):
        self.num_blocks = num_layers

get

get(key, default=None)

Return the attribute key, or default when it is missing or None.

Optional fields (omega_0, num_heads, latent_dim, modes) default to None; treating None as unset lets each network apply its own default.

Source code in pinnrl/config/__init__.py
def get(self, key: str, default: Any = None) -> Any:
    """Return the attribute ``key``, or ``default`` when it is missing or ``None``.

    Optional fields (``omega_0``, ``num_heads``, ``latent_dim``, ``modes``) default
    to ``None``; treating ``None`` as unset lets each network apply its own default.
    """
    value = getattr(self, key, None)
    return default if value is None else value

__getitem__

__getitem__(key)

Get the value for a given key, also to make it subscriptable.

Source code in pinnrl/config/__init__.py
def __getitem__(self, key: str) -> Any:
    """Get the value for a given key, also to make it subscriptable."""
    return getattr(self, key)

from_architecture classmethod

from_architecture(
    architecture, arch_cfg, input_dim, output_dim
)

Build from one entry of the architectures: block of config.yaml.

hidden_dim and num_layers default to the first width and the length of hidden_dims when only the list is given (else 128 and 4); activation defaults to tanh. Architecture extras (omega_0, modes, ...) are copied when present.

Source code in pinnrl/config/__init__.py
@classmethod
def from_architecture(
    cls,
    architecture: str,
    arch_cfg: dict[str, Any],
    input_dim: int,
    output_dim: int,
) -> "ModelConfig":
    """Build from one entry of the ``architectures:`` block of ``config.yaml``.

    ``hidden_dim`` and ``num_layers`` default to the first width and the
    length of ``hidden_dims`` when only the list is given (else 128 and 4);
    ``activation`` defaults to ``tanh``. Architecture extras
    (``omega_0``, ``modes``, ...) are copied when present.
    """
    hidden_dims = arch_cfg.get("hidden_dims")
    model = cls(
        input_dim=input_dim,
        hidden_dim=arch_cfg.get("hidden_dim", hidden_dims[0] if hidden_dims else 128),
        output_dim=output_dim,
        num_layers=arch_cfg.get("num_layers", len(hidden_dims) if hidden_dims else 4),
        activation=arch_cfg.get("activation", "tanh"),
        fourier_features=architecture == "fourier",
        fourier_scale=arch_cfg.get("scale", 1.0),
        dropout=arch_cfg.get("dropout", 0.0),
        layer_norm=arch_cfg.get("layer_norm", False),
        architecture=architecture,
    )
    for key in _ARCHITECTURE_EXTRAS:
        if key in arch_cfg:
            setattr(model, key, arch_cfg[key])
    return model

LearningRateSchedulerConfig dataclass

LearningRateSchedulerConfig(
    type, warmup_epochs, min_lr, factor, patience
)

Learning rate scheduler settings.

Attributes:

Name Type Description
type str

Scheduler type ("cosine", "reduce_lr" or "constant").

warmup_epochs int

Epochs of linear LR warm-up before the scheduler starts; cosine then anneals over num_epochs - warmup_epochs.

min_lr float

Minimum learning rate (cosine eta_min and reduce_lr floor).

factor float

Multiplicative factor for reduce_lr scheduler.

patience int

Epochs without improvement before reduce_lr lowers the LR.

EarlyStoppingConfig dataclass

EarlyStoppingConfig(enabled, patience, min_delta)

Early-stopping criteria for training.

Attributes:

Name Type Description
enabled bool

Whether early stopping is active.

patience int

Epochs without validation-loss improvement before stopping (checked every validation_frequency epochs).

min_delta float

Minimum validation-loss decrease to count as improvement.

LBFGSConfig dataclass

LBFGSConfig(
    history_size=50,
    max_iter=20,
    line_search_fn="strong_wolfe",
    tolerance_grad=1e-07,
    tolerance_change=1e-09,
)

L-BFGS optimizer settings.

Attributes:

Name Type Description
history_size int

Number of past gradients/updates retained for the inverse-Hessian approximation.

max_iter int

Max L-BFGS iterations per step() call.

line_search_fn str | None

Line-search strategy passed to torch.optim.LBFGS ("strong_wolfe" or None).

tolerance_grad float

First-order optimality tolerance.

tolerance_change float

Loss-change tolerance for early termination.

AdaptiveWeightsConfig dataclass

AdaptiveWeightsConfig(
    enabled=False,
    strategy="rbw",
    alpha=0.9,
    eps=1e-05,
    initial_weights=None,
)

Configuration for adaptive loss weighting.

Attributes:

Name Type Description
enabled bool

Whether adaptive weighting is active.

strategy str

Weight adaptation strategy ("lrw" or "rbw").

alpha float

EMA retention factor (0–1); higher adapts more slowly.

eps float

Small constant for numerical stability.

initial_weights list[float]

Weights for [residual, boundary, initial] returned by the first update.

RLConfig dataclass

RLConfig(
    enabled=False,
    n_x_cells=None,
    n_t_cells=None,
    policy_fraction=None,
    temperature=None,
    probe_cells=None,
    probe_points=None,
    probe_every=None,
)

The rl: block: the experimental residual-bandit sampler (--rl).

Sampler fields left as None fall back to the defaults of :class:~pinnrl.rl.bandit_sampler.ResidualBanditSampler, so those defaults live in one place.

from_dict classmethod

from_dict(cfg)

Build from the rl: block of config.yaml; unknown keys are ignored.

Source code in pinnrl/config/__init__.py
@classmethod
def from_dict(cls, cfg: dict[str, Any]) -> "RLConfig":
    """Build from the ``rl:`` block of ``config.yaml``; unknown keys are ignored."""
    return cls(**{k: v for k, v in cfg.items() if k in cls.__dataclass_fields__})

build_sampler

build_sampler(pde, device=None)

Create the :class:ResidualBanditSampler these settings describe for pde.

Source code in pinnrl/config/__init__.py
def build_sampler(self, pde, device: torch.device | None = None):
    """Create the :class:`ResidualBanditSampler` these settings describe for ``pde``."""
    from pinnrl.rl.bandit_sampler import ResidualBanditSampler

    overrides = {
        name: getattr(self, name)
        for name in self.__dataclass_fields__
        if name != "enabled" and getattr(self, name) is not None
    }
    return ResidualBanditSampler(
        tuple(pde.domain[0]),
        tuple(pde.time_domain),
        device=device or pde.device,
        **overrides,
    )

resolve_device

resolve_device(name)

Return torch.device(name) if that backend is available, else CPU.

Only "cuda" and "mps" are probed; any other name resolves to CPU, so a config written on one machine still runs on another.

Source code in pinnrl/config/__init__.py
def resolve_device(name: str) -> torch.device:
    """Return ``torch.device(name)`` if that backend is available, else CPU.

    Only ``"cuda"`` and ``"mps"`` are probed; any other name resolves to CPU,
    so a config written on one machine still runs on another.
    """
    if name == "cuda" and torch.cuda.is_available():
        return torch.device("cuda")
    if name == "mps" and torch.backends.mps.is_available():
        return torch.device("mps")
    return torch.device("cpu")