Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,60 @@ other release, fixes included, the minor.

## [Unreleased]

## [0.13] - 2026-10-09

Simpler, faster and harder to misuse. Each computation now has one
implementation (#204): a simulation's random numbers are counter-based, a
function of the seed, the stream, the simulation and the draw alone
(#209); every timeline is recorded by the event loop (#205); numba's engine
takes every level's events in one loop (#206) and keeps whether each level
works up to date without truth tables (#255, #254); a decision diagram has
one search and one replay (#207); and a run's changes are put in time order
by numpy alone (#208). The exact curves are built several times as fast,
an MTTF takes a few hundred evaluations where it took thousands (#229),
and capacity runs, timelines and conditional runs spend less time around
the compiled loop (#246, #247, #248). Common-cause groups are worked out
module by module, linear in the groups rather than doubling per group, in
fault trees and non-repairable diagrams (#219) and in repairable ones
(#218), whose members' tests and repairs may now take time (#220); limited
repair crews refuse common-cause groups where they dropped them (#251,
#252); and `availability_rate` is exact just after a scheduled maintenance
(#240). From the 0.12 persona check (#213): a mixture as a repairable life
(#227), times first in the measures over time (#224), replacement intervals
from a calendar (#230), discounted costs from new and endless horizons
(#231), chunks of a plain run (#236), unavailability over time to its own
precision (#237), `to_dict()` on every result (#235), public levers
(#244), simulations of one diagram from several threads (#216), a shared
fitted model drawn once in parameter uncertainty (#214), messages that say
what to do and inputs checked where they are given (#232, #233), and docs
brought up to date (#238). What 0.12 deprecated is gone.

Behaviour changes: a `RepairableRBD` simulation's seeded results differ
from 0.12's, within their standard errors, as its random numbers are
counter-based (#209); exact curves move in their seventh or eighth
significant figure (a grid of 1,000 steps); a cost result's `mean`,
`cost_rate` and breakdowns, and the new `mean_availability`, are the run's
estimate (exact or conditional by default), the simulations' own being
`sample_mean` (#223); a model given for the input or output node is
refused (#217); costs, intervals and times given as text or booleans,
surpyval's distribution class in place of a model, and seeds that are not
whole numbers or lists of them are refused (#233, #232), as are unknown
nodes in `allowed=`, `offset_shares=` and `with_intervals` (#222) and a
component that fails and is repaired at once; a fixed-probability node's
simulated lifetime is 0 or infinite, so seeded draws of such a diagram
change; `compare(quantity="cost")` counts acquisition, and `cost()` with
only an acquisition cost gives a result (#234); a test offset within a
billionth of the interval is 0 (#237); a capacity run's probabilities can
differ from 0.12's in their last digits (#246); what 0.12 deprecated is
removed: calling `SparesDemand.mean()` or `std()` raises `TypeError` (they
are properties), as do `StandbyModel`'s and `LoadSharingModel`'s
`mc_samples`, `lower` and `seed`, and `StandbyModel`'s
`switching_probability` and `dormancy_factor` are given by name; and
surpyval 0.23 is required. Deprecated, to go in 0.14:
`optimal_inspection_intervals(offsets=)` (now `offset_shares=`), calling
`CapacityDistribution.mean()`, and the simulation options of an exact
`mean`.

### Added

- **A surpyval `MixtureModel` as a repairable component's life (#227).**
Expand Down Expand Up @@ -367,6 +421,7 @@ other release, fixes included, the minor.
bound method printed when `()` is left off.)
- **Warnings point at the line that called the package (#232)**, where
several pointed inside it.

### Deprecated

- **`optimal_inspection_intervals(offsets=)` is renamed `offset_shares=`
Expand All @@ -388,6 +443,17 @@ other release, fixes included, the minor.
`_calendar_lever` and `_as_spec`, stay for 0.13 (its `Lever` tuple with
a `bounds` field at its end), and may change or go in 0.14: use
`levers()`, `Lever` (its `calendar` and `value`) and `with_levers`.

### Removed

- **What 0.12 deprecated.** `SparesDemand.mean` and `std` are properties
alone: calling them, `mean()`, raises `TypeError` (#184). `StandbyModel`
and `LoadSharingModel` no longer take `mc_samples`, `lower` or `seed`,
which set a fit to simulated lifetimes that 0.12 removed (#149):
passing them raises `TypeError`. `StandbyModel`'s `switching_probability`
and `dormancy_factor` came after them, so they are now given by name, and
a call that gave them by position is refused rather than misread.

### Fixed

- **`availability_rate` just after a scheduled maintenance** (#240). A
Expand Down
17 changes: 8 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,20 +338,19 @@
non-parametric node, and a standby or load-sharing model with no exact
or numerical reliability refuses one (`is_simulated`), where a fit to
simulated lifetimes stood in, and is left to the simulations. What 0.12
deprecates goes in 0.13 (`NEXT_REMOVAL`): calling
`SparesDemand.mean()`/`std()`, now properties (#184, through
`deprecation.called`), and those models' `mc_samples`, `lower` and
`seed`, which set the fit (through `deprecation.ignored`).
`test_the_calls_go_in_the_release_after_next` fails once the version
reaches it. What 0.13 deprecates goes in 0.14 (`REMOVAL_AFTER_NEXT`):
`optimal_inspection_intervals(offsets=)`, renamed `offset_shares=`
(#222, through `deprecation.renamed`); calling
deprecated went in 0.13, and `test_removed_in_0_13.py` keeps it gone:
calling `SparesDemand.mean()`/`std()` (#184) and those models'
`mc_samples`, `lower` and `seed` raise `TypeError`, and `StandbyModel`'s
options after them are given by name. What 0.13 deprecates goes in 0.14
(`NEXT_REMOVAL`): `optimal_inspection_intervals(offsets=)`, renamed
`offset_shares=` (#222, through `deprecation.renamed`); calling
`CapacityDistribution.mean()`, now a property (#235, through
`deprecation.called`); and `mc_samples` and `seed` given to the exact
`mean` of a `StandbyModel`, `LoadSharingModel` or `DegradingNode`,
which ignores them (#233, through `deprecation.ignored` in
`standby_node.drawn_mean`), all of which
`test_what_0_13_deprecates_goes_in_0_14` holds to.
`test_what_0_13_deprecates_goes_in_0_14` holds to: it fails once the
version reaches `NEXT_REMOVAL`.
- **Exact by default, simulation on request.** Where an analysis can be
computed exactly or numerically, that is the default, and the Monte-Carlo
estimate is a `method="simulate"` away (as for `NonRepairableRBD.mean`).
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/redundancy-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ reliability, naming the simulations that take it: `random`,
`NonRepairableRBD`, or `availability` and `cost` of a `RepairableRBD`.
Until 0.12 such an arrangement was given a Kaplan–Meier fit to simulated
lifetimes, which carried their Monte-Carlo error into every exact analysis
of the system; the `mc_samples`, `lower` and `seed` that set the fit are
now ignored, and warn until 0.13 refuses them.
of the system; the `mc_samples`, `lower` and `seed` that set the fit went
in 0.13.

`mean()` and `random(size, seed=None)` give the arrangement's mean lifetime
and draw lifetimes; `cs(x, X)` is its conditional survival. A standby node
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/saving.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ path = Path(tempfile.mkdtemp()) / "plant.json"
rbd.to_json(path) # written; returns None
NonRepairableRBD.from_json(path).sf(30) == rbd.sf(30) # True
type(RBD.from_dict(data)).__name__ # 'NonRepairableRBD': the base class dispatches on type
data["type"], data["repyability_version"] # ('NonRepairableRBD', '0.12')
data["type"], data["repyability_version"] # ('NonRepairableRBD', '0.13')
```

What is saved:
Expand Down
2 changes: 1 addition & 1 deletion repyability/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
dependencies) via ``[tool.setuptools.dynamic]`` in ``pyproject.toml``.
"""

__version__ = "0.12"
__version__ = "0.13"
18 changes: 1 addition & 17 deletions repyability/rbd/load_sharing_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
import numpy as np
from surpyval import Hypoexponential

from repyability.utils.deprecation import ignored, refuse_removed_names
from repyability.utils.deprecation import refuse_removed_names
from repyability.utils.wrappers import conditional_survival, numpy_seed

from ._dependent_lifetimes import LoadSharingSurvival
Expand Down Expand Up @@ -143,14 +143,6 @@ class LoadSharingModel:
k : int, optional
The minimum number of surviving units for the group to work, from
1 to ``len(models)``, by default 1.
mc_samples : int, optional
Deprecated and unused: the number of simulated lifetimes the
reliability of different units was fitted to until 0.12. Passing
it warns, and 0.13 will refuse it.
lower : float, optional
Deprecated and unused, as ``mc_samples`` is: that fit's lower limit.
seed : int or None, optional
Deprecated and unused, as ``mc_samples`` is: that fit's seed.

Attributes
----------
Expand Down Expand Up @@ -209,15 +201,7 @@ def __init__(
models,
load,
k=1,
mc_samples=None,
lower=None,
seed=None,
):
ignored(
"LoadSharingModel()",
"its reliability is no longer fitted to simulated lifetimes.",
{"mc_samples": mc_samples, "lower": lower, "seed": seed},
)
models = list(models)
if len(models) == 0:
raise ValueError("LoadSharingModel needs at least one unit.")
Expand Down
21 changes: 5 additions & 16 deletions repyability/rbd/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from scipy.special import ndtri

from repyability.rbd import _montecarlo as montecarlo
from repyability.utils.deprecation import REMOVAL_AFTER_NEXT, called
from repyability.utils.deprecation import called
from repyability.utils.wrappers import outside_level


Expand Down Expand Up @@ -1995,7 +1995,6 @@ def mean(self) -> Any:
return called(
self._per_time(np.sum(parts, axis=0)),
"CapacityDistribution.mean",
REMOVAL_AFTER_NEXT,
)

def delivered_fraction(self, demand: float) -> Any:
Expand Down Expand Up @@ -2724,21 +2723,15 @@ def __repr__(self) -> str:

@property
def mean(self) -> float:
"""The expected number of spares used.

A property, as the other results' values are (#184): calling it,
``mean()``, as before 0.12, still gives it, with a
``FutureWarning``, until 0.13.
"""The expected number of spares used (a property, as the other
results' values are, #184).

Returns
-------
float
The mean number of replacements.
"""
return called(
float(self.probabilities @ np.arange(len(self.probabilities))),
"SparesDemand.mean",
)
return float(self.probabilities @ np.arange(len(self.probabilities)))

@property
def std(self) -> float:
Expand All @@ -2751,11 +2744,7 @@ def std(self) -> float:
The standard deviation of the number of replacements.
"""
counts = np.arange(len(self.probabilities))
mean = float(self.mean)
return called(
float(np.sqrt(self.probabilities @ (counts - mean) ** 2)),
"SparesDemand.std",
)
return float(np.sqrt(self.probabilities @ (counts - self.mean) ** 2))

def covered(self, stock: int) -> float:
"""The probability that ``stock`` spares cover the horizon's
Expand Down
26 changes: 3 additions & 23 deletions repyability/rbd/standby_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
from surpyval import Hypoexponential

from repyability.utils.checks import simulation_options, whole_number
from repyability.utils.deprecation import (
REMOVAL_AFTER_NEXT,
ignored,
refuse_removed_names,
)
from repyability.utils.deprecation import ignored, refuse_removed_names
from repyability.utils.wrappers import numpy_seed

from ._dependent_lifetimes import (
Expand Down Expand Up @@ -56,7 +52,6 @@ def drawn_mean(model, name: str, exact, mc_samples, seed, method) -> float:
"the mean is worked out exactly (or numerically), so nothing is "
"drawn; method='simulate' estimates it from draws instead.",
options,
REMOVAL_AFTER_NEXT,
)
return value

Expand Down Expand Up @@ -189,21 +184,13 @@ class StandbyModel:
k : int, optional
The number of units that must operate for the arrangement to work,
from 1 to ``len(reliabilities)``, by default 1.
mc_samples : int, optional
Deprecated and unused: the number of simulated lifetimes the
reliability was fitted to until 0.12. Passing it warns, and 0.13
will refuse it.
lower : float, optional
Deprecated and unused, as ``mc_samples`` is: that fit's lower limit.
switching_probability : float or sequence of float, optional
The probability, in ``[0, 1]``, that switching onto the next spare
succeeds: a scalar for every switch, or one value per spare
(length ``len(reliabilities) - k``). A failed switch ends the
arrangement's life when the unit it should replace fails. By
default 1.0 (perfect switching). Imperfect switching is supported
for cold standby only.
seed : int or None, optional
Deprecated and unused, as ``mc_samples`` is: that fit's seed.
for cold standby only. Given by name, as ``dormancy_factor`` is.
dormancy_factor : float, optional
The dormant-to-operating aging ratio, in ``[0, 1]``: 0 is cold, 1
is hot and anything between is warm. By default 0.0.
Expand Down Expand Up @@ -286,17 +273,10 @@ def __init__(
self,
reliabilities,
k=1,
mc_samples=None,
lower=None,
*,
switching_probability=1.0,
seed=None,
dormancy_factor=0.0,
):
ignored(
"StandbyModel()",
"its reliability is no longer fitted to simulated lifetimes.",
{"mc_samples": mc_samples, "lower": lower, "seed": seed},
)
k = whole_number(k, "k (how many units must operate)")
if k > len(reliabilities):
raise ValueError(
Expand Down
14 changes: 2 additions & 12 deletions repyability/tests/test_api_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from repyability._version import __version__
from repyability.rbd.ccf import with_parameters
from repyability.rbd.uncertainty import draw_ccf_models
from repyability.utils.deprecation import NEXT_REMOVAL, REMOVAL_AFTER_NEXT
from repyability.utils.deprecation import NEXT_REMOVAL

W, E = surv.Weibull.from_params, surv.Exponential.from_params
F = FixedEventProbability.from_params
Expand Down Expand Up @@ -258,16 +258,6 @@ def test_the_spares_demand_values_are_properties():
demand = rbd.spares_demand(1000.0)["pump"]
assert demand.mean == pytest.approx(10.0, rel=1e-5)
assert demand.std == pytest.approx(np.sqrt(10.0), rel=1e-4)
with pytest.warns(FutureWarning, match=r"write mean, not mean\(\)"):
assert demand.mean() == demand.mean
with pytest.warns(FutureWarning, match="0.13"):
assert demand.std() == demand.std


def test_the_calls_go_in_the_release_after_next():
# When the version reaches NEXT_REMOVAL, what 0.12 deprecates must go.
version = tuple(map(int, __version__.split(".")))
assert version < tuple(map(int, NEXT_REMOVAL.split(".")))


def test_what_0_13_deprecates_goes_in_0_14():
Expand All @@ -278,7 +268,7 @@ def test_what_0_13_deprecates_goes_in_0_14():
# names callers used before the levers were public, kept as they were
# for 0.13: rbd._sensitivity's levers and Lever (#244).
version = tuple(map(int, __version__.split(".")))
assert version < tuple(map(int, REMOVAL_AFTER_NEXT.split(".")))
assert version < tuple(map(int, NEXT_REMOVAL.split(".")))


def test_an_uncertainty_result_shows_a_summary():
Expand Down
4 changes: 3 additions & 1 deletion repyability/tests/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ def crow():
"max_samples",
),
(
lambda: StandbyModel([W([100, 2])] * 2, n_sims=100),
lambda: StandbyModel([W([100, 2])] * 2).mean(
method="simulate", n_sims=100
),
"n_sims",
"mc_samples",
),
Expand Down
21 changes: 1 addition & 20 deletions repyability/tests/test_removed_in_0_12.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
and the standby and load-sharing models with no exact or numerical
reliability refuse it, rather than fitting one to simulated lifetimes, and
are left to the simulations. Their constructors' simulation settings, which
set that fit, warn until 0.13 (``NEXT_REMOVAL``)."""
set that fit, went in 0.13 (``test_removed_in_0_13``)."""

import warnings

Expand Down Expand Up @@ -257,25 +257,6 @@ def test_a_repairable_component_with_one_is_simulated(build, case):
assert 0.0 < float(np.mean(result.availability)) <= 1.0


@pytest.mark.parametrize("build, case", NO_RELIABILITY, ids=IDS)
def test_the_fits_settings_warn_until_0_13(build, case):
from repyability.utils.deprecation import NEXT_REMOVAL

model = quiet(build)
kind = type(model)
with pytest.warns(
FutureWarning, match=rf"mc_samples, seed.*{NEXT_REMOVAL}"
):
if kind is StandbyModel:
StandbyModel(
model.reliabilities, k=model.k, mc_samples=100, seed=1
)
else:
LoadSharingModel(
model.models, load=model.load, mc_samples=100, seed=1
)


def test_exact_and_numerical_models_are_unchanged():
for build in (
lambda: StandbyModel([W([100, 2])] * 2),
Expand Down
Loading
Loading