Skip to content

Validation

Every generator validates its arguments before drawing anything, so an invalid call fails immediately rather than returning quietly wrong data.

All errors derive from ValidationError, which derives from ValueError — so except ValueError catches them all, and except ValidationError catches only this package's.

from gen_surv import generate
from gen_surv.validation import ValidationError

try:
    generate(model="cphm", n=-1, beta=0.5, covariate_range=2.0,
             model_cens="uniform", cens_par=1.0)
except ValidationError as exc:
    print(exc)

validation

Input validation utilities.

This module unifies the low-level validation helpers and the higher-level checks used by the data generators.

ValidationError

Bases: ValueError

Base class for input validation errors.

PositiveIntegerError

PositiveIntegerError(name: str, value: Any)

Bases: ValidationError

Raised when a value expected to be a positive integer is invalid.

Source code in gen_surv/validation.py
def __init__(self, name: str, value: Any) -> None:
    super().__init__(
        f"Argument '{name}' must be a positive integer; got {value!r} of type {type(value).__name__}. "
        "Please provide a whole number greater than 0."
    )

PositiveValueError

PositiveValueError(name: str, value: Any)

Bases: ValidationError

Raised when a value expected to be positive is invalid.

Source code in gen_surv/validation.py
def __init__(self, name: str, value: Any) -> None:
    super().__init__(
        f"Argument '{name}' must be greater than 0; got {value!r} of type {type(value).__name__}. "
        "Try a positive number such as 1.0."
    )

ChoiceError

ChoiceError(name: str, value: Any, choices: Iterable[str])

Bases: ValidationError

Raised when a value is not among an allowed set of choices.

Source code in gen_surv/validation.py
def __init__(self, name: str, value: Any, choices: Iterable[str]) -> None:
    choices_str = "', '".join(sorted(choices))
    super().__init__(
        f"Argument '{name}' must be one of '{choices_str}'; got {value!r} of type {type(value).__name__}. "
        "Choose a valid option."
    )

LengthError

LengthError(name: str, actual: int, expected: int)

Bases: ValidationError

Raised when a sequence does not have the expected length.

Source code in gen_surv/validation.py
def __init__(self, name: str, actual: int, expected: int) -> None:
    super().__init__(
        f"Argument '{name}' must be a sequence of length {expected}; got length {actual}. "
        "Adjust the number of elements."
    )

NumericSequenceError

NumericSequenceError(
    name: str, value: Any, index: int | None = None
)

Bases: ValidationError

Raised when a sequence contains non-numeric elements.

Source code in gen_surv/validation.py
def __init__(self, name: str, value: Any, index: int | None = None) -> None:
    if index is None:
        super().__init__(
            f"All elements in '{name}' must be numeric; got {value!r}. "
            "Convert or remove non-numeric values."
        )
    else:
        super().__init__(
            f"All elements in '{name}' must be numeric; found {value!r} of type {type(value).__name__} at index {index}. "
            "Replace or remove this entry."
        )

PositiveSequenceError

PositiveSequenceError(name: str, value: Any, index: int)

Bases: ValidationError

Raised when a sequence contains non-positive elements.

Source code in gen_surv/validation.py
def __init__(self, name: str, value: Any, index: int) -> None:
    super().__init__(
        f"All elements in '{name}' must be greater than 0; found {value!r} at index {index}. "
        "Use positive numbers only."
    )

ListOfListsError

ListOfListsError(name: str, value: Any)

Bases: ValidationError

Raised when a value is not a list of lists.

Source code in gen_surv/validation.py
def __init__(self, name: str, value: Any) -> None:
    super().__init__(
        f"Argument '{name}' must be a list of lists; got {value!r} of type {type(value).__name__}. "
        "Wrap items in a list."
    )

ParameterError

ParameterError(name: str, value: Any, constraint: str)

Bases: ValidationError

Raised when a parameter falls outside its allowed range.

Source code in gen_surv/validation.py
def __init__(self, name: str, value: Any, constraint: str) -> None:
    super().__init__(
        f"Invalid value for '{name}': {value!r} (type {type(value).__name__}). {constraint}. "
        "Check and adjust this parameter."
    )

ensure_positive_int

ensure_positive_int(value: int, name: str) -> None

Ensure value is a positive integer.

Source code in gen_surv/validation.py
def ensure_positive_int(value: int, name: str) -> None:
    """Ensure ``value`` is a positive integer."""
    if not isinstance(value, Integral) or isinstance(value, bool) or value <= 0:
        raise PositiveIntegerError(name, value)

ensure_finite

ensure_finite(value: float | int, name: str) -> None

Ensure value is a real number that is neither NaN nor infinite.

Every comparison with NaN is false, so a check written as value <= 0 silently admits it, and inf > 0 is true. Both then reach NumPy, where they either surface as an unrelated error -- OverflowError: high - low range exceeds valid bounds from a uniform draw -- or produce a frame quietly full of NaN. Rejecting them here is what makes the message name the argument the caller got wrong.

Source code in gen_surv/validation.py
def ensure_finite(value: float | int, name: str) -> None:
    """Ensure ``value`` is a real number that is neither NaN nor infinite.

    Every comparison with NaN is false, so a check written as ``value <= 0``
    silently admits it, and ``inf > 0`` is true. Both then reach NumPy, where
    they either surface as an unrelated error -- ``OverflowError: high - low
    range exceeds valid bounds`` from a uniform draw -- or produce a frame
    quietly full of NaN. Rejecting them here is what makes the message name the
    argument the caller got wrong.
    """
    if not isinstance(value, Real) or isinstance(value, bool):
        raise ParameterError(name, value, "must be a number")
    if not math.isfinite(float(value)):
        raise ParameterError(name, value, "must be a finite number")

ensure_positive

ensure_positive(value: float | int, name: str) -> None

Ensure value is a finite positive number.

Source code in gen_surv/validation.py
def ensure_positive(value: float | int, name: str) -> None:
    """Ensure ``value`` is a finite positive number."""
    if not isinstance(value, Real) or isinstance(value, bool):
        raise PositiveValueError(name, value)
    if not math.isfinite(float(value)):
        # A separate message: NaN and infinity are numbers, and saying so is
        # more use than "must be greater than 0" for a value no comparison
        # would have caught.
        raise ParameterError(name, value, "must be a finite number")
    if value <= 0:
        raise PositiveValueError(name, value)

ensure_probability

ensure_probability(value: float | int, name: str) -> None

Ensure value lies in the closed interval [0, 1].

Source code in gen_surv/validation.py
def ensure_probability(value: float | int, name: str) -> None:
    """Ensure ``value`` lies in the closed interval [0, 1]."""
    ensure_finite(value, name)
    if not (0 <= float(value) <= 1):
        raise ParameterError(name, value, "must be between 0 and 1")

ensure_in_choices

ensure_in_choices(
    value: str, name: str, choices: Iterable[str]
) -> None

Ensure value is one of the allowed options.

Parameters:

Name Type Description Default
value str

Value provided by the user.

required
name str

Name of the argument being validated. Used in error messages.

required
choices Iterable[str]

Iterable of valid string options.

required

Raises:

Type Description
ChoiceError

If value is not present in choices.

Source code in gen_surv/validation.py
def ensure_in_choices(value: str, name: str, choices: Iterable[str]) -> None:
    """Ensure ``value`` is one of the allowed options.

    Parameters
    ----------
    value:
        Value provided by the user.
    name:
        Name of the argument being validated. Used in error messages.
    choices:
        Iterable of valid string options.

    Raises
    ------
    ChoiceError
        If ``value`` is not present in ``choices``.
    """
    if value not in choices:
        raise ChoiceError(name, value, choices)

ensure_sequence_length

ensure_sequence_length(
    seq: Sequence[Any], length: int, name: str
) -> None

Ensure a sequence has an expected number of elements.

Parameters:

Name Type Description Default
seq Sequence[Any]

Sequence-like object (e.g., list or tuple).

required
length int

Required number of elements in seq.

required
name str

Parameter name for error reporting.

required

Raises:

Type Description
LengthError

If seq does not contain exactly length elements.

Source code in gen_surv/validation.py
def ensure_sequence_length(seq: Sequence[Any], length: int, name: str) -> None:
    """Ensure a sequence has an expected number of elements.

    Parameters
    ----------
    seq:
        Sequence-like object (e.g., ``list`` or ``tuple``).
    length:
        Required number of elements in ``seq``.
    name:
        Parameter name for error reporting.

    Raises
    ------
    LengthError
        If ``seq`` does not contain exactly ``length`` elements.
    """
    if len(seq) != length:
        raise LengthError(name, len(seq), length)

ensure_numeric_sequence

ensure_numeric_sequence(
    seq: Sequence[Any], name: str
) -> None

Validate that a sequence consists solely of numbers.

Parameters:

Name Type Description Default
seq Sequence[Any]

Sequence whose elements should all be int or float.

required
name str

Parameter name for error reporting.

required

Raises:

Type Description
NumericSequenceError

If any element cannot be interpreted as a numeric value.

Source code in gen_surv/validation.py
def ensure_numeric_sequence(seq: Sequence[Any], name: str) -> None:
    """Validate that a sequence consists solely of numbers.

    Parameters
    ----------
    seq:
        Sequence whose elements should all be ``int`` or ``float``.
    name:
        Parameter name for error reporting.

    Raises
    ------
    NumericSequenceError
        If any element cannot be interpreted as a numeric value.
    """
    arr = _to_float_array(seq, name)
    bad = np.where(~np.isfinite(arr))[0]
    if bad.size:
        idx = int(bad[0])
        raise ParameterError(f"{name}[{idx}]", seq[idx], "must be a finite number")

ensure_positive_sequence

ensure_positive_sequence(
    seq: Sequence[float], name: str
) -> None

Validate that a sequence contains only positive numbers.

Parameters:

Name Type Description Default
seq Sequence[float]

Sequence of numeric values.

required
name str

Parameter name for error reporting.

required

Raises:

Type Description
PositiveSequenceError

If any element is less than or equal to zero. The offending value and its index are reported in the error message.

Source code in gen_surv/validation.py
def ensure_positive_sequence(seq: Sequence[float], name: str) -> None:
    """Validate that a sequence contains only positive numbers.

    Parameters
    ----------
    seq:
        Sequence of numeric values.
    name:
        Parameter name for error reporting.

    Raises
    ------
    PositiveSequenceError
        If any element is less than or equal to zero. The offending value and
        its index are reported in the error message.
    """
    arr = _to_float_array(seq, name)
    nonpos = np.where((arr <= 0) | ~np.isfinite(arr))[0]
    if nonpos.size:
        idx = int(nonpos[0])
        raise PositiveSequenceError(name, seq[idx], idx)

ensure_censoring_model

ensure_censoring_model(model_cens: str) -> None

Validate that the censoring model is supported.

Parameters:

Name Type Description Default
model_cens str

Censoring model name provided by the user.

required

Raises:

Type Description
ChoiceError

If model_cens is not one of "uniform" or "exponential".

Source code in gen_surv/validation.py
def ensure_censoring_model(model_cens: str) -> None:
    """Validate that the censoring model is supported.

    Parameters
    ----------
    model_cens:
        Censoring model name provided by the user.

    Raises
    ------
    ChoiceError
        If ``model_cens`` is not one of ``"uniform"`` or ``"exponential"``.
    """
    ensure_in_choices(model_cens, "model_cens", _ALLOWED_CENSORING)

validate_gen_cphm_inputs

validate_gen_cphm_inputs(
    n: int,
    model_cens: str,
    cens_par: float,
    covariate_range: float,
    beta: float | None = None,
) -> None

Validate input parameters for CPHM data generation.

beta is a log hazard ratio and may be any sign, so no positivity check reaches it. It still has to be a finite number: NaN propagates into every drawn time, and the frame comes back the right shape and entirely NaN.

Source code in gen_surv/validation.py
def validate_gen_cphm_inputs(
    n: int,
    model_cens: str,
    cens_par: float,
    covariate_range: float,
    beta: float | None = None,
) -> None:
    """Validate input parameters for CPHM data generation.

    ``beta`` is a log hazard ratio and may be any sign, so no positivity check
    reaches it. It still has to be a finite number: NaN propagates into every
    drawn time, and the frame comes back the right shape and entirely NaN.
    """
    _validate_base(n, model_cens, cens_par)
    ensure_positive(covariate_range, "covariate_range")
    if beta is not None:
        ensure_finite(beta, "beta")

validate_gen_cmm_inputs

validate_gen_cmm_inputs(
    n: int,
    model_cens: str,
    cens_par: float,
    beta: Sequence[float],
    covariate_range: float,
    rate: Sequence[float],
) -> None

Validate inputs for generating CMM (Continuous-Time Markov Model) data.

Source code in gen_surv/validation.py
def validate_gen_cmm_inputs(
    n: int,
    model_cens: str,
    cens_par: float,
    beta: Sequence[float],
    covariate_range: float,
    rate: Sequence[float],
) -> None:
    """Validate inputs for generating CMM (Continuous-Time Markov Model) data."""
    _validate_base(n, model_cens, cens_par)
    _validate_beta(beta)
    ensure_positive(covariate_range, "covariate_range")
    ensure_sequence_length(rate, _CMM_RATE_LEN, "rate")
    # Only the length was checked. A negative entry reached NumPy and surfaced
    # as "ValueError: scale < 0" from inside the generator; NaN passed straight
    # through into every drawn time.
    ensure_positive_sequence(rate, "rate")

validate_gen_tdcm_inputs

validate_gen_tdcm_inputs(
    n: int,
    dist: str,
    corr: float,
    dist_par: Sequence[float],
    model_cens: str,
    cens_par: float,
    beta: Sequence[float],
    lam: float,
) -> None

Validate inputs for generating TDCM (Time-Dependent Covariate Model) data.

Source code in gen_surv/validation.py
def validate_gen_tdcm_inputs(
    n: int,
    dist: str,
    corr: float,
    dist_par: Sequence[float],
    model_cens: str,
    cens_par: float,
    beta: Sequence[float],
    lam: float,
) -> None:
    """Validate inputs for generating TDCM (Time-Dependent Covariate Model) data."""
    _validate_base(n, model_cens, cens_par)
    ensure_in_choices(dist, "dist", {"weibull", "exponential"})

    # The endpoints are excluded because the Gaussian copula underneath cannot
    # take them: its covariance is [[1, corr], [corr, 1]], singular at
    # |corr| = 1. This validator used to allow them and the call failed later
    # inside `validate_dg_biv_inputs`, reporting a different range than the one
    # checked here.
    if dist == "weibull":
        if not (0 < corr < 1):
            raise ParameterError("corr", corr, "with dist='weibull' must be in (0,1)")
        ensure_sequence_length(dist_par, _WEIBULL_DIST_PAR_LEN, "dist_par")
        ensure_positive_sequence(dist_par, "dist_par")

    if dist == "exponential":
        if not (-1 < corr < 1):
            raise ParameterError(
                "corr", corr, "with dist='exponential' must be in (-1,1)"
            )
        ensure_sequence_length(dist_par, _EXP_DIST_PAR_LEN, "dist_par")
        ensure_positive_sequence(dist_par, "dist_par")

    _validate_tdcm_beta(beta)
    ensure_positive(lam, "lambda")

validate_gen_thmm_inputs

validate_gen_thmm_inputs(
    n: int,
    model_cens: str,
    cens_par: float,
    beta: Sequence[float],
    covariate_range: float,
    rate: Sequence[float],
) -> None

Validate inputs for generating THMM (Time-Homogeneous Markov Model) data.

Source code in gen_surv/validation.py
def validate_gen_thmm_inputs(
    n: int,
    model_cens: str,
    cens_par: float,
    beta: Sequence[float],
    covariate_range: float,
    rate: Sequence[float],
) -> None:
    """Validate inputs for generating THMM (Time-Homogeneous Markov Model) data."""
    _validate_base(n, model_cens, cens_par)
    _validate_beta(beta)
    ensure_positive(covariate_range, "covariate_range")
    ensure_sequence_length(rate, _THMM_RATE_LEN, "rate")
    ensure_positive_sequence(rate, "rate")

validate_dg_biv_inputs

validate_dg_biv_inputs(
    n: int,
    dist: str,
    corr: float,
    dist_par: Sequence[float],
) -> None

Validate inputs for the :func:sample_bivariate_distribution helper.

Source code in gen_surv/validation.py
def validate_dg_biv_inputs(
    n: int, dist: str, corr: float, dist_par: Sequence[float]
) -> None:
    """Validate inputs for the :func:`sample_bivariate_distribution` helper."""
    ensure_positive_int(n, "n")
    ensure_in_choices(dist, "dist", {"weibull", "exponential"})

    if not isinstance(corr, (int, float)) or not (-1 < corr < 1):
        raise ParameterError("corr", corr, "must be a numeric value between -1 and 1")

    ensure_positive_sequence(dist_par, "dist_par")
    if dist == "exponential":
        ensure_sequence_length(dist_par, _EXP_DIST_PAR_LEN, "dist_par")
    if dist == "weibull":
        ensure_sequence_length(dist_par, _WEIBULL_DIST_PAR_LEN, "dist_par")

validate_gen_aft_log_normal_inputs

validate_gen_aft_log_normal_inputs(
    n: int,
    beta: Sequence[float],
    sigma: float,
    model_cens: str,
    cens_par: float,
) -> None

Validate parameters for the log-normal AFT generator.

Source code in gen_surv/validation.py
def validate_gen_aft_log_normal_inputs(
    n: int,
    beta: Sequence[float],
    sigma: float,
    model_cens: str,
    cens_par: float,
) -> None:
    """Validate parameters for the log-normal AFT generator."""
    _validate_aft_common(n, beta, model_cens, cens_par)
    ensure_positive(sigma, "sigma")

validate_gen_aft_weibull_inputs

validate_gen_aft_weibull_inputs(
    n: int,
    beta: Sequence[float],
    shape: float,
    scale: float,
    model_cens: str,
    cens_par: float,
) -> None

Validate parameters for the Weibull AFT generator.

Source code in gen_surv/validation.py
def validate_gen_aft_weibull_inputs(
    n: int,
    beta: Sequence[float],
    shape: float,
    scale: float,
    model_cens: str,
    cens_par: float,
) -> None:
    """Validate parameters for the Weibull AFT generator."""
    _validate_aft_common(n, beta, model_cens, cens_par)
    ensure_positive(shape, "shape")
    ensure_positive(scale, "scale")

validate_gen_aft_log_logistic_inputs

validate_gen_aft_log_logistic_inputs(
    n: int,
    beta: Sequence[float],
    shape: float,
    scale: float,
    model_cens: str,
    cens_par: float,
) -> None

Validate parameters for the log-logistic AFT generator.

Source code in gen_surv/validation.py
def validate_gen_aft_log_logistic_inputs(
    n: int,
    beta: Sequence[float],
    shape: float,
    scale: float,
    model_cens: str,
    cens_par: float,
) -> None:
    """Validate parameters for the log-logistic AFT generator."""
    _validate_aft_common(n, beta, model_cens, cens_par)
    ensure_positive(shape, "shape")
    ensure_positive(scale, "scale")

validate_competing_risks_inputs

validate_competing_risks_inputs(
    n: int,
    n_risks: int,
    baseline_hazards: Sequence[float] | None,
    betas: Sequence[Sequence[float]] | None,
    covariate_dist: str,
    max_time: float | None,
    model_cens: str,
    cens_par: float,
) -> None

Validate parameters for competing risks data generation.

Source code in gen_surv/validation.py
def validate_competing_risks_inputs(
    n: int,
    n_risks: int,
    baseline_hazards: Sequence[float] | None,
    betas: Sequence[Sequence[float]] | None,
    covariate_dist: str,
    max_time: float | None,
    model_cens: str,
    cens_par: float,
) -> None:
    """Validate parameters for competing risks data generation."""
    _validate_covariate_inputs(n, None, model_cens, cens_par, covariate_dist, max_time)
    ensure_positive_int(n_risks, "n_risks")

    if baseline_hazards is not None:
        ensure_sequence_length(baseline_hazards, n_risks, "baseline_hazards")
        ensure_positive_sequence(baseline_hazards, "baseline_hazards")

    if betas is not None:
        if not isinstance(betas, list) or any(not isinstance(b, list) for b in betas):
            raise ListOfListsError("betas", betas)
        for b in betas:
            ensure_numeric_sequence(b, "betas")

validate_piecewise_params

validate_piecewise_params(
    breakpoints: Sequence[float],
    hazard_rates: Sequence[float],
) -> None

Validate breakpoint and hazard rate sequences.

Source code in gen_surv/validation.py
def validate_piecewise_params(
    breakpoints: Sequence[float], hazard_rates: Sequence[float]
) -> None:
    """Validate breakpoint and hazard rate sequences."""
    ensure_sequence_length(hazard_rates, len(breakpoints) + 1, "hazard_rates")
    ensure_positive_sequence(breakpoints, "breakpoints")
    ensure_positive_sequence(hazard_rates, "hazard_rates")
    if np.any(np.diff(breakpoints) <= 0):
        raise ParameterError(
            "breakpoints",
            breakpoints,
            "must be a strictly increasing sequence. Sort the list and remove duplicates.",
        )

validate_gen_piecewise_inputs

validate_gen_piecewise_inputs(
    n: int,
    breakpoints: Sequence[float],
    hazard_rates: Sequence[float],
    n_covariates: int,
    model_cens: str,
    cens_par: float,
    covariate_dist: str,
) -> None

Validate parameters for :func:gen_piecewise_exponential.

Source code in gen_surv/validation.py
def validate_gen_piecewise_inputs(
    n: int,
    breakpoints: Sequence[float],
    hazard_rates: Sequence[float],
    n_covariates: int,
    model_cens: str,
    cens_par: float,
    covariate_dist: str,
) -> None:
    """Validate parameters for :func:`gen_piecewise_exponential`."""
    _validate_covariate_inputs(n, n_covariates, model_cens, cens_par, covariate_dist)
    validate_piecewise_params(breakpoints, hazard_rates)

validate_gen_mixture_inputs

validate_gen_mixture_inputs(
    n: int,
    cure_fraction: float,
    baseline_hazard: float,
    n_covariates: int,
    model_cens: str,
    cens_par: float,
    max_time: float | None,
    covariate_dist: str,
) -> None

Validate parameters for :func:gen_mixture_cure.

Source code in gen_surv/validation.py
def validate_gen_mixture_inputs(
    n: int,
    cure_fraction: float,
    baseline_hazard: float,
    n_covariates: int,
    model_cens: str,
    cens_par: float,
    max_time: float | None,
    covariate_dist: str,
) -> None:
    """Validate parameters for :func:`gen_mixture_cure`."""
    _validate_covariate_inputs(
        n, n_covariates, model_cens, cens_par, covariate_dist, max_time
    )
    ensure_positive(baseline_hazard, "baseline_hazard")
    if not 0 < cure_fraction < 1:
        raise ParameterError(
            "cure_fraction",
            cure_fraction,
            "must be between 0 and 1 (exclusive). Try a value like 0.5",
        )

validate_gen_recurrent_events_inputs

validate_gen_recurrent_events_inputs(
    n: int,
    process: str,
    baseline: object,
    baseline_params: dict[str, float] | None,
    n_covariates: int,
    stratum_effects: Sequence[float] | None,
    max_events: int | None,
    followup_time: float,
    model_cens: str,
    cens_par: float,
) -> None

Validate parameters for :func:gen_surv.recurrent.gen_recurrent_events.

Source code in gen_surv/validation.py
def validate_gen_recurrent_events_inputs(
    n: int,
    process: str,
    baseline: object,
    baseline_params: dict[str, float] | None,
    n_covariates: int,
    stratum_effects: Sequence[float] | None,
    max_events: int | None,
    followup_time: float,
    model_cens: str,
    cens_par: float,
) -> None:
    """Validate parameters for :func:`gen_surv.recurrent.gen_recurrent_events`."""
    ensure_positive_int(n, "n")
    ensure_positive_int(n_covariates, "n_covariates")
    ensure_censoring_model(model_cens)
    ensure_positive(cens_par, "cens_par")
    ensure_positive(followup_time, "followup_time")
    ensure_in_choices(process, "process", _RECURRENT_PROCESSES)
    _validate_baseline_params(baseline, baseline_params)

    if stratum_effects is not None:
        # Andersen-Gill is defined by an intensity that does not depend on the
        # event history, so per-event effects contradict the process. Silently
        # applying them would mislabel PWP data as AG, and silently dropping
        # them would discard an argument the caller clearly meant.
        if process == "ag":
            raise ParameterError(
                "stratum_effects",
                stratum_effects,
                "is not applicable to process='ag', whose intensity cannot "
                "depend on the event number; use process='pwp_tt' or "
                "process='pwp_gt'",
            )
        ensure_numeric_sequence(stratum_effects, "stratum_effects")
        ensure_positive_sequence(stratum_effects, "stratum_effects")
        if len(stratum_effects) == 0:
            raise ParameterError(
                "stratum_effects", stratum_effects, "must not be empty"
            )

    if max_events is not None:
        ensure_positive_int(max_events, "max_events")