Skip to content

transaction

Transaction, a retryable, all-or-nothing block of correlated random draws for StochasBase.

Transaction dataclass

Python
Transaction(
    base: StochasBase,
    retry_on: type[Exception] | tuple[type[Exception], ...],
    max_retries: int,
)

Iterable of retry attempts returned by StochasBase.transaction().

Construct via Transaction.begin(), which snapshots base.dists, base.design, and base.named. A failed attempt (retryable or not) rolls back to that snapshot before the exception is either suppressed (to retry) or allowed to propagate, so a transaction only ever commits the registrations made by the attempt that succeeds.

begin classmethod

Python
begin(
    base: StochasBase,
    retry_on: type[Exception],
    max_retries: int = 10,
) -> Self
Python
begin(
    base: StochasBase,
    retry_on: tuple[type[Exception], ...],
    max_retries: int = 10,
) -> Self
Python
begin(
    base: StochasBase,
    retry_on: type[Exception] | tuple[type[Exception], ...],
    max_retries: int = 10,
) -> Self

Starts a transaction, snapshotting base.dists, base.design, and base.named for rollback.

Source code in src/stochas/transaction.py
Python
@classmethod
def begin(
    cls,
    base: StochasBase,
    retry_on: type[Exception] | tuple[type[Exception], ...],
    max_retries: int = 10,
) -> Self:
    """Starts a transaction, snapshotting `base.dists`, `base.design`, and `base.named` for rollback."""
    transaction = cls(base=base, retry_on=retry_on, max_retries=max_retries)
    transaction._dists_snapshot = dict(base.dists.root)
    transaction._design_snapshot = dict(base.design.root)
    transaction._named_snapshot = dict(base.named.root)
    return transaction

TransactionAttempt dataclass

Python
TransactionAttempt(transaction: Transaction)

One attempt of a StochasBase.transaction(); use as with attempt:.

base property

Python
base: StochasBase

The StochasBase model this transaction is retrying against: the one .transaction() was called on. Use it for anything not wrapped below, e.g. attempt.base.with_seed(...) or attempt.base.model_dump().

attempt_number property

Python
attempt_number: int

1-indexed number of this attempt.

sample_dist

Python
sample_dist(
    dist: Distribution[T],
    size: int = 1,
    force: bool = False,
    warn: bool = True,
    convert_units: bool = True,
) -> NamedValue[NDArray[Any, T]]

Samples dist via attempt.base (the model this transaction is retrying against), reseeding it only the first time this transaction sees dist.name.

Without this, every attempt would reset the distribution's RNG to the same state and draw the exact same (rejected) value forever. reset_rng is therefore not exposed here; it is derived from whether this transaction has already seeded dist.name, tracked by the actual instance last used (Transaction._seeded, a DistributionDict) rather than the name alone. A caller may pass a brand-new Distribution instance with the same name on every attempt instead of reusing one, and dist.adopt_prior_state transplants the previous instance's seed, trial number, and live _rng stream onto the new one so both patterns draw identical values.

If that carried-over state leaves dist.is_nominal true, every remaining attempt for this name is going to deterministically return the same already-rejected nominal value; this is worth a warning, logged once per name per transaction rather than once per retry.

Source code in src/stochas/transaction.py
Python
def sample_dist[T](
    self,
    dist: Distribution[T],
    size: int = 1,
    force: bool = False,
    warn: bool = True,
    convert_units: bool = True,
) -> NamedValue[NDArray[Any, T]]:
    """
    Samples `dist` via `attempt.base` (the model this transaction is retrying against), reseeding it only the first time this transaction sees `dist.name`.

    Without this, every attempt would reset the distribution's RNG to the same state and draw the exact same (rejected) value forever. `reset_rng` is therefore not exposed here; it is derived from whether this transaction has already seeded `dist.name`, tracked by the actual instance last used (`Transaction._seeded`, a `DistributionDict`) rather than the name alone. A caller may pass a brand-new `Distribution` instance with the same name on every attempt instead of reusing one, and `dist.adopt_prior_state` transplants the previous instance's seed, trial number, and live `_rng` stream onto the new one so both patterns draw identical values.

    If that carried-over state leaves `dist.is_nominal` true, every remaining attempt for this name is going to deterministically return the same already-rejected nominal value; this is worth a warning, logged once per name per transaction rather than once per retry.
    """
    # Distribution is abstract; any instance is necessarily one of AnyDist's
    # concrete members, so this cast just bridges the generic call-site type
    # to the closed discriminated union DistributionDict stores.
    concrete_dist = cast(AnyDist, dist)

    prior = self.transaction._seeded.get(dist.name)
    reset_rng = prior is None
    if prior is not None:
        dist.adopt_prior_state(prior)
    self.transaction._seeded.force_update(concrete_dist, warn=False)

    if (
        not reset_rng
        and dist.is_nominal
        and dist.name not in self.transaction._warned_nominal_names
    ):
        self.transaction._warned_nominal_names.add(dist.name)
        logger.warning(
            f"Distribution {dist.name} is nominal on retry attempt "
            f"{self.attempt_number} of a transaction; every remaining attempt "
            "will keep returning the same nominal value instead of a new draw."
        )

    return self.transaction.base.sample_dist(
        dist,
        size=size,
        force=force,
        warn=warn,
        reset_rng=reset_rng,
        convert_units=convert_units,
    )

sample_design

Python
sample_design(
    dv: DesignFloat,
    force: bool = False,
    warn: bool = True,
    convert_units: bool = True,
) -> float
Python
sample_design(
    dv: DesignInt,
    force: bool = False,
    warn: bool = True,
    convert_units: bool = True,
) -> int
Python
sample_design(
    dv: DesignBool,
    force: bool = False,
    warn: bool = True,
    convert_units: bool = True,
) -> bool
Python
sample_design(
    dv: DesignCategorical[T],
    force: bool = False,
    warn: bool = True,
    convert_units: bool = True,
) -> T
Python
sample_design(
    dv: AnyDesignValue,
    force: bool = False,
    warn: bool = True,
    convert_units: bool = True,
) -> Any

Samples dv via attempt.base (the model this transaction is retrying against).

Design variables aren't RNG-backed (an optimizer, not stochas, picks their value), so there's no reseeding to manage here: this is a plain passthrough to StochasBase.sample_design, kept on TransactionAttempt so a design variable stays fixed for the duration of one attempt and rolls back with everything else on a rejected one, exactly like sample_dist.

Source code in src/stochas/transaction.py
Python
def sample_design(
    self,
    dv: AnyDesignValue,
    force: bool = False,
    warn: bool = True,
    convert_units: bool = True,
) -> Any:
    """
    Samples `dv` via `attempt.base` (the model this transaction is retrying against).

    Design variables aren't RNG-backed (an optimizer, not stochas, picks their value), so there's no reseeding to manage here: this is a plain passthrough to `StochasBase.sample_design`, kept on `TransactionAttempt` so a design variable stays fixed for the duration of one attempt and rolls back with everything else on a rejected one, exactly like `sample_dist`.
    """
    return self.transaction.base.sample_design(
        dv, force=force, warn=warn, convert_units=convert_units
    )