"""Immutable settings for reliability analyses.
Each analysis takes one frozen settings object through its ``options``
keyword: :class:`FORMOptions` for FORM and the design-point methods built on
it (system FORM, sensitivity analysis, the Strong Maximum Test),
:class:`SORMOptions` for SORM, and :class:`SimulationOptions` for the
simulation methods and active learning. Unknown fields are rejected, values
are validated when the object is created, and the defaults are those of
PySTRA 1.x. Use :func:`dataclasses.replace` to derive modified settings.
A simulation method rejects a :class:`SimulationOptions` setting that it
does not use, rather than silently ignoring it.
"""
import math
from dataclasses import dataclass, field
from numbers import Integral, Real
from .errors import ModelError
__all__ = ["FORMOptions", "SORMOptions", "SimulationOptions"]
TRANSFORMS = (None, "cholesky", "svd", "nataf", "rosenblatt")
def _integer(value, name):
if isinstance(value, bool) or not isinstance(value, Integral) or value < 1:
raise ModelError(f"{name} must be a positive integer")
def _positive(value, name, zero=False):
if (
isinstance(value, bool)
or not isinstance(value, Real)
or not math.isfinite(value)
or value < 0
or (value == 0 and not zero)
):
bound = "non-negative" if zero else "positive"
raise ModelError(f"{name} must be a finite {bound} number")
def _choice(value, name, choices):
if value not in choices:
raise ModelError(f"{name} must be one of {choices}, not {value!r}")
def _dependence(options):
_choice(options.transform, "transform", TRANSFORMS)
order = options.rosenblatt_order
if order is not None:
order = tuple(order)
if not all(isinstance(i, Integral) and not isinstance(i, bool) for i in order):
raise ModelError("rosenblatt_order must be a sequence of variable indices")
object.__setattr__(options, "rosenblatt_order", tuple(int(i) for i in order))
[docs]
@dataclass(frozen=True, kw_only=True)
class SORMOptions:
"""Settings for SORM.
Parameters
----------
fit : {"curve", "point"}, default "curve"
Curve fitting through the Hessian at the design point, or point
fitting on the limit-state surface.
formula : {"breitung", "modified_breitung"}, default "breitung"
The approximation reported as the estimate; both are computed.
ffd_parameter : float, default 1000
Finite-difference step divisor for the Hessian: the step in standard
coordinates is its reciprocal.
form : FORMOptions, default FORMOptions()
Settings of the FORM analysis that SORM runs when none is supplied.
A supplied FORM analysis keeps its own settings.
"""
fit: str = "curve"
formula: str = "breitung"
ffd_parameter: float = 1000
form: FORMOptions = field(default_factory=FORMOptions)
def __post_init__(self):
_choice(self.fit, "fit", ("curve", "point"))
_choice(self.formula, "formula", ("breitung", "modified_breitung"))
_positive(self.ffd_parameter, "ffd_parameter")
if not isinstance(self.form, FORMOptions):
raise ModelError("form must be FORMOptions")
[docs]
@dataclass(frozen=True, kw_only=True)
class SimulationOptions:
"""Settings for the simulation methods and active learning.
Parameters
----------
n_samples : int, default 100000
Sample budget: points for crude Monte Carlo, importance sampling and
distribution analysis, lines for line sampling, and samples per level
for subset simulation.
block_size : int, default 1000
Points passed to the limit-state function per call, and crude Monte
Carlo's convergence-check interval.
target_cov : float, default 0.05
Crude Monte Carlo and importance sampling stop once the estimate's
coefficient of variation reaches this value; 0 uses the whole budget.
sampling_std : float, default 1.0
Standard deviation of the sampling density in standard coordinates.
bins : int, optional
Histogram bins for distribution analysis; chosen from the sample size
if omitted.
transform : {None, "cholesky", "svd", "nataf", "rosenblatt"}, default None
Isoprobabilistic transformation; None selects it from the model.
rosenblatt_order : sequence of int, optional
Conditioning order of the variables for the Rosenblatt
transformation.
"""
n_samples: int = 100_000
block_size: int = 1000
target_cov: float = 0.05
sampling_std: float = 1.0
bins: int | None = None
transform: str | None = None
rosenblatt_order: tuple[int, ...] | None = None
def __post_init__(self):
_integer(self.n_samples, "n_samples")
_integer(self.block_size, "block_size")
_positive(self.target_cov, "target_cov", zero=True)
_positive(self.sampling_std, "sampling_std")
if self.bins is not None:
_integer(self.bins, "bins")
_dependence(self)
def _require_defaults(self, method, unused):
"""Reject settings that *method* does not use."""
defaults = SimulationOptions()
changed = [
name for name in unused if getattr(self, name) != getattr(defaults, name)
]
if changed:
raise ModelError(f"{method} does not use {', '.join(changed)}")