From 07d42618482669c8a2e987a41a40242016f6fdc7 Mon Sep 17 00:00:00 2001 From: Derryn Knife Date: Fri, 9 Oct 2026 20:41:03 +1000 Subject: [PATCH] Release 0.13: remove what 0.12 deprecated; version, CHANGELOG, saving guide (#260) * Refuse node_availability with crews and common-cause groups (#251) With limited repair crews, every long-run value but node_availability already refused common-cause groups. node_availability gave each component's value in the crews' chain, which has no common causes, although a shared failure leaves one member waiting for the crew. The crews' chain now refuses groups itself (_require_crew_chain), so neither it nor its route can be read without them. The refusal says to simulate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS * Keep every level's structure up to date; no truth tables (#255) The compiled engine built a table of whether the system works in each of the 2^n states of its components (n <= 20), and one for each nested RBD, on every run: 0.11 s for 20 components, over half of a 1,000-simulation run. The incremental structure it used above 20 components is as fast as the table at every size measured, and faster at 20, so it is now the one way: _compiled._kept lays out every level's structure as one (each path set's level, each level's root and whether it always works), and the kernel keeps it up to date at every level (_keep, _level_works). The tables, truth_table, _works and MAX_TABLED are gone, and with them the refusal of a nested RBD of more than 20 components. Results are the same, bit for bit. A 1,000-simulation run of two lines of ten components takes 0.036 s rather than 0.119 s; 20,000, 0.62 s rather than 0.70 s. engines.API is 3, as the run's arrays changed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS * Work out each stream's key once a run (#254) A run's Plan keeps each stream's Philox key, worked out once, and hands it to the blocks: 40 SeedSequences rather than 800 for 20,000 simulations of 20 components (about 0.035 s of 0.65 s). Laying the draws out more tightly was measured and left out: a limit for each simulation, extended alone, with first chunks nearer the expected draws, worked out a third fewer values but ran the simulations that ran out again, for no net gain on any of five benchmarks. The quantile functions' overhead is raised in surpyval (SurPyval#769). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS * Capacity: add up equal levels with a sorted reduction (#246) merged() summed the probabilities of equal levels with np.add.at, one row at a time. A stable sort of the rows by level and np.add.reduceat make the same sums in the same order (each level from its first row, and 0.0 + x is x), so results are the same, bit for bit. tidy() is cached: a run tidies the same few levels for every state it meets. 25 redundant pairs in series (50 components with capacities), 2,000 h, numba: 500 simulations 0.39 s -> 0.31 s, 2,000 simulations 1.04 s -> 0.96 s; 70 pairs on the Python loop, 100 simulations, 3.83 s -> 3.5 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS * Timelines: work out each change's position once (#247) A _Data's changes' histories and positions in them are a cached property, worked out once with a repeat of the offsets (no gather through the histories' numbers), where every measure (uptime, failures, the curve, the merges) worked them out again. A unit's own histories no longer store their changes' index among their cause's changes, which is their position (index None; _Data.indices gives either). Results are the same, bit for bit. The 12-component bridge feeding a vote, 20,000 simulations, numba: simulate_timelines 0.76 s -> 0.69 s, the first measures over its result 0.79 s -> 0.68 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS * Conditional runs: order the modules' changes in one sort (#248) paths() put every module's changes in order with a four-key lexsort, and the stretches with another. The changes now take one sort of a key that grows with the simulation and the time (each simulation's times shifted past the one before's by a power of two beyond the window, so rounding never puts two out of order), and each run of equal keys (changes at one instant, or within rounding) is put in order exactly: by simulation, time, rank, then as given. The stretches need no sort: a simulation's start goes before its first change, the changes already in order. values() and capacity_values() find each joint state's stretches, and the changes down from and into it, from one stable sort, rather than comparing every stretch with every state. Results are the same, bit for bit. Also: the simulation guide said the compiled engine takes nested RBDs of up to 20 components, which #255 lifted. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS * CHANGELOG: faster capacity runs, timelines and conditional runs (#246, #247, #248) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS * Capacity: black (#246) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS * Capacity: sum no levels when none is given; equal to np.add.at to rounding (#246) The sorted reduction marked a first row even with no rows, so a run whose capacity had no level left failed in add.reduceat. Its sums also differ from np.add.at's in the last bits where a level has three or more rows (reduceat adds a level's later rows up before its first): the test now checks the sums to rounding and the levels exactly, and the CHANGELOG says so. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS * Release 0.13: remove what 0.12 deprecated; version, CHANGELOG, saving guide What 0.12 deprecated goes: calling SparesDemand.mean() or std(), properties since 0.12 (#184), raises TypeError, as do StandbyModel's and LoadSharingModel's mc_samples, lower and seed, which set a fit to simulated lifetimes that 0.12 removed (#149). StandbyModel's switching_probability and dormancy_factor came after them, so they are given by name, and a call that gave them by position is refused rather than misread. test_removed_in_0_13 keeps them gone. deprecation.NEXT_REMOVAL is now 0.14, what 0.13 deprecates (offsets=, CapacityDistribution.mean() called, an exact mean's simulation options), and REMOVAL_AFTER_NEXT goes. The CHANGELOG's [Unreleased] becomes [0.13] - 2026-10-09, with its summary and behaviour changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS --------- Co-authored-by: Claude --- CHANGELOG.md | 66 +++++++++++++++++++++++ CLAUDE.md | 17 +++--- docs/guide/redundancy-models.md | 4 +- docs/guide/saving.md | 2 +- repyability/_version.py | 2 +- repyability/rbd/load_sharing_node.py | 18 +------ repyability/rbd/results.py | 21 ++------ repyability/rbd/standby_node.py | 26 ++------- repyability/tests/test_api_consistency.py | 14 +---- repyability/tests/test_messages.py | 4 +- repyability/tests/test_removed_in_0_12.py | 21 +------- repyability/tests/test_removed_in_0_13.py | 52 ++++++++++++++++++ repyability/utils/deprecation.py | 48 ++++++++--------- 13 files changed, 167 insertions(+), 128 deletions(-) create mode 100644 repyability/tests/test_removed_in_0_13.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cf61b7e0..21887dd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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).** @@ -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=` @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 5418f523..fbc4489c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`). diff --git a/docs/guide/redundancy-models.md b/docs/guide/redundancy-models.md index 0f5f1c2b..2e7e5d82 100644 --- a/docs/guide/redundancy-models.md +++ b/docs/guide/redundancy-models.md @@ -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 diff --git a/docs/guide/saving.md b/docs/guide/saving.md index 280d7939..1e2bf6f3 100644 --- a/docs/guide/saving.md +++ b/docs/guide/saving.md @@ -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: diff --git a/repyability/_version.py b/repyability/_version.py index df19ad71..639c5fa1 100644 --- a/repyability/_version.py +++ b/repyability/_version.py @@ -5,4 +5,4 @@ dependencies) via ``[tool.setuptools.dynamic]`` in ``pyproject.toml``. """ -__version__ = "0.12" +__version__ = "0.13" diff --git a/repyability/rbd/load_sharing_node.py b/repyability/rbd/load_sharing_node.py index f178188e..89a6074d 100644 --- a/repyability/rbd/load_sharing_node.py +++ b/repyability/rbd/load_sharing_node.py @@ -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 @@ -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 ---------- @@ -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.") diff --git a/repyability/rbd/results.py b/repyability/rbd/results.py index 8d25ebe4..de7747e8 100644 --- a/repyability/rbd/results.py +++ b/repyability/rbd/results.py @@ -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 @@ -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: @@ -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: @@ -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 diff --git a/repyability/rbd/standby_node.py b/repyability/rbd/standby_node.py index 05ed9ae8..0ea35755 100644 --- a/repyability/rbd/standby_node.py +++ b/repyability/rbd/standby_node.py @@ -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 ( @@ -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 @@ -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. @@ -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( diff --git a/repyability/tests/test_api_consistency.py b/repyability/tests/test_api_consistency.py index 3b4987f5..14807d41 100644 --- a/repyability/tests/test_api_consistency.py +++ b/repyability/tests/test_api_consistency.py @@ -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 @@ -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(): @@ -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(): diff --git a/repyability/tests/test_messages.py b/repyability/tests/test_messages.py index 18cbfb63..92e03922 100644 --- a/repyability/tests/test_messages.py +++ b/repyability/tests/test_messages.py @@ -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", ), diff --git a/repyability/tests/test_removed_in_0_12.py b/repyability/tests/test_removed_in_0_12.py index 7f1f2a45..dd03067c 100644 --- a/repyability/tests/test_removed_in_0_12.py +++ b/repyability/tests/test_removed_in_0_12.py @@ -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 @@ -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), diff --git a/repyability/tests/test_removed_in_0_13.py b/repyability/tests/test_removed_in_0_13.py new file mode 100644 index 00000000..bb6f178d --- /dev/null +++ b/repyability/tests/test_removed_in_0_13.py @@ -0,0 +1,52 @@ +"""What 0.12 deprecated is gone in 0.13: calling ``SparesDemand.mean()`` +and ``std()``, properties since 0.12 (#184), and ``StandbyModel``'s and +``LoadSharingModel``'s ``mc_samples``, ``lower`` and ``seed``, which set a +fit to simulated lifetimes that 0.12 removed (#149).""" + +import numpy as np +import pytest +import surpyval as surv + +from repyability import LoadSharingModel, RepairableRBD, StandbyModel + +W, E = surv.Weibull.from_params, surv.Exponential.from_params + + +def aft(): + rng = np.random.default_rng(0) + load = rng.uniform(0.5, 2.0, size=400) + x = rng.exponential(100.0 / np.exp(0.6 * (load - 1.0)), size=400) + return surv.ExponentialAFT.fit(x + 1e-3, Z=load.reshape(-1, 1)) + + +def test_the_spares_demand_values_are_not_called(): + rbd = RepairableRBD( + [("s", "pump"), ("pump", "t")], + {"pump": {"reliability": E([0.01]), "repairability": "instant"}}, + ) + demand = rbd.spares_demand(1000.0)["pump"] + assert type(demand.mean) is float and type(demand.std) is float + for value in (demand.mean, demand.std): + with pytest.raises(TypeError, match="not callable"): + value() + + +@pytest.mark.parametrize("name", ["mc_samples", "lower", "seed"]) +def test_the_fits_settings_are_refused(name): + with pytest.raises(TypeError, match=name): + StandbyModel([W([100, 2])] * 2, **{name: 1}) + with pytest.raises(TypeError, match=name): + LoadSharingModel([aft()] * 2, load=2.0, **{name: 1}) + + +def test_a_standby_models_options_are_given_by_name(): + # A switching probability or dormancy factor given by position was + # after the removed settings, so it is refused rather than misread. + units = [E([0.01])] * 2 + with pytest.raises(TypeError): + StandbyModel(units, 1, None, None, 0.9) + with pytest.raises(TypeError): + StandbyModel(units, 1, 0.9) + given = StandbyModel(units, 1, switching_probability=0.9) + assert given.switching_probability == 0.9 + assert float(given.mean()) == pytest.approx(100.0 + 0.9 * 100.0) diff --git a/repyability/utils/deprecation.py b/repyability/utils/deprecation.py index 27dd55b4..6d5c744f 100644 --- a/repyability/utils/deprecation.py +++ b/repyability/utils/deprecation.py @@ -1,24 +1,23 @@ """Deprecated names, arguments and calls. -A deprecation gives one minor release's notice: what 0.12 deprecates goes -in 0.13 (``NEXT_REMOVAL``). Each warns with a ``FutureWarning``, which +A deprecation gives one minor release's notice: what 0.13 deprecates goes +in 0.14 (``NEXT_REMOVAL``). Each warns with a ``FutureWarning``, which Python always shows, as the notice is short: -- calling a result's value that is now a property, such as - ``SparesDemand.mean()`` (``called``, #184); -- ``StandbyModel``'s and ``LoadSharingModel``'s ``mc_samples``, ``lower`` - and ``seed``, which set a fit to simulated lifetimes that 0.12 removed - (``ignored``, #149). - -What 0.13 deprecates goes in 0.14 (``REMOVAL_AFTER_NEXT``): the -search's ``offsets`` of ``optimal_inspection_intervals``, renamed -``offset_shares`` (``renamed``, #222); ``CapacityDistribution.mean()``, -now a property (``called``, #235); and the simulation options of a -``StandbyModel``'s, ``LoadSharingModel``'s or ``DegradingNode``'s exact -``mean`` (``ignored``, #233). - -What 0.11 deprecated went in 0.12 (#149): its old names are refused with -the names that took their place (``refuses_removed_names``, #232). +- the search's ``offsets`` of ``optimal_inspection_intervals``, renamed + ``offset_shares`` (``renamed``, #222); +- calling ``CapacityDistribution.mean()``, now a property (``called``, + #235); +- the simulation options of a ``StandbyModel``'s, ``LoadSharingModel``'s + or ``DegradingNode``'s exact ``mean``, which it ignores (``ignored``, + #233). + +What 0.12 deprecated went in 0.13: calling ``SparesDemand.mean()`` and +``std()``, properties since 0.12 (#184), and ``StandbyModel``'s and +``LoadSharingModel``'s ``mc_samples``, ``lower`` and ``seed``, which set a +fit to simulated lifetimes that 0.12 removed (#149). What 0.11 deprecated +went in 0.12 (#149): its old names are refused with the names that took +their place (``refuses_removed_names``, #232). """ import functools @@ -30,10 +29,8 @@ from repyability.utils.wrappers import outside_level -#: The release that removes what 0.12 deprecates. -NEXT_REMOVAL = "0.13" #: The release that removes what 0.13 deprecates. -REMOVAL_AFTER_NEXT = "0.14" +NEXT_REMOVAL = "0.14" def _called_warning(name: str, removal: str) -> None: @@ -77,7 +74,7 @@ class CalledArray(np.ndarray): of the array, with a ``FutureWarning``.""" _name: str = "" - _removal: str = REMOVAL_AFTER_NEXT + _removal: str = NEXT_REMOVAL def __call__(self) -> np.ndarray: _called_warning(self._name, self._removal) @@ -90,7 +87,7 @@ def __reduce__(self): def called(value: Any, name: str, removal: str = NEXT_REMOVAL) -> Any: """``value``, a result's property ``name`` (``"Class.attribute"``) that used to be a method: still callable, with a ``FutureWarning``, until - ``removal`` (``NEXT_REMOVAL`` for what 0.12 deprecated). A number + ``removal`` (``NEXT_REMOVAL`` by default). A number becomes a ``CalledValue``, an array a ``CalledArray``.""" if np.ndim(value) == 0: return CalledValue(float(value), name, removal) @@ -107,7 +104,7 @@ def ignored( ) -> None: """Warn that the arguments in ``given`` that were passed (not None or False) are ignored by ``method``, for the reason ``why``: refused in - ``removal`` (``NEXT_REMOVAL`` for what 0.12 deprecated).""" + ``removal`` (``NEXT_REMOVAL`` by default).""" passed = [ name for name, value in given.items() @@ -126,11 +123,10 @@ def ignored( def renamed(method: str, old: str, new: str, why: str) -> None: """Warn that ``method``'s argument ``old`` is renamed ``new``, for the - reason ``why``: deprecated in 0.13, it is refused in - ``REMOVAL_AFTER_NEXT``.""" + reason ``why``: deprecated in 0.13, it is refused in ``NEXT_REMOVAL``.""" warnings.warn( f"{method}'s {old}= is renamed {new}=, as {why}. Passing {old}= is " - f"deprecated, and {REMOVAL_AFTER_NEXT} will refuse it.", + f"deprecated, and {NEXT_REMOVAL} will refuse it.", FutureWarning, stacklevel=outside_level(), )