Skip to content

Merge dev into master: the work since 0.12 - #259

Merged
derrynknife merged 46 commits into
masterfrom
dev
Oct 9, 2026
Merged

derrynknife merged 46 commits into
masterfrom
dev

Conversation

@derrynknife

Copy link
Copy Markdown
Owner

Summary

Everything on dev since 0.12, into master. It is a merge, not a release: the version stays 0.12, and the CHANGELOG entries stay under [Unreleased].

What it brings (the CHANGELOG's [Unreleased] section has the details):

Closes #204
Closes #205
Closes #206
Closes #207
Closes #208
Closes #209
Closes #213
Closes #214
Closes #215
Closes #216
Closes #217
Closes #218
Closes #219
Closes #220
Closes #221
Closes #222
Closes #223
Closes #224
Closes #225
Closes #226
Closes #227
Closes #228
Closes #229
Closes #230
Closes #231
Closes #232
Closes #233
Closes #234
Closes #235
Closes #236
Closes #237
Closes #238
Closes #239
Closes #240
Closes #244
Closes #246
Closes #247
Closes #248
Closes #251
Closes #252
Closes #253
Closes #255

Type of change

Checklist

  • Tests added/updated (asserting against a reference value where possible)
  • black, isort, flake8, and mypy pass (this PR's CI)
  • coverage run -m pytest passes and stays above the coverage gate: this PR's CI runs the full suite on Python 3.11–3.14, with numba, on the minimum surpyval and on surpyval's develop
  • CHANGELOG.md updated under [Unreleased]
  • Any Monte-Carlo tests are seeded (deterministic)

Notes for reviewers

🤖 Generated with Claude Code

https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS


Generated by Claude Code

claude and others added 30 commits October 4, 2026 04:38
…nctions

The input and output nodes are inferred from the edges and never fail, so
a model given for one was silently dropped. Forgetting a component's edge
to the output node made that component the output node, and the answer
changed with no warning (#217). The base RBD's structure check now
reports such a model as an invalid structure, naming the edge most likely
missing, on both RBD classes. PerfectReliability, or a fixed probability
of failing of 0 (as node_availability()'s 1.0 for each end gives), may
still be given for an end, and on_infeasible_rbd="warn" keeps the old
behaviour.

NonRepairableRBD.node_mttf() leaves junctions out, as the importance
measures do, where it raised on PerfectReliability's missing mean though
analysis_routes() called it exact (#228).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
)

The routes test now calls every analysis the report lists (27 were never
called) on one catalogue of diagram kinds (tests/catalogue.py), shared
with the engines' agreement test and with new checks of the exact and
numerical routes against the simulation of the same quantity. A guard
fails when a node class, spec key, policy or diagram option has no
diagram, naming it.

It found the report calling exact or simulated what the method refuses,
now refused in the report with the method's own message:
NonRepairableRBD's parameter-uncertainty methods on a node with no
reliability or a structure too meshed; minimum_effort_allocation off a
series system (now found from the graph, a pass per node); a
system_capacity of capacities taken from models; and a repairable
availability allocation with no component to allocate.

Also: a fixed-probability node's simulated lifetime is 0 or inf, as sf
takes it (random() read surpyval's 0/1 indicator as a time), and an MTTF
estimate refuses a diagram of fixed probabilities alone; node_mttf()
gives a PerfectUnreliability node 0 (both helpers gain mean()); and a
MixtureModel node no longer breaks analysis_routes() or saving.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
A diagram simulated from several threads at once crashed
(AttributeError '_cancelled'/'_crews') or returned another seed's
result: the event loop keeps a run's state on the diagram, and draws
that cannot be streamed come from numpy's global RNG, which a seeded run
seeds and restores. A process-wide re-entrant lock now makes the runs
take turns, each giving what it gives alone: RepairableRBD._run (but a
sharded run's parent, so a shard_map on threads cannot deadlock),
numpy_seed and the timelines' event-loop re-runs hold it. The exact
methods, n_jobs' processes and the compiled engine's threads within a
run are not held up. test_threads.py checks seeded calls on threads
against their serial results (it fails without the lock).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
…rtainty (#215)

When the dependent modules stayed in the state they started in, in every
simulation (a standby pair that never went down in the window), each
simulation's expected values given them were the same: the run reported
a standard error of 0 and met a tolerance after its first batch, for the
value of the rest of the system alone. Such a run now warns; a default
run's mean intervals are its simulations' own (method="simulated") and
judge its tolerance; a run of the modules alone has no error to give
(nan) and runs on to max_samples. A run with no modules stays exact.

ConfidenceInterval.method now names every case of a repairable run's
mean (#223): "simulated", "control_variate", "conditional" or "exact".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
In a RepairableRBD, nodes holding the same fitted life but repair fits of
their own had their life drawn per node by default, and no spec could
share it alone, so the *_uncertainty intervals were about 40% too narrow
and the vega split wrong. Draws are now shared per fitted object in each
role: a node may come under several inputs, a role of it under one
({("a", "b"): {"reliability": "fit"}, "a": {"repairability": "fit"}}),
which the default now gives, ordered by first node and role. In both
diagram classes, a spec that draws one node and leaves others holding
the same fitted object as fitted is warned about.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
…221, #222)

optimal_inspection_intervals no longer refuses a component whose tests
can miss a failure (coverage below 1), which 0.12 listed as numerical:
it keeps its full tests' interval, and its test interval is chosen among
those that divide it, from allowed (one that does not is refused, by
name, with some that do) or, left out, from every one in range (a spread
of divisors, then every one around the best).

Its offsets= are shares of the interval, where with_intervals' and the
plan's offsets are times, so a share passed to with_intervals undid a
stagger silently: it is renamed offset_shares=, offsets= kept with a
FutureWarning until 0.14 (deprecation.renamed, REMOVAL_AFTER_NEXT). An
offset for a component with no tests is refused also when its interval
is given; allowed= and offset_shares= dicts naming a node not chosen are
refused; a node that is not a component is refused listing them; and an
inspection plan always has its offsets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
A cost result's mean is its run's estimate of the expected cost, the one
mean_interval gives: exact where the exact methods work it out, given
the modules' histories where those apply, else the simulations' own.
mean_se, cost_rate and the breakdowns follow it (the breakdowns exact or
conditional too, from expected_cost or the module run); sample_mean and
sample_se are the simulations' own. AvailabilityResult gains
mean_availability (the interval's estimate) and
sample_mean_availability. Both results' reprs are summaries.

The docs' examples are checked for the True/False, string, tuple and
list outputs they quote; the costs guide's interval check, which printed
False, and a stale stepped-simulation time are fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
- Grid of 500 steps over a component's typical up time (was 2,000).
- A curve that has not settled grows to where its distance from its
  long-run value, falling cycle by cycle, says it will have settled,
  rather than four times as far.
- Identical plain components (the same life and repair models, the same
  state at 0) share one curve.
- unit_curve's convolutions transform each series once (_Spectra), and
  the series inverse uses a lean FFT product.

Tests whose tolerances were set for the 2,000-step grid, the docs'
quoted numbers and the CHANGELOG are still to be updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CpLNEXQaaMvAeEngrKJFC
…219)

A FaultTree's or NonRepairableRBD's exact values conditioned on every
combination of every group's shock outcomes, doubling the time per group.
Each group is now conditioned on only within the smallest module holding
its members; where a module's groups would multiply past 64 outcomes,
their causes are written out as shock events (repeated nodes of the
diagram, repeated events of the tree) for the decision diagram to work
out. An MGL model's exclusive shocks go in as the independent causes that
fail the same sets as often, where those exist. The values are those of
every combination, which the new tests check against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
…rves

1,000 steps over a component's typical up time rather than the work in
progress's 500: the exact step is still 5-6 times as fast as before (the
settling, the shared curves and the transforms do the rest), with the
error four times what it was at 2,000 rather than sixteen, so that event
counts near fixed-time maintenance and point values soon after the start
keep to a few parts in a million.

Tests set to the 2,000-step grid's error are set to four times it, the
error falling as the square of the step; own_rate's differences span a
step of the coarser grid; the twin's curves are of three different units,
now that identical ones share a curve. New tests: identical components
share one curve, which gives what a curve each gives, and a curve is
followed about as far as it takes to settle. The greeks guide quotes the
valve's rate just after its block replacement as computed now (it
converges slowly as the grid is refined, as it did before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CpLNEXQaaMvAeEngrKJFC
…urves

Tests of event counts and point values against closed forms, and of a
flat diagram against its nested form, are set to four times their error
at 2,000 steps. CLAUDE.md: identical components share one curve, so
anything that makes a unit's curve depend on more than its two models
and its state at 0 must leave it out in _plain_unit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CpLNEXQaaMvAeEngrKJFC
…epairs that take time (#218, #220)

#218: the long-run values, importance measures, failure frequency and
values over time split each time by every combination of every group's
states, so memory doubled per group and ten groups were killed by the
OS. Each group is now conditioned on only within the smallest module
holding its members (_ccf_modules.Tabled), an owner's combinations laid
out as a last array axis and worked out a bounded chunk at a time: fifty
groups in series take a third of a second in flat memory, and groups
that meet in one module are faster and leaner too. Too many combinations
in one module are refused before anything is built; the capacity
distribution and the allocations, which still split, refuse where that
would take too much memory. Limited repair crews no longer drop the
groups from the long-run values: they refuse, as over time.

#220: a tested member's tests and repairs may take a fixed or an
exponential time. The group's chain gains levels for a member off line
for a test, under a test while failed, and under repair, as the
simulation has them; the issue's 1oo2 with an eight-hour MRT gives
9.591e-4 against IEC 61508-6's 9.609e-4. What the chain does not take is
refused saying what to do, without doubled apostrophes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
A component's rate was its whole curve differenced over its grid's step,
which smoothed over a down time far shorter than a step: the greeks
guide's valve, maintained for about 6 h on a grid of days, had its rate
a day after its replacement 1-2% low. _rates.derivative now takes a
curve's own rate where it has one: a GridCurve differences its grid and
adds its dips' rates, each from its down time's density differenced on
its own scale (_density), and its ChainDips' from their interpolation on
their fine grids (_cubic_slope); a BlockCurve the return from the block
replacement before it; ShiftedCurve and StartedBlockCurve pass their
curves' on; a nested RBD's SystemCurve takes its nodes' rates times their
importance. The guides' quoted rates change to the corrected values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CpLNEXQaaMvAeEngrKJFC
Some curves already have a rate: a tested unit's constant failure rate.
_rates.derivative takes a curve's derivative where it is callable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CpLNEXQaaMvAeEngrKJFC
#224, #225, #226, #227)

- A RepairableRBD's importance measures and parameter_sensitivity take
  their times first, as a NonRepairableRBD's do: birnbaum_importance(5.0)
  is birnbaum_importance(x=5.0), barlow_proschan_importance(100.0) its
  window (#224).
- A bare string given to working_nodes, broken_nodes or nodes is one
  node, on every method of RBD and both diagram classes, as is
  FaultTree.occurs("ab")'s event; unknown nodes are listed in order
  (#225).
- The uncertainty methods take arrays of targets as the point methods
  do, a row of draws per target from the same parameter draws, and name
  the argument out of range; a life that may never end has infinite
  bounds, without numpy's warning (#226).
- A surpyval MixtureModel is a RepairableRBD life (#227): the analyses
  take its sf, ff, df and mean, the streams draw it by a bracketed Newton
  inversion of its distribution (no qf, surpyval #651; the components'
  sf summed for the tail, surpyval #671), and imperfect repair and starts
  from an age through MixtureLife (surpyval #626). The uncertainty
  methods say a mixture has no parameter covariance; two mixtures are the
  same model only with the same weights; any other life is refused
  naming the component. Four mixture diagrams join the routes catalogue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
Build the exact curves several times as fast
availability_rate: differentiate the curves' dips exactly (#240)
surpyval's development branch no longer takes p, 0.22's name for a
limited-failure population's share that ever fails, which 0.22 knows by
no other: the building guide's example could not be written for both.
0.23 names it lfp_p, pickles its fits (SurPyval#573) and takes
success_run's level as alpha_ci (SurPyval#580), so with it as the
minimum: models are read by lfp_p alone (lfp_extras and the two names in
_PLAIN go; a file saved before 0.10 with p in its extras still loads), a
run of fitted models goes to n_jobs' workers as it is (_ModelPickler
goes; dumps keeps its message for what cannot be sent), the tests pass
alpha_ci and lfp_p directly, and NonRepairable.mean_availability's
example runs. CLAUDE.md's list of surpyval workarounds is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CpLNEXQaaMvAeEngrKJFC
…r FV, faster interval searches (#229)

- An MTTF integrates the survival function on a few pieces a decade
  from the models' kinks (a numerical curve's grid, where a support
  starts), each by the Gauss-Kronrod (7, 15) rule, and follows the tail
  only until it is 0: 513 evaluations where 20 different Weibulls in
  series took 63,100, to the same value. Network.mean() on a 10x10 grid
  takes 7 s where it took 209; the plan's replay takes more times at once.
- A network refused as too meshed is refused at once after.
- The decision diagrams' computed table starts again past 2^19 entries,
  and a cofactor leaves the nodes after its variable alone: Fussell-Vesely
  on a meshed core takes half the memory.
- A common-cause group's chain keeps exp(G dt) for the steps it takes
  again; a plan keeps its groups' tables, and a search's plans share the
  tested units' models: a 2oo3 group's plan takes 0.2 s where it took
  1.4 to 6.5, its choice of five intervals 32 s where it took 506.
- Interval plans as good (within 1e-10) keep the first tried, so ties
  between identical members no longer turn on rounding.
- A tested unit's walk convolves directly or by FFT itself.
- CLAUDE.md: the MTTF's kinks, and the testing policy (targeted tests for
  dev, the full suite on request and before merging to main).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
Require surpyval 0.23, and drop the workarounds for 0.22
… from new, cost API, exact compare and plain chunks (#230, #231, #234, #236)

- optimal_replacement_intervals(allowed=...) chooses from calendar
  intervals, never replacing (inf) among them; 0.12's notes corrected
  (#230).
- total_cost and expected_cost discount from new by parts on adaptive
  Gauss-Kronrod pieces; an endless horizon with a discount rate, many
  horizons at once, and a warning when the rate discounts the costs to
  almost nothing, as a rate per year with models in hours does (#231).
- cost() and the run's cost result carry the acquisition cost when it is
  all that is priced; compare's cost counts it (#234).
- compare is exact where both designs' expected values are (MTTFs, or
  mission availability and expected cost), simulated otherwise or on
  request (method="simulate", control_variate=False); its route says so,
  and a diagram too meshed to work out is left to the simulation (#236).
- simulate_chunk and availability_from_chunks take control_variate=False
  and conditional=False, so chunks reproduce a plain run (#236).
- Warnings point at the caller's own line (outside_level).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
…t serialise, unavailability over time (#232, #233, #235, #237)

#232: the names 0.12 removed are refused naming their replacement
(refuse_removed_names), fussel_vesely names fussell_vesely; typos in node
names, parts, trains, groups, roles and parameters get the closest name;
a junction named as a component is called one; outage logs say which
outage is wrong and how; gates in a loop say so; a single CCFGroup is a
list of one; a PerfectReliability instance is the class (and a junction's
spec without repairs saves); seeds are checked where given; spares_stock
with crews says it has no simulation; warnings point outside the package
(outside_level); no message reads "'a''s".

#233: costs, intervals and times given as text or True/False are refused;
surpyval's distribution itself is refused for a model of it; a repairable
component given a life alone is told it needs its repairs;
NonRepairable(life, "instant") replaces in no time; the exact mean of
StandbyModel, LoadSharingModel and DegradingNode warns of the options it
ignores (FutureWarning, 0.14 refuses) and takes method="simulate";
percentiles below 1 warn of their 0-100 scale; a part listing a component
twice, or one in a part and nodes, is refused.

#235: to_dict() on every result, AnalysisRoute, Pairs, Timeline(s), ready
for json.dumps; summary reprs for SparesDemand, ControlVariate and
ConditionalRun; CapacityDistribution.mean is a property (calling it warns
until 0.14); an infinite risk reduction worth no longer warns.

#237: an inspection offset within a billionth of the interval is no
offset; FaultTree.from_rbd keeps a common-cause member the logic makes
irrelevant; the staggered-tests convention is documented; new
point_unavailability and mission_unavailability work out a small
unavailability to its own precision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
A diagram's fault tree keeps a common-cause member its logic makes
irrelevant, where the tests expected a refusal; gates in a loop say so; an
outage that ends before it starts says that; a life alone given as a
repairable component is told it needs its repairs; the advice for a mean
with no exact value is mean(method='simulate', ...); and a component in
both nodes and a part of spares_demand is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
…ads well (#238)

The saving guide no longer says a simulated standby or load-sharing node is
fitted to simulated lifetimes; the spares guide's mean and std are
properties; a common-cause member fails with probability Q to first order,
not exactly (common-cause guide, concepts, MGL); and the Raises sections
of sf_given_state, remaining_life and mean_residual_life take a number as
an age.

Junctions, pooled spares, discounting, whole trains, plans that keep both
risks, common causes in fault trees and the unavailability over time are
in the README's list, the docs' home table and the guide's index; the
glossary gains junction, the Greeks, differential and Barlow-Proschan
importance, discount rate, fill rate, lead time, producer's and consumer's
risk, and unavailability.

help(): the package's docstring says where to start; the API reference's
cross-references read as names at run time (utils.docs, applied to the
exported API), the web pages keeping their links; the sensitivity
measures name their Greeks; availability's longest options point to the
guide. test_help.py checks it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
…al 0.23 minimum)

The CHANGELOG keeps both sides' entries; serialisation takes is_mixture
and leaves lfp_extras, which surpyval 0.23 made unnecessary; CLAUDE.md's
surpyval workarounds list what remains (the mixture's quantile).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
…sults' properties and methods (#227, #235)

test_performance_equivalence forced the slow path by patching
repairable_rbd.inverse_sampler, which #227 replaced with stream_sampler
(a mixture's quantile on top): it patches that now. The repairable guide
says a result's values are properties and its intervals methods, and that
to_dict() gives it as plain data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
A tested component with an exponential life, whose tests or repairs take
time, follows its state from new until it is the long run's, which the
chain took to be within 1e-15 of it. The iterated state settles a few
roundings from the stationary one, how few depending on the platform's
numerical libraries: on CI's Python 3.12 and 3.13 it never came that
close, and its values over time past what the grid can follow were
refused as not settled. The bound is now SETTLED (1e-12), as the walk's
for any other life; a test nudges the long run by a few roundings. A
refusal with no node names "A tested unit" rather than "Component None".

Two tests that failed in CI after the merge of dev:
- the discounted cost from new is checked to the precision of the
  1,000-step availability curve it is worked out from (rel 1e-6);
- the saving guide's engine comment no longer opens with a literal, which
  the docs test checked against the engine of a job without numba.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
…endations-2lmh5a

Fixes from the 0.12 persona check: correctness, API, messages, results, unavailability over time, docs (#213)
#244)

parameter_sensitivity's levers had no public API: a report or an app that
named them, showed their values or treated a calendar lever apart reached
into repyability.rbd._sensitivity (levers, _calendar_lever, _as_spec).

- RepairableRBD.levers() and NonRepairableRBD.levers() list what
  parameter_sensitivity moves, in the order it reports them, as Lever
  results: key and name, value, bounds (a model parameter's surpyval's,
  an option's as its constructor checks it), discrete, and calendar.
- with_levers({lever: value}) builds the diagram with levers moved as the
  sensitivities move them, by Lever or (key, name). It refuses a lever the
  diagram does not have (with the closest name), a fraction of a count,
  text and booleans, and a value out of range (with the lever's range).
- The private names callers used stay as they were for 0.13.
- The non-repairable DIM's proportional changes take the levers' values,
  where _parameter_values listed the parameters by hand.
- Docs: the greeks guide, the API page, the CHANGELOG and CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
claude and others added 16 commits October 4, 2026 22:15
surpyval's develop branch gives FixedEventProbability a quantile function,
so inverse_sampler, which takes a model that samples by inverse transform
with one global uniform a draw, now takes it there, and its draws are
surpyval's own, to the last bit. The test that listed it among the models
the sampler declines failed in the upstream job; it now holds the sampler
to its contract on either surpyval: declined without a quantile function,
and reproducing surpyval's draws with one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
…endations-2lmh5a

Public levers: levers(), Lever and with_levers on both diagram classes (#244)
The Python engine's second implementation of independent components'
lives and repairs, which drew their histories from their streams, is
gone: the event loop records them as it runs, the same histories bit for
bit, and TimelineSimulation.method is always "event loop". Recording is
cheaper (each component's changes kept together as they happen, planned
changes by their places), and packaging the records into a block takes a
third of its old time.

A component that fails at once and is repaired at once is refused when
the diagram is built: the loop would change its state without end.

CLAUDE.md gains #204's rule for clean, simple implementations and loses
the drawn histories' notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
The compiled loop wrote a component's events twice: once for the
system's own components in _simulate, once for a nested RBD's in
_advance, with a rule that a change to one goes into the other. One loop
in _simulate now takes every level's events: the system's to the end,
and a nested RBD's to its next change when that is wanted where it is a
component, its level stacked on the one waiting for it. Each level
starts when its node does. A component's first and next events, a
standby group's step and the repair crews are each written once.

Results are the same, bit for bit. A system with nested RBDs runs
faster (a three-level system in about half the time), since a nested
RBD's change no longer costs a call carrying the run's arrays; the
others run as before. Sharing helpers called per event, or a function
per simulation, cost 3 to 7 times the loop: numba copies and
reference-counts every array such a call carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
surpyval's development branch gives FixedEventProbability a quantile
function (0 or inf) while its random() still draws the 0/1 event
indicators its fit takes. The sampler takes the quantile function and
draws lifetimes: the test now checks they are 0 exactly where surpyval's
draw from the same uniform is the event, rather than equal to the
indicators. It passes on surpyval 0.23 and develop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
The compiled radix sort and merge (_time_order) and their tuning constant
go. The +1 and -1 changes of state are netted by one sort of integers,
each time's bits above the change's sign; the capacity's changes, and the
changes in how many systems can carry an unlimited amount, by one sort of
(time, change) pairs. Results are the same, bit for bit; test_time_order
checks each against its definition written out plainly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
The rest of the change whose first commit removed _time_order: the +1
and -1 changes of state are netted by one sort of integers, each time's
bits above the change's sign; the capacity's changes, and the changes in
how many systems can carry an unlimited amount, by one sort of (time,
change) pairs. Results are the same, bit for bit; test_time_order checks
each against its definition written out plainly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
bdd.search and shannon.replay/replay_gradient are each written once and
run as Python or compiled by numba as written (_bdd_kernel), where numba
is installed and a core is large. The compiled search's separate design,
its packing of states into integers and its fall back to Python for a
wide frontier go; the replay writes into room its caller gives (lists in
Python, arrays reused across columns when compiled), with the reverse
pass on the values the forward one left.

Plans are the same step for step and values bit for bit: bdd_records.json
holds the plans, step counts and replay values the old code made (and the
new code re-records the file byte for byte), and test_bdd_records checks
both modes against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
Uniform k of simulation r from a stream is now the k-th of numpy's Philox
generator with the stream's key (from the run's entropy and the stream's
name) and the counter (0, r, 0, 0): a function of those alone. _philox
writes Philox4x64-10 once, run on numpy's arrays or compiled by numba a
counter at a time (_philox_kernel); test_streams checks both against
numpy's Philox, and that the layout (block widths, chunk rows) changes
nothing a run gives. compare and the control variate no longer give two
systems' streams the same widths, and shards() no longer passes them.

This changes seeded results: seeded_event_loop.json is re-recorded, the
docs' quoted numbers and the prose built on them are updated, and
engines.API is 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
--doctest-modules imports every module, and _philox_kernel imports numba:
the plain CI jobs, which have no numba, failed to collect it. Ignore it
there, as _kernel and _bdd_kernel are, in place of the removed
_time_order, and leave it out of coverage likewise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
The docstrings' seeded examples quote the new streams' numbers. A
simulated timelines test compared a history's change times as pairs: it
held only while the first history had none, as the old streams gave it.
A test that random prices add spread compared two runs' sample standard
deviations, which chance can order either way: it now checks the spread
the prices add to each simulation, with the failures the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
test_runs_with_groups_are_reproducible_and_compiled patched
_compiled.available to True for an analysis_routes check, then ran a
compare under it. Since the streams' Philox runs compiled wherever numba
is said to be available (#209), that compare tried to import numba on
CI's jobs without it. The patch now covers the route check alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS
Clean implementations (#204): timelines from the event loop (#205), one compiled event loop (#206), one time order (#208), one decision-diagram search and replay (#207), counter-based streams (#209)
…252) (#256)

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.


Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS

Co-authored-by: Claude <noreply@anthropic.com>
…257)

* 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS

---------

Co-authored-by: Claude <noreply@anthropic.com>
… (#258)

* 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS

* Capacity: black (#246)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUGaeQXKxjjiQ1ULT1ApXS

---------

Co-authored-by: Claude <noreply@anthropic.com>
@derrynknife
derrynknife merged commit c7b257b into master Oct 9, 2026
20 checks passed
@derrynknife derrynknife mentioned this pull request Oct 9, 2026
7 of 10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment