PyBNF algorithms (pybnf.algorithms)¶
The pybnf.algorithms package holds PyBNF’s fitting and sampling
algorithms. Every fit type is re-exported from the package facade
(pybnf.algorithms.<Name>), but the implementations live in the submodules
documented below: shared execution primitives in core
and base, gradient-free and gradient-based optimizers
under optimizers, Bayesian samplers under samplers, and the model-checking
utility in model_check.
Core execution¶
Core execution primitives shared by every algorithm.
Holds the Result / FailedSimulation containers, the Job runner,
the JobGroup smoothing/model-parallel folding family, run_job,
result_from_completed, and the dask as_completed import.
This module is the patched seam (ADR-0001). Tests and the slow
integration_harness patch run_job / as_completed / Job here,
and the run loop resolves them as core.<name> so that “patch where it is
defined” stays honest as the algorithm classes move out of the package
facade in later commits.
Import direction is one-way: this module imports only leaf modules
(data / pset / printing) and never anything from the
algorithms package facade or any algorithm leaf.
- class pybnf.algorithms.core.FailedSimulation(paramset, name, fail_type, einfo=(None, None, None))[source]¶
- normalize(settings)[source]¶
Normalizes the Data object in this result, according to settings :param settings: Config value for ‘normalization’: a string representing the normalization type, a dict mapping exp files to normalization type, or None :return:
- postprocess_data(settings)[source]¶
Postprocess the Data objects in this result with a user-defined Python script :param settings: A dict that maps a tuple (model, suffix) to a Python filename to load. That file is expected to contain the definition for the function postprocess(data), which takes a Data object and returns a processed data object :return: None
- class pybnf.algorithms.core.HybridJobGroup(job_id, replica_subjob_ids)[source]¶
A JobGroup to handle combined smoothing and model-level parallelism.
- class pybnf.algorithms.core.Job(models, params, job_id, output_dir, timeout, calc_future, norm_settings, postproc_settings, delete_folder=False, replicate_index=0, stochastic_seed_policy='auto', model_slice=None)[source]¶
Container for information necessary to perform a single evaluation in the fitting algorithm
- class pybnf.algorithms.core.JobGroup(job_id, subjob_ids)[source]¶
Represents a group of jobs that are identical replicates to be averaged together for smoothing
- class pybnf.algorithms.core.MultimodelJobGroup(job_id, subjob_ids)[source]¶
A JobGroup to handle model-level parallelism
- class pybnf.algorithms.core.Result(paramset, simdata, name)[source]¶
Container for the results of a single evaluation in the fitting algorithm
- add_result(other)[source]¶
Add simulation data of other models from another Result object into this Result object :param other: The other Result object :return:
- normalize(settings)[source]¶
Normalizes the Data object in this result, according to settings :param settings: Config value for ‘normalization’: a string representing the normalization type, a dict mapping exp files to normalization type, or None :return:
- postprocess_data(settings)[source]¶
Postprocess the Data objects in this result with a user-defined Python script :param settings: A dict that maps a tuple (model, suffix) to a Python filename to load. That file is expected to contain the definition for the function postprocess(data), which takes a Data object and returns a processed data object :return: None
- pybnf.algorithms.core.result_from_completed(future, result, params, job_id)[source]¶
Translate one completed item from
as_completed(futures, with_results=True, raise_errors=False)into a Result.- With those flags, dask hands back per future:
the job’s return value (a Result / FailedSimulation) for a finished job,
the worker’s
(type, exception, traceback)tuple for an errored job, anda
CancelledErrorfor a cancelled job.
A failed job becomes a
FailedSimulationso the run continues, except a user-targetedPybnfErroris re-raised (it would fail every job, so failing fast is better than burning the whole fit). ACancelledErroris returned unchanged for the caller to treat as fatal, and anything unrecognized is treated as a failed simulation rather than crashing the run.Future.statusis part of dask’s public Future API, so this relies on no dask internals. See lanl/PyBNF#388 for the history (the previous approach subclassed as_completed and overrode a private method that dask later renamed).
- pybnf.algorithms.core.run_job(j, debug=False, failed_logs_dir='', models=None, calc=None)[source]¶
Runs the Job j. This function is passed to Dask instead of j.run_simulation because if you pass j.run_simulation, Dask leaks memory associated with j.
models/calcare the worker-resolved model_list and ObjectiveCalculator thatAlgorithm.runscattered once per fit and hands toclient.submitas direct kwargs, so dask substitutes the Futures with their concrete values here on the worker (see_ResolvedFuturefor why a Future stashed as a Job attribute cannot be used instead). When provided they are rebound onto the Job; when omitted – the in-process paths that callrun_jobdirectly (_rerun_best_fit_to_save_data,ModelCheck, the folder-free test harnesses) – the Job’s ownmodels/calc_futureattributes are used as-is.
The Algorithm base class and the module-level helpers it relies on.
Extracted from algorithms.py (M1 Step 2). The run loop resolves the three
monkeypatched names as core.run_job / core.as_completed / core.Job
(ADR-0001), so it imports the core module object rather than binding those
names locally; the never-patched data classes are imported by name.
- class pybnf.algorithms.base.Algorithm(config)[source]¶
Base class for every PyBNF fit type (“method”); defines the run-loop contract.
The contract (ADR-0007). A method plugs into the framework by subclassing
Algorithmand implementing exactly two abstract methods:start_run()() -> list[PSet]— the initial parameter sets to evaluate.got_result()(Result) -> list[PSet] | 'STOP'— given one completed result, the next parameter sets to evaluate, or the string'STOP'to end the run.
plus registering itself with a config schema via
@register_fit_type(..., schema=...)(ADR-0002, ADR-0005). The.nameof any returned PSet, if set, MUST be unique across the run (it determines the simulation folder name; uniqueness is not checked elsewhere).Everything else the framework needs has an overridable default (
reset,add_iterations,cleanup,get_backup_every,should_pickle). The run loop itself — job submission,as_completeddraining, backup cadence, best-fit save, sim-dir teardown — lives inrun()and is shared by every method, not replaceable: a method customizes what to propose, never the outer loop.- add_iterations(n)[source]¶
Adds n additional iterations to the algorithm. May be overridden in subclasses that don’t use self.max_iterations to track the iteration count
- backup(pending_psets=())[source]¶
Create a backup of this algorithm object that can be reloaded later to resume the run
- Parameters:
pending_psets – Iterable of PSets that are currently submitted as jobs, and will need to get re-submitted when resuming the algorithm
- Returns:
- get_backup_every()[source]¶
Returns a number telling after how many individual simulation returns should we back up the algorithm. Makes a good guess, but could be overridden in a subclass
- abstractmethod got_result(res)[source]¶
Called by the scheduler when a simulation is completed, with the pset that was run, and the resulting simulation data
- Parameters:
res (Result) – result from the completed simulation
- Returns:
List of PSet(s) to be run next or ‘STOP’ string.
- make_job(params)[source]¶
Creates a new Job using the specified params, and additional specifications that are already saved in the Algorithm object If smoothing or model-level parallelism is turned on, makes grouped subjobs.
- Parameters:
params (PSet)
- Returns:
list of Jobs
- output_results(name='', no_move=False)[source]¶
Tells the Trajectory to output a log file now with the current best fits.
This should be called periodically by each Algorithm subclass, and is called by the Algorithm class at the end of the simulation. :return: :param name: Custom string to add to the saved filename. If omitted, we just use a running counter of the number of times we’ve outputted. :param no_move: If True, overrides the config setting delete_old_files=2, and does not move the result to overwrite sorted_params.txt :type name: str
- random_latin_hypercube_psets(n)[source]¶
Generates n random PSets with a latin hypercube distribution. Variables with bounded initialization distributions follow the Latin hypercube; the others are randomized independently from their initializer.
- Parameters:
n – Number of psets to generate
- Returns:
- random_pset()[source]¶
Generates a random PSet based on the distributions and bounds for each parameter specified in the configuration
- Returns:
- reset(bootstrap)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- static should_pickle(k)[source]¶
Checks to see if key ‘k’ should be included in pickling. Currently allows all entries in instance dictionary except for ‘trajectory’
- Parameters:
k
- Returns:
- spawn_chain_rngs(n)[source]¶
Return
nindependent Generators, one per parallel chain/replica.Spawning is deterministic in
(seed, n), so chainialways draws from the same stream regardless of the order in which dask returns its results. This is what makes the parallel samplers reproducible under nondeterministic scheduling – a single shared stream would interleave by completion order and so would not reproduce run to run.
- abstractmethod start_run()[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
Optimizers¶
Shared bases¶
Shared scaffolding for the start-point local optimizers (Powell + CMA-ES, #403).
Both Powell (conjugate-direction) and CMA-ES are derivative-free, black-box local
optimizers that begin from a single point and search in sampling space u –
log10 for log-scaled parameters, linear otherwise. That is the same space the
prior and proposal arithmetic already operate in (FreeParameter._scale,
ADR-0003/0010), so log parameters are optimized geometrically (a multiplicative
step is an additive u step) exactly as Simplex does its log-space arithmetic.
StartPointOptimizer factors out the two pieces of plumbing they share:
start-point resolution – the injected refiner start point (set by
pybnf._refine_best_fitunderSTART_POINT_KEY) when refining; the single-valuevar/logvarspecs of a standalone point-start fit (the same start point Simplex parses, ADR-0015); or, for astart_from_boxoptimizer (CMA-ES) given boundeduniform_var/loguniform_varpriors, the box center in sampling spaceu– its global-start mode (#404, ADR-0017);the
u<->PSetconversion, which maps each coordinate back to a stored value and reflects it into the box viaFreeParameter.set_value()(a no-op for the unboundedvar/logvarof a point-start fit; active when refining or globally searching a bounded fit’s parameters).
Simplex predates this and keeps its own byte-identical start-point parsing; the
two new optimizers are the ≥2-member event (ADR-0009) that earns the shared base.
These methods plug into the run loop through start_run / got_result only
(ADR-0007); no method overrides run().
- class pybnf.algorithms.optimizers.local_base.StartPointOptimizer(config)[source]¶
Base for the start-point local optimizers. Subclasses implement
start_run/got_resultand setSTART_POINT_KEY.- START_POINT_KEY = None¶
The internal config key the refiner start point is injected under (mirrors
SimplexAlgorithm’s'simplex_start_point'). Set by each subclass;pybnf._refine_best_fitwrites the best fit here so refinement starts from it instead of parsing the (refiner-irrelevant) variable specs.
Shared scaffolding for the gradient-based local optimizers (TRF/LM + L-BFGS-B, #386).
The metaheuristic fit types (de, pso, ss, cmaes, …) only ever ask each evaluated
PSet for its scalar objective value. A gradient optimizer instead consumes
the residual vector + residual-Jacobian (TRF / Levenberg–Marquardt) or the scalar
gradient (L-BFGS-B) that #385 assembles from bngsim’s forward output-sensitivity
tensor. GradientOptimizer factors out everything a new gradient method
needs so a leaf (trf.py, lbfgs.py) implements only its step math –
mirroring how StartPointOptimizer factors the start-point / u ↔ PSet
plumbing out of Powell and CMA-ES.
What this base provides¶
The edition + capability gate (
_gate_gradient_supported()). Gradient fitting consumes the edition-2 surface (bind-by-id routing ADR-0034, thenoise_model/ measurement layer) and bngsim’s forward sensitivities, so the fit is refused – with a clear message pointing at a metaheuristicfit_type– on a legacy (edition < 2) config, a non-bngsim model, or a bngsim build without theoutput_sensitivitiesfeature. Never a silent finite-difference fallback.The gradient path activation (
_setup_gradient_path()). Builds each experiment’sExperimentRoutingonce (it depends only on model structure, conditions, and free-parameter ids – never on the parameter values, #449) andapply_routings the union request onto every model, so each simulatedDatacarries its sensitivity tensor. Run before the model scatter (fromstart_run); the request rides the pickle to the workers (BngsimModel.__getstate__keeps it, rebuilding only the engine).Master-side scoring (
requires_master_scoring = True). The worker scoring path nullsres.simdataafter scoring (#385/#388), which would discard the sensitivity tensors; the flag makesAlgorithm.runkeep scoring on the master so everyResultreturns with its full simdata forgradient_at().The per-evaluation assembly (
gradient_at()). Aligns a Result’ssimdatawithexp_dataand the prebuilt routings, and returns the assembledGradientResult(objective gradient + residual Jacobian, in sampling spaceu), folding in any constraint-penalty gradient.The ``u``-space box (
_u_bounds()). The finite reflecting box for bounded (uniform_var/loguniform_var) priors,±inffor an unbounded point start – the bounds the leaf’s step projects/reflects into.
The search runs in sampling space u (StartPointOptimizer), and #385 already
delivers the gradient transformed into u once (ADR-0029), so a leaf never
re-transforms. Leaves own their start_run / got_result state machine and
must be picklable for backup/resume, exactly like Powell and CMA-ES (ADR-0007).
- pybnf.algorithms.optimizers.gradient_base.DONE = <object object>¶
Sentinel a
GradientRunner’sgotreturns when that start has converged or otherwise terminated (no further evaluation to propose). It is only ever returned synchronously intoGradientOptimizer.got_result()and never stored in pickled state, so a plain module-level object (identity-checked) is enough.
- class pybnf.algorithms.optimizers.gradient_base.GradientOptimizer(config, refine=False)[source]¶
Base for the gradient-based local optimizers (#386).
A leaf subclass supplies only its per-start step math as a
GradientRunner(Levenberg–Marquardt fortrf, L-BFGS-B forlbfgs) via_make_runner(); this base owns the wholestart_run/got_resultorchestration – seeding the runners, theu``<->PSet plumbing, :meth:`gradient_at` assembly, name-tagging / routing, reporting, and the multi-start ``STOPcoordination. The leaf must setSTART_POINT_KEYlike anyStartPointOptimizer, plus_method_labeland_start_banner()for its messages.Local multi-start (#386). A box-start gradient fit runs
Nindependent starts concurrently (Nreusespopulation_size) – start 0 from the box center (preserving the deterministic single-start behavior), the rest from Latin-hypercube samples across the prior box – and keeps the global best. The async run loop already supports this without change:start_runreturns a list ofNinitial PSets, eachgot_resultadvances just the start that owns the returned Result (routed by PSet name throughpending) and returns that start’s next PSet (or[]once it converges), and'STOP'fires only when the last start finishes. Every evaluated PSet across all starts lands in the trajectory (add_to_trajectoryruns beforegot_result), sotrajectory.best_fit()is the global best for free – each runner only tracks its own best for its own convergence test.- add_iterations(n)[source]¶
Extend every start’s per-start iteration budget by
n(the-rresume path). Runners already exist when this is called on a resumed run (they ride the backup pickle), so bump each alongside the template budget.
- got_result(res)[source]¶
Route a completed Result to the start that owns it, advance just that start’s runner, and return its next PSet –
[]once it converges (other starts keep going), or'STOP'only when the last live start finishes.
- gradient_at(res)[source]¶
Assemble the objective gradient + residual-Jacobian at
res’s point.resis a master-scored Result, sores.simdatacarries each experiment’s forward-sensitivity tensor. Aligns it withexp_dataover the scored(model, suffix)pairs (the same intersection the objective scores), attaches each one’s prebuilt routing, and returns the assembledGradientResult– residual / Jacobian / scalar gradient in sampling spaceu(#385 transformed it once; the optimizer never re-transforms). Any constraint-penalty gradient is added to the scalargradient(and clearsleast_squares_exact, since a penalty is not a sum of squares).The free-parameter list (column order + current values) is read straight off the evaluated PSet, so the
d theta/d uscale factors are taken at the point actually simulated. Converts aGradientNotSupported(an objective the assembly cannot differentiate) into a clear, fail-fastPybnfErrorpointing at a metaheuristic fit_type.
- requires_master_scoring = True¶
Keep objective scoring on the master so every Result returns with its simdata (the sensitivity tensors the gradient assembly reads); see
Algorithm.run. Without this the worker path nullsres.simdata.
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- class pybnf.algorithms.optimizers.gradient_base.GradientRunner(u0, lower, upper, max_iterations)[source]¶
Headless, picklable per-start step machine in sampling space
u(#386).A gradient leaf’s step math, factored out of the optimizer so a single fit can run
Nof them concurrently – local multi-start, the diversity a purely local gradient method otherwise lacks (it only ever descends into the one basin its start lands in). A runner owns one start’s entire mutable state – the iterate, the curvature / trust-region model, the reflecting box, the tunables – and is purenumpy: it knows nothing aboutPSets, the objective, routing, backup, or dask.GradientOptimizerdrives it:u0 = runner.start() # the first point to evaluate nxt = runner.got(u_point, score, grad) # consume one completed evaluation
where
u_pointis the realized (box-projected)u-vector of the completed evaluation,scoreits objective value, andgradthe assembledGradientResultat it;gotreturns the nextuto evaluate, or theDONEsentinel. Because a runner holds only plainfloat/ndarray/list, the optimizer that owns the list of runners pickles for backup/resume exactly like the single-start machine did (ADR-0007). Being backend-free, a runner is also unit-testable offline by feeding it scores + gradients from an analytic function (no bngsim) – how the step math is validated against a scipy oracle and how the multi-start win is demonstrated.A leaf subclass (
trf.py/lbfgs.py) sets up its own model state in__init__and implementsgot(); the orchestrator only ever callsstart(),got(), and readsiteration/fval/stop_reason/progress_detail()for reporting.
Metaheuristic optimizers¶
ParticleSwarm optimizer (the pso fit type).
Extracted from the algorithms package (M1 Step 4). Subclasses Algorithm and inherits its run loop + execution seam; it makes no core.* call of its own.
- class pybnf.algorithms.optimizers.particle_swarm.PSOConfig(*, particle_weight: float = 0.7, adaptive_n_max: float = 30, adaptive_n_stop: float = inf, adaptive_abs_tol: float = 0.0, adaptive_rel_tol: float = 0.0, cognitive: float = 1.5, social: float = 1.5, v_stop: float = 0.0)[source]¶
Particle-swarm config fields, co-located with the method (ADR-0002, ADR-0006). These defaults previously lived in
config_schema.GlobalConfig; moving them here makespso’s config knowledge travel with its algorithm class. The values are byte-identical to the old global defaults – the int-literaladaptive_n_max = 30stays an int (pydantic does not validate defaults), matching the golden-config net.particle_weight_finalis intentionally NOT here: it is not a default but a runtime fallback (set toparticle_weightwhen absent, in__init__below), so it stays a pass-through extra.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class pybnf.algorithms.optimizers.particle_swarm.ParticleSwarm(config)[source]¶
Implements particle swarm optimization.
The implementation roughly follows Moraes et al 2015, although is reorganized to better suit PyBNF’s format. Note the global convergence criterion discussed in that paper is not used (would require too long a computation), and instead uses ????
- add_iterations(n)[source]¶
Adds n additional iterations to the algorithm. May be overridden in subclasses that don’t use self.max_iterations to track the iteration count
- got_result(res)[source]¶
Updates particle velocity and position after a simulation completes.
- Parameters:
res – Result object containing the run PSet and the resulting Data.
- Returns:
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
The Differential Evolution optimizer family (de and ade fit types).
DifferentialEvolutionBase is the shared base; DifferentialEvolution (de) and
AsynchronousDifferentialEvolution (ade) subclass it. Extracted byte-identical
(M1 Step 4). The family makes no core.* call of its own — the run loop and the
execution seam are inherited from Algorithm.
- class pybnf.algorithms.optimizers.differential_evolution.AsynchronousDifferentialEvolution(config)[source]¶
Implements a simple asynchronous differential evolution algorithm.
Contains no islands or migrations. Instead, each time a PSet finishes, proposes a new PSet at the same index using the standard DE formula and whatever the current population happens to be at the time.
- got_result(res)[source]¶
Called when a simulation run finishes
- Parameters:
res – Result object
- Returns:
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- start_run()[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
- class pybnf.algorithms.optimizers.differential_evolution.DEFamilyConfig(*, mutation_rate: float = 0.5, mutation_factor: float = 0.5, stop_tolerance: float = 0.002, de_strategy: str = 'rand1')[source]¶
Config fields shared by the whole DE family, co-located with
DifferentialEvolutionBase(ADR-0002, ADR-0006) – exactly the keys that base__init__reads.aderegisters against this base directly (it adds no config keys);deextends it (DifferentialEvolutionConfig). Values are byte-identical to the oldGlobalConfigdefaults.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class pybnf.algorithms.optimizers.differential_evolution.DifferentialEvolution(config)[source]¶
Implements the parallelized, island-based differential evolution algorithm described in Penas et al 2015.
In some cases, I had to make my own decisions for specifics I couldn’t find in the original paper. Namely: At each migration, a user-defined number of individuals are migrated from each island. For each individual, a random index is chosen; the same index for all islands. A random permutation is used to redistribute individuals with that index to different islands.
Each island performs its migration individually, on the first callback when all islands are ready for that migration. It receives individuals from the migration iteration, regardless of what the current iteration is. This can sometimes lead to wasted effort. For example, suppose migration is set to occur at iteration 40, but island 1 has reached iteration 42 by the time all islands reach 40. Individual j on island 1 after iteration 42 gets replaced with individual j on island X after iteration 40. Some other island Y receives individual j on island 1 after iteration 40.
- got_result(res)[source]¶
Called when a simulation run finishes
This is not thread safe - the Scheduler must ensure only one process at a time enters this function. (or, I should rewrite this function to make it thread safe)
- Parameters:
res – Result object
- Returns:
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- start_run()[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
- class pybnf.algorithms.optimizers.differential_evolution.DifferentialEvolutionBase(config)[source]¶
- got_result(res)[source]¶
Called by the scheduler when a simulation is completed, with the pset that was run, and the resulting simulation data
- Parameters:
res (Result) – result from the completed simulation
- Returns:
List of PSet(s) to be run next or ‘STOP’ string.
- new_individual(individuals, base_index=None)[source]¶
Create a new individual for the specified island, according to the set strategy
- Parameters:
base_index – The index to use for the new individual, or None for a random index.
- Returns:
- start_run()[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
- class pybnf.algorithms.optimizers.differential_evolution.DifferentialEvolutionConfig(*, mutation_rate: float = 0.5, mutation_factor: float = 0.5, stop_tolerance: float = 0.002, de_strategy: str = 'rand1', islands: int = 1, migrate_every: int = 20, num_to_migrate: int = 3)[source]¶
de-specific config: the island/migration fields only synchronous DE reads (adeis async and ignores them). Demonstrates the shared-base pattern the MCMC family reuses (ADR-0006).- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
ScatterSearch optimizer (the ss fit type).
Extracted byte-identical (M1 Step 4). Subclasses Algorithm and inherits its run loop + execution seam; it makes no core.* call of its own.
- class pybnf.algorithms.optimizers.scatter_search.ScatterSearch(config)[source]¶
Implements ScatterSearch as described in the introduction of Penas et al 2017 (but not the fancy parallelized version from that paper). Uses the individual combination method described in Egea et al 2009
- get_backup_every()[source]¶
Overrides base method because Scatter Search runs n*(n-1) PSets per iteration.
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- start_run()[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
- class pybnf.algorithms.optimizers.scatter_search.ScatterSearchConfig(*, local_min_limit: int = 5)[source]¶
Scatter-search config fields, co-located with the method (ADR-0002, ADR-0006).
local_min_limitis the only defaulted scatter key (it was the lone# --- scatter search ---entry inGlobalConfig);init_sizeandreserve_sizeare NOT here – like PSO’sparticle_weight_finalthey are runtime-defaulted in__init__(init_size→10*len(variables),reserve_size→max_iterationswhen absent), so they stay pass-through extras. Value byte-identical to the old global default.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
SimplexAlgorithm — the parallelized Nelder-Mead simplex (the sim fit type).
Also used as the refinement step after another fit. Extracted byte-identical
(M1 Step 4). Subclasses Algorithm and sets _is_simplex = True so the
inherited run() teardown always clears the Simulations directory at end-of-fit.
The start value is mapped out of sampling space by the parameter’s own scale
(FreeParameter.from_sampling_space), so log10 and natural-log vars both work.
- class pybnf.algorithms.optimizers.simplex.SimplexAlgorithm(config, refine=False)[source]¶
Implements a parallelized version of the Simplex local search algorithm, as described in Lee and Wiswall 2007, Computational Economics
- START_POINT_KEY = 'simplex_start_point'¶
Internal config key the refiner start point is injected under, read by __init__ below and written by pybnf._refine_best_fit (the registry-driven refiner dispatch, #403/ADR-0015). Simplex predates StartPointOptimizer and keeps its own byte-identical start-point parsing, so this is declared here.
- a_plus_b_times_c_minus_d(a, b, c, d, v)[source]¶
Performs the calculation a + b*(c-d), where a, c, and d are assumed to be in log space if v is in log space, and the final result respects the box constraints on v.
- Parameters:
a
b
c
d
v (FreeParameter)
- Returns:
- ab_plus_cd(a, b, c, d, v)[source]¶
Performs the calculation ab + cd where b and d are assumed to be in log space if v is in log space, and the final result respects the box constraints on v :param a: :param b: :param c: :param d: :param v: :type v: FreeParameter :return:
- get_sums()[source]¶
Simplex helper function Returns a dict mapping parameter name p to the sum of the parameter value over the entire current simplex :return: dict
- got_result(res)[source]¶
Called by the scheduler when a simulation is completed, with the pset that was run, and the resulting simulation data
- Parameters:
res (Result) – result from the completed simulation
- Returns:
List of PSet(s) to be run next or ‘STOP’ string.
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- start_run()[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
- class pybnf.algorithms.optimizers.simplex.SimplexConfig(*, simplex_step: float = 1.0, simplex_reflection: float = 1.0, simplex_expansion: float = 1.0, simplex_contraction: float = 0.5, simplex_shrink: float = 0.5, simplex_stop_tol: float = 0.0)[source]¶
Simplex (Nelder-Mead) config fields, co-located with the method (ADR-0002, ADR-0006) – the six defaulted
simplex_*knobs__init__reads unconditionally. NOT here (stay pass-through extras):simplex_max_iterationsandsimplex_log_stepare runtime-guarded (if 'X' in config.config) so they default at runtime, andsimplex_start_pointis an internal key the algorithm sets itself. The Simplexvar/logvar-only free-parameter rule stays inconfig.py(cross-config knowledge, ADR-0006 #5). Values are byte-identical to the oldGlobalConfigdefaults.The refine->simplex cross-fit_type reach (ADR-0006/0013):
_refine_best_fitruns Simplex on a non-simplex config and readssimplex_*. Under narrowing these keys are in asimfit’s config via this schema, and in any other fit withrefine == 1via the whole-schema overlay_build_configapplies (the_REFINER_SCHEMAseam inconfig.py).- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Simulated Annealing optimizer (the sa fit type).
A clean rewrite-to-spec (M2.2, ADR-0008). sa was historically built on the
Bayesian sampler base and evaluated the posterior (prior + likelihood) in its
Metropolis accept – a silent MAP estimator for normal/lognormal priors, unlike
every other PyBNF optimizer. It is now a true optimizer: it minimizes the raw
objective, using the prior only for the initial random draw (like de/pso/ss/sim).
Box constraints are enforced by proposal reflection (FreeParameter.add) plus
the parameter bounds, exactly as before.
The algorithm runs population_size independent annealing chains in parallel.
Each chain makes a fixed-magnitude random-walk proposal and a Metropolis accept at
its own inverse temperature beta; an accepted uphill (objective-increasing)
move cools the chain (beta += cooling). A chain finishes when its beta
reaches beta_max or it runs max_iterations; the run stops once every chain
has finished. sa is deprecated (it warns at dispatch) but still runs.
- class pybnf.algorithms.optimizers.simulated_annealing.SimulatedAnnealing(config)[source]¶
Simulated annealing as a true optimizer (ADR-0008): minimizes the raw objective across
population_sizeindependent annealing chains.- add_iterations(n)[source]¶
Extend the budget by
niterations, restarting any chain that had already finished atmax_iterations(mirrors the resume behavior of the other iterative methods); chains that finished by reachingbeta_maxstay finished.
- choose_new_pset(oldpset)[source]¶
Perturb
oldpsetby a fixed-magnitude (step_size) random-walk move. The direction is isotropic; any component that would leave the box is reflected back inside (FreeParameter.adddefaults to reflect=True), so the symmetric proposal keeps the plain Metropolis ratio valid.
- got_result(res)[source]¶
Called by the scheduler when a simulation is completed, with the pset that was run, and the resulting simulation data
- Parameters:
res (Result) – result from the completed simulation
- Returns:
List of PSet(s) to be run next or ‘STOP’ string.
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- start_run()[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
- class pybnf.algorithms.optimizers.simulated_annealing.SimulatedAnnealingConfig(*, step_size: float = 0.2, beta: list = <factory>, cooling: float = 0.01, beta_max: float = inf)[source]¶
Simulated-annealing config fields, co-located with the method (ADR-0002, ADR-0008). Standalone –
sais an optimizer, no longer part of the MCMC family – carrying only the four knobs the algorithm reads. The defaults are byte-identical to the values these keys held under the old MCMC-family schema; under narrowing (ADR-0013) they appear only in ansafit’s effective config.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Powell’s conjugate-direction optimizer (the powell fit type, #403).
A native, derivative-free local optimizer (Numerical Recipes §10.7), implemented
inside PyBNF’s run-loop contract – start_run / got_result only, no
run() override (ADR-0007). It is the second user-selectable refiner alongside
Simplex (refine_method = powell) and also runs standalone (fit_type =
powell), started from a single var / logvar point exactly like Simplex.
Why native (not scipy.optimize.minimize): scipy is a blocking driver,
which would force either a run() override or a bridging thread; reimplementing
the method as an explicit, picklable state machine keeps the one shared run loop
(so backup/resume work like every other method) and adds no dependency.
The method, in sampling space u (StartPointOptimizer): hold a set of n
search directions (initially the coordinate axes). One cycle line-minimizes
along each direction in turn; then – per the Numerical Recipes criterion – the
net move of the cycle may replace the direction of greatest decrease, building up
mutually conjugate directions that converge quadratically on a quadratic bowl.
Line minimization is a serial bracketing + Brent 1-D search (ADR-0016,
superseding the original fixed-step parabola): geometric (golden) expansion from
±powell_step brackets a minimum, then Brent’s method (parabolic interpolation
with a golden-section fallback) refines it to powell_line_tol. The search is
confined to the feasible t-interval – the range of step lengths that keep
every parameter inside its box – so the bound reflection never folds the 1-D
slice and Brent sees the true objective; a minimum that wants to lie past a bound
lands on the boundary. Unlike the fixed-step parabola, this follows long curved
(non-quadratic) valleys and adapts its step. It is serial – one objective
evaluation per reactor batch – so Powell no longer evaluates probes concurrently
(it was only ever two jobs on a single-point search); CMA-ES is the parallel
derivative-free method.
All state is plain numpy / float / list – no generator, no thread,
including the _BrentLineSearch sub-state-machine – so Algorithm.backup
can pickle the optimizer mid-run and run(resume=...) continues it.
- class pybnf.algorithms.optimizers.powell.PowellAlgorithm(config, refine=False)[source]¶
Powell’s conjugate-direction method as a picklable reactor state machine.
- START_POINT_KEY = 'powell_start_point'¶
Refiner start-point key (see StartPointOptimizer / pybnf._refine_best_fit).
- add_iterations(n)[source]¶
Adds n additional iterations to the algorithm. May be overridden in subclasses that don’t use self.max_iterations to track the iteration count
- got_result(res)[source]¶
Called by the scheduler when a simulation is completed, with the pset that was run, and the resulting simulation data
- Parameters:
res (Result) – result from the completed simulation
- Returns:
List of PSet(s) to be run next or ‘STOP’ string.
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- start_run()[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
- class pybnf.algorithms.optimizers.powell.PowellConfig(*, powell_step: float = 1.0, powell_line_tol: float = 0.0001, powell_stop_tol: float = 1e-05)[source]¶
Powell config fields, co-located with the method (ADR-0002, ADR-0006).
powell_stepis the initial bracketing step in sampling spaceu(a factor of10**powell_stepfor a log parameter) – the first probe distance along each direction, from which the line search expands a bracket and refines.powell_line_tolis the fractional tolerance to which each 1-D (Brent) line minimum is resolved.powell_stop_tolends the search when a whole cycle improves the objective by less than that fraction. Like Simplex’ssimplex_max_iterations,powell_max_iterations(the cycle budget) is runtime-guarded – it defaults to the globalmax_iterationswhen unset – so it is a valid key but not a schema field.powell_start_pointis internal (the refiner injects it), so it is not modeled here either.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
CMA-ES – Covariance Matrix Adaptation Evolution Strategy (cmaes fit type, #403).
A native, derivative-free, black-box optimizer (Hansen & Ostermeier 2001), the
third PyBNF refiner alongside Simplex and Powell (refine_method = cmaes) and a
first-class standalone optimizer (fit_type = cmaes). Implemented inside the
run-loop contract – start_run / got_result only, no run() override
(ADR-0007) – and natively (no cma dependency).
Unlike the serial Simplex/Powell line searches, CMA-ES is population-based: it
draws lambda candidates per generation from a multivariate normal
N(m, sigma**2 C), so the generation evaluates in parallel and CMA-ES actually
exploits PyBNF’s cluster – the same generation-synchronized reactor pattern as
Differential Evolution (accumulate the whole generation, then update). The search
runs in sampling space u (StartPointOptimizer), so log parameters adapt
geometrically.
Each generation ranks the candidates by objective and updates the distribution
mean m (weighted recombination of the best mu), the step size sigma
(cumulative step-length adaptation via the conjugate evolution path p_sigma),
and the covariance C (rank-one p_c update + rank-mu update). The
constants follow Hansen’s standard defaults (the positive-weights formulation).
lambda is the configured population_size (floored at 4); cmaes_sigma0
is the initial step in u; the run ends at max_iterations generations or
when the largest principal step sigma*sqrt(max eig C) falls below
cmaes_stop_tol.
CMA-ES is also a strong global optimizer over a bounded box, so it gains a
box / global-start mode (#404, ADR-0017; registry start_from_box=True):
given bounded uniform_var / loguniform_var priors instead of a
var / logvar start point, it begins at the box center (StartPointOptimizer)
and seeds the covariance with the per-coordinate box widths in u – so the
initial per-coordinate standard deviation is cmaes_sigma0 * (box width) and
the first generation spans the whole box. There cmaes_sigma0 is read as a
fraction of each box width (default 0.3); in point-start / refine mode it is the
absolute initial step in u as before. Candidates are repaired into the box by
FreeParameter.set_value and the reflected point enters the update (see
got_result), so bounds are respected. As a refiner (refine_method = cmaes)
the start point is the injected best fit and the covariance starts isotropic –
box mode applies only to a standalone bounded-prior fit.
All state is plain numpy / float / list (mean, sigma, covariance,
evolution paths, the pending generation) – picklable, so backup/resume work like
every other method.
- class pybnf.algorithms.optimizers.cmaes.CMAESAlgorithm(config, refine=False)[source]¶
CMA-ES as a picklable, generation-synchronized reactor state machine.
- START_POINT_KEY = 'cmaes_start_point'¶
Refiner start-point key (see StartPointOptimizer / pybnf._refine_best_fit).
- add_iterations(n)[source]¶
Adds n additional iterations to the algorithm. May be overridden in subclasses that don’t use self.max_iterations to track the iteration count
- got_result(res)[source]¶
Called by the scheduler when a simulation is completed, with the pset that was run, and the resulting simulation data
- Parameters:
res (Result) – result from the completed simulation
- Returns:
List of PSet(s) to be run next or ‘STOP’ string.
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- start_run()[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
- class pybnf.algorithms.optimizers.cmaes.CMAESConfig(*, cmaes_sigma0: float = 0.3, cmaes_stop_tol: float = 1e-11)[source]¶
CMA-ES config fields, co-located with the method (ADR-0002, ADR-0006).
cmaes_sigma0is the initial overall step size in sampling spaceu(a factor of10**cmaes_sigma0for a log parameter) in point-start / refine mode; in box / global-start mode (bounded priors, #404/ADR-0017) it is instead read as a fraction of each box width, so the initial per-coordinate standard deviation iscmaes_sigma0 * (box width)– one knob, one default (0.3), interpreted relative to the start the fit actually uses.cmaes_stop_tolends the run when the largest principal standard deviation of the search distribution shrinks below it. The population sizelambdais the globalpopulation_size(consistent with de/pso/sa), and the generation budget is the globalmax_iterations, so neither is duplicated here.cmaes_start_pointis internal (the refiner injects it), so it is not a schema field.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Gradient-based optimizers¶
Trust-Region-Reflective least-squares optimizer (trf fit type, #386 / #460).
The primary gradient-based local optimizer: the workhorse for Gaussian / sum-of-
squares objectives (the common case in PyBNF). It consumes the residual vector +
residual-Jacobian #385 assembles from bngsim’s forward output sensitivities, and
approximates the objective Hessian as JᵀJ – far better conditioned and faster-
converging than feeding a scalar gradient to a generic quasi-Newton method on a
least-squares problem. This is the D2D (Data2Dynamics) workhorse step; see
examples/becker_d2d_gradient/ for the standalone methodological reference.
Why native (not scipy.optimize.least_squares): scipy is a blocking driver that
calls fun/jac synchronously, so it cannot farm its evaluations to PyBNF’s
distributed propose/score loop (the same incompatibility powell.py documents).
The method is reimplemented as an explicit, picklable step machine – here a headless
GradientRunner (_TRFRunner)
that GradientOptimizer drives inside
the run-loop contract, no run() override (ADR-0007) – so backup/resume work like
every other method and one evaluation is one scheduler job. Factoring the step machine
into a per-start runner is also what lets a fit run N of them concurrently (local
multi-start, the orchestration the base owns). scipy is the test oracle for the step
math (tests/test_gradient_runner.py), never called in the production loop – the
Branch–Coleman–Li math below is ported in-process (the _trf_* helpers).
The method (Trust-Region-Reflective, Branch–Coleman–Li 1999)¶
In sampling space u (StartPointOptimizer / GradientOptimizer), with the
residual r and residual-Jacobian J from #385 at the current point x
(gradient g = Jᵀr, Gauss–Newton Hessian A = JᵀJ), the bounds are handled by the
Coleman–Li affine scaling, matching scipy.optimize.least_squares(method='trf'):
the Coleman–Li scaling
v(x):vᵢis the distance fromxᵢto the bound the anti-gradient points at (ubᵢ-xᵢwhengᵢ<0,xᵢ-lbᵢwhengᵢ>0, else1). WithD = diag(√v)the first-order optimality condition isD²g = 0– so the scaled gradient‖v·g‖∞reads as zero both in the interior (g=0) and on an active face (v=0there). A bound-active optimum is therefore recognized as optimal, where a plain‖g‖∞never would be;the trust-region subproblem is solved in the scaled (“hat”) space
x = D x̂, where the Newton model is½ x̂ᵀ(DAD + C)x̂ + (Dg)ᵀx̂withC = diag(g·dv)the curvature the scaling itself contributes (dv ∈ {-1,0,1}), via a single SVD of the augmented Jacobian[J D; C^{½}]and More’s root-find on the trust radiusΔ;reflective step selection: a trust-region step that would leave the box is reflected off the first bound it crosses, and the best (by predicted model reduction) of {the constrained trust step, the reflected step, a scaled-gradient Cauchy step} is taken, kept strictly interior by a step-back factor
θ(the Coleman–Li differentiability requirement). Reflection lets the iterate slide along an active face and converge cleanly onto a bound-active corner, where simply clipping the unconstrained step into the box can stall.
Δ is the trust-region knob: it shrinks when a step is rejected or over-predicted
(Δ ← ¼‖step‖ when the gain ratio ρ < ¼) and grows when a good step pushes against
the trust boundary (Δ ← 2Δ when ρ > ¾ and the step hit the boundary). The
initial Δ₀ is derived from the (scaled) start point, not a tunable.
Each iteration costs one objective evaluation (the trial). The whole scaled-SVD
subproblem solve + reflective step selection is in-process linear algebra on the cached
r/J – no evaluations. On accept, the trial’s own residual/Jacobian – already
assembled, since master-side scoring returns the simdata – become the next iterate’s, so
an accepted step needs no re-evaluation; on reject, only Δ shrinks and the step is
re-solved from the same cached SVD. The run stops when the scaled gradient is flat
(‖v·g‖∞ ≤ trf_grad_tol – the first-order optimality test that respects active
bounds), the step is negligible (‖δ‖ ≤ trf_step_tol·(‖x‖+trf_step_tol)), or the
iteration budget is spent. In the box interior (no bound active at the solution) the
scaling and reflection fall away and the method is an ordinary trust-region least-squares
step, MINPACK-style.
Scope. TRF consumes the exact least-squares residual – the Gaussian (any
scale/location) and the Student-t (#459). A fit whose objective is not an exact sum of
squares (an estimated noise scale, a Laplace / count family, active constraints;
GradientResult.least_squares_exact == False) has no faithful residual model, so this
optimizer refuses it with a pointer to the L-BFGS-B path (fit_type = lbfgs, #386’s
fallback). Local multi-start is provided by GradientOptimizer (the base runs
N independent _TRFRunner starts concurrently and keeps the global best).
All runner state is plain numpy / float (the point, residual, Jacobian, the
cached scaling + SVD, the trust radius) – picklable, so Algorithm.backup checkpoints
the optimizer (and its list of runners) mid-run.
- class pybnf.algorithms.optimizers.trf.TRFAlgorithm(config, refine=False)[source]¶
Bounded Trust-Region-Reflective least-squares: a method-agnostic multi-start orchestrator (
GradientOptimizer) over per-start_TRFRunnerstep machines.- START_POINT_KEY = 'trf_start_point'¶
The internal config key the refiner start point is injected under (mirrors
SimplexAlgorithm’s'simplex_start_point'). Set by each subclass;pybnf._refine_best_fitwrites the best fit here so refinement starts from it instead of parsing the (refiner-irrelevant) variable specs.
- fit_type = 'trf'¶
Message label + refiner start-point key (see StartPointOptimizer).
- class pybnf.algorithms.optimizers.trf.TRFConfig(*, trf_grad_tol: float = 1e-08, trf_step_tol: float = 1e-08)[source]¶
TRF (Trust-Region-Reflective) config fields, co-located with the method (ADR-0006).
trf_grad_tolends the run when the largest component of the scaled least-squares gradientv·Jᵀr(in sampling space) falls below it – a first-order optimality test that reads as the ordinary‖Jᵀr‖∞in the interior and as zero on a bound the gradient pushes against (the Coleman–Li scalingvvanishes there).trf_step_tolends it when an accepted stepδbecomes negligible relative to the point (‖δ‖ ≤ trf_step_tol·(‖x‖+trf_step_tol)). The initial trust radius is derived from the (scaled) start point – there is no damping/radius tunable. Like Powell’s cycle budget,trf_max_iterationsis runtime-guarded – it defaults to the globalmax_iterationswhen unset – so it is a valid key but not a schema field.trf_start_pointis internal (the refiner injects it), so it is not modeled here either.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Bounded limited-memory BFGS optimizer – L-BFGS-B (lbfgs fit type, #386).
The scalar-gradient sibling of the trust-region least-squares method (trf.py):
where TRF consumes the residual vector + residual-Jacobian and is the workhorse for
exact-least-squares (Gaussian / Student-t, fixed scale) objectives, this method consumes
only the scalar objective value (res.score) and the scalar gradient
(gradient_at(res).gradient) that #385 assembles from bngsim’s forward output
sensitivities. That makes it the fallback for precisely the objectives TRF refuses –
the ones whose GradientResult.least_squares_exact is False and so have no
faithful sum-of-squares residual: an estimated noise scale (the retained +log σ
normalizer is not a square), the Laplace / count families, and a fit with active
constraint penalties. The assembly returns a finite scalar gradient for all of
these, so the same gradient seam drives both methods; only the step math differs.
Why native (not scipy.optimize.minimize(method='L-BFGS-B')): scipy is a blocking
driver that calls fun/jac synchronously, so it cannot farm its evaluations to
PyBNF’s distributed propose/score loop (the same incompatibility powell.py and
trf.py document). The method is reimplemented as an explicit, picklable step
machine – here a headless GradientRunner
(_LBFGSRunner) that GradientOptimizer
drives inside the run-loop contract, no run() override (ADR-0007) – so backup/resume
work like every other method and one evaluation is one scheduler job. Factoring the step
machine into a per-start runner is also what lets a fit run N of them concurrently
(local multi-start, the orchestration the base owns); the runner itself is pure u-space
numpy, so its math is unit-testable offline against a scipy oracle with no backend.
The method (L-BFGS-B: Cauchy point + subspace min + line search)¶
The Byrd–Lu–Nocedal–Zhu (1995) algorithm. In sampling space u
(StartPointOptimizer / GradientOptimizer), with the scalar gradient
g = ∇F from #385 at the current point x:
keep the
mmost recent curvature pairs(s, y)(s = xₖ₊₁ - xₖ,y = gₖ₊₁ - gₖ) and form the limited-memory Hessian approximationBin its direct (not inverse) form – here a denseθIseed updated by the cached pairs (_form_b()), since PyBNF fits have smalln(the paper’s compactθI − W M Wᵀproducts only pay off for largen);generalized Cauchy point (
_generalized_cauchy_point(), their Algorithm CP): walk the piecewise-linear projected steepest-descent pathP[x − t·g, l, u], sort the per-coordinate breakpoints, and stop at the first local minimizer of the quadratic modelmalong it – the Cauchy pointxᶜand the active set (the coordinates pinned to a bound on the way);subspace minimization (
_subspace_min(), their §5 dense primal): minimize the quadratic model over the free variables fromxᶜ, holding the active coordinates at their bounds, truncated to stay feasible – the subspace minimizerx̄. The search direction isd = x̄ − x. Both stages are pure in-process linear algebra on the cached pairs + gradient – no objective evaluations;backtracking (Armijo) line search along
d: start at step lengthα = 1(the full step lands atx̄) and project each trialP[x + α d]onto the box (_u_bounds()); accept the firstαwhose projected sufficient-decrease holds,F(P[x+αd]) ≤ F(x) + c₁ · gᵀ(P[x+αd] - x)(Bertsekas’ projected Armijo, testing the actual displacement so a clamped coordinate is handled correctly; it reduces to the usualα gᵀdwhen no bound is active), else shrinkα ← backtrack·αand retry;on accept, fold the realized
(s, y)into the history (skipping a non-positive- curvature pair, which would break the BFGS positive-definite invariant) and step.
Each line-search trial costs one objective evaluation; an accepted step’s gradient
is assembled from that trial’s own simdata (master-side scoring returns it), so the step
needs no re-evaluation. The run stops when the projected gradient is flat
(‖P[x-g]-x‖∞ ≤ lbfgs_grad_tol – the first-order optimality test that respects active
bounds), the accepted step is negligible (‖s‖ ≤ lbfgs_step_tol·(‖x‖+lbfgs_step_tol)),
the line search cannot reduce the objective even along steepest descent (a stalled
minimum), or the iteration budget is spent.
In the box interior (no bound active at the solution) the Cauchy point falls short of
its first breakpoint and the subspace minimization runs over every variable, so the step
reduces exactly to the unconstrained limited-memory quasi-Newton step −B⁻¹g; the
active-set machinery only bites when bounds are active at the optimum.
All runner state is plain numpy / float / list (the point, gradient, the
(s, y) history, the line-search scratch) – picklable, so Algorithm.backup
checkpoints the optimizer (and its list of runners) mid-run, exactly like Powell /
CMA-ES / TRF (ADR-0007). B and the Cauchy / subspace work are recomputed per
iteration from the cached pairs, so no dense matrix joins the persisted state.
- class pybnf.algorithms.optimizers.lbfgs.LBFGSAlgorithm(config, refine=False)[source]¶
Bounded limited-memory BFGS (L-BFGS-B): a method-agnostic multi-start orchestrator (
GradientOptimizer) over per-start_LBFGSRunnerstep machines.- START_POINT_KEY = 'lbfgs_start_point'¶
The internal config key the refiner start point is injected under (mirrors
SimplexAlgorithm’s'simplex_start_point'). Set by each subclass;pybnf._refine_best_fitwrites the best fit here so refinement starts from it instead of parsing the (refiner-irrelevant) variable specs.
- fit_type = 'lbfgs'¶
Message label + refiner start-point key (see StartPointOptimizer).
- class pybnf.algorithms.optimizers.lbfgs.LBFGSConfig(*, lbfgs_grad_tol: float = 1e-06, lbfgs_step_tol: float = 1e-08, lbfgs_history: int = 10, lbfgs_c1: float = 0.0001, lbfgs_backtrack: float = 0.5)[source]¶
L-BFGS-B config fields, co-located with the method (ADR-0006).
lbfgs_grad_tolends the run when the largest component of the projected gradientP[x-g]-x(in sampling space) falls below it – a first-order optimality test that reads as the ordinary‖g‖∞in the interior and as zero on a bound the gradient pushes against.lbfgs_step_tolends it when an accepted stepsbecomes negligible relative to the point (‖s‖ ≤ lbfgs_step_tol·(‖x‖+lbfgs_step_tol)).lbfgs_historyism, the number of recent curvature pairs the limited-memory Hessian retains (more pairs ⇒ a richer Hessian model at higher per-step cost).lbfgs_c1is the Armijo sufficient-decrease constant (0 < c₁ < 1, conventionally1e-4) andlbfgs_backtrackthe step-length reduction factor on a rejected trial (0 < β < 1). Like Powell’s / TRF’s cycle budget,lbfgs_max_iterationsis runtime-guarded – it defaults to the globalmax_iterationswhen unset – so it is a valid key but not a schema field.lbfgs_start_pointis internal (the refiner injects it), so it is not modeled here either.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Profile-likelihood identifiability + confidence intervals (profile_likelihood
job type, #446 / #466).
Profile likelihood is the Data2Dynamics (D2D) method for parameter identifiability and
confidence intervals (Raue et al., Bioinformatics 25(15):1923-1929, 2009). For each
fitted parameter theta_k it fixes theta_k to a grid of values around the optimum
theta* and re-optimizes all the other parameters at each grid point, tracing the
profile chi2_PL(theta_k) = min_{j != k} chi2(theta). The confidence interval is the
range where the profile stays below a Delta chi2 threshold (the chi-square quantile
at the chosen confidence level, 1 dof); a profile that stays flat diagnoses a
structurally non-identifiable parameter, one that rises on only one side (or reaches
a bound without crossing) a practically non-identifiable one, and one that crosses
the threshold on both sides an identifiable one with a finite CI.
A standalone new-era job, not a fit stage (ADR-0031, #446)¶
This is a self-contained job_type = profile_likelihood run selected on the modern
(edition >= 2) surface, not a stage auto-triggered at the end of a fit. It subclasses
GradientOptimizer, so it inherits the
whole gradient path – the edition-2 / sensitivity-backend / differentiable-dynamics gates,
the per-experiment routing setup, and the per-evaluation GradientOptimizer.gradient_at()
assembly – and depends on it exactly as trf / lbfgs do (bngsim forward
sensitivities, BNGSIM_HAS_OUTPUT_SENS).
The inner optimizer – the one that both polishes to theta* and re-optimizes the
remaining free parameters at each grid point – mirrors the trf/lbfgs split so profile
likelihood fits every objective the gradient methods do, not just the exact-least-squares
ones. The kind is chosen automatically from the objective’s structure, read once from an
assembled GradientResult (a preflight evaluation on the
polish path, or the explicit-theta* evaluation): an exact sum of squares (a Gaussian
/ Student-t family with a fixed noise scale and no constraints, least_squares_exact)
profiles with the reduced-dimension _TRFRunner (the
D2D reference, consuming the residual / Jacobian); anything else – an estimated noise
scale, a Laplace / count family, active constraint penalties – has no faithful
residual and profiles with the scalar-gradient
_LBFGSRunner instead (the same objectives
job_type = lbfgs handles). least_squares_exact is a structural property of the
objective, not the point, so the single read governs the whole run.
Obtaining theta* (the open design question of #466)¶
Two sources, resolved at start_run():
Explicit override. If every free parameter declares an
initial_value:in itsparameter:record (so eachFreeParametercarries avalue), those values aretheta*. The job evaluates that one point (for the reference objective) and skips straight to profiling – the fast, deterministic path for a fit you have already run.Integrated polish. Otherwise the job first runs a gradient polish – the base’s own multi-start Trust-Region-Reflective fit over the bounded-prior box (
population_sizestarts,max_iterationsbudget) – to findtheta*, then profiles around it. One.confdoes the whole fit-and-profile end to end.
Either way the Delta chi2 reference is the global objective minimum found across the
whole run (trajectory.best_score()), so a profile scan that improves slightly on the
polish optimum re-references cleanly rather than reporting a spuriously negative rise.
The driver (this issue’s core deliverable)¶
Profiling walks outward from theta* in sampling space u (priors/scale.py,
ADR-0029 – log10 for a log-scaled parameter, so a D2D log10 grid is the u
grid; the Delta chi2 threshold is on the objective, never on the transformed
parameter). Each profiled parameter runs two independent _ProfileTracks (one
per direction). A track takes an adaptive step – shrinking where the profile steepens,
growing where it is flat, targeting a fixed Delta chi2 rise per grid point – and at
each grid point re-optimizes the remaining free parameters with a reduced-dimension inner
runner of the selected kind (a _TRFRunner with the fixed column dropped from the
Jacobian and the full residual kept, or an _LBFGSRunner on the scalar gradient with the
fixed coordinate dropped), warm-started from the neighboring grid point’s optimum. A
direction terminates on the threshold crossing, on a bound, or on a per-direction point cap.
Parallel across parameters (#446 sub-issue 2 / #467)¶
The per-parameter profiles – and the two directions of a single parameter – are
independent, so they farm across the existing Dask scheduler rather than running serially,
exactly as the base’s local multi-start does: up to profile_likelihood_max_parallel
directional tracks (default: all of them) run concurrently, each an inherently sequential
warm-started walk holding one evaluation in flight, routed back to its owner by PSet name.
A cap only queues the excess tracks – they run as slots free – so coverage is never
silently truncated; a direction that stops at its point budget is reported as such.
Serialized + resumable outputs (#467)¶
Every state object here is plain numpy / float / list (the tracks, the inner
runners, the per-point cost / theta_opt / nfev / success accumulated in
self._profiles), so the optimizer pickles through PyBNF’s ordinary backup/resume
machinery exactly like the other gradient methods (ADR-0007) – a resumed run re-submits
only the in-flight tracks and never recomputes a finished one. At the end the driver writes
the finished artifacts to Results/: the per-parameter curve files, the CI +
identifiability summary table, and – when matplotlib (the optional pybnf[plot] extra)
is importable – the profile plots (Delta chi2 panels with the threshold + CI lines,
the reference notebook’s Cell 9); a missing matplotlib skips only the plots, never the run.
scipy stays out of the production loop: the chi-square threshold comes from a
dependency-free probit approximation (_chi2_quantile_1dof()).
- class pybnf.algorithms.optimizers.profile_likelihood.ProfileLikelihoodAlgorithm(config, refine=False)[source]¶
Standalone profile-likelihood driver (
job_type = profile_likelihood, #446/#466).A two-phase job over the
GradientOptimizergradient path: an optional multi-start gradient polish to the optimumtheta*(skipped when the config suppliestheta*viainitial_value:on every parameter), then the profile phase – one adaptive outward_ProfileTrackper parameter per direction, up toprofile_likelihood_max_parallelof them farmed across the scheduler concurrently (#467), each re-optimizing the remaining free parameters with a reduced-dimension inner runner. Both the polish and the re-optimizations use the same inner optimizer, selected once from the objective’s structure (_select_runner_kind()): the trust-region-reflective_TRFRunnerfor an exact sum of squares, the scalar-gradient_LBFGSRunnerotherwise – so profile likelihood covers the estimated-scale / Laplace / count / constrained objectives too, not only the exact-least-squares ones. At the end it extracts each parameter’s confidence interval and identifiability class and writes the finished artifacts toResults/– the per-parameter curves, the CI/classification summary table, and (with matplotlib) the profile plots. All per-point state pickles through PyBNF’s backup/resume, so a resumed run continues without recomputing finished tracks.- got_result(res)[source]¶
Route a completed Result to the start that owns it, advance just that start’s runner, and return its next PSet –
[]once it converges (other starts keep going), or'STOP'only when the last live start finishes.
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- class pybnf.algorithms.optimizers.profile_likelihood.ProfileLikelihoodConfig(*, profile_likelihood_params: Any = None, profile_likelihood_confidence: float = 0.95, profile_likelihood_step: float = 0.1, profile_likelihood_min_step: float = 0.001, profile_likelihood_max_step: float = 1.0, profile_likelihood_dchi2_target: float = 0.0, profile_likelihood_max_points: int = 40, profile_likelihood_reopt_max_iterations: int = 50, profile_likelihood_grad_tol: float = 1e-08, profile_likelihood_step_tol: float = 1e-08, profile_likelihood_max_parallel: int = 0)[source]¶
Profile-likelihood config fields, co-located with the method (ADR-0006).
profile_likelihood_paramsselects which free parameters to profile (a list of ids; absent -> profile every free parameter).profile_likelihood_confidenceis the CI confidence level (theDelta chi2threshold is its chi-square 1-dof quantile).profile_likelihood_stepis the initial outward step in sampling spaceu(log10 decades for a log-scaled parameter), adapted betweenprofile_likelihood_min_stepandprofile_likelihood_max_stepto hold each grid point’s objective rise nearprofile_likelihood_dchi2_target(0-> auto, one tenth of the threshold).profile_likelihood_max_pointscaps the grid points per direction.profile_likelihood_reopt_max_iterationscaps the inner re-optimization’s iterations at each grid point;profile_likelihood_grad_tol/profile_likelihood_step_tolare its (and the polish’s) optimality / step tolerances, shared by whichever inner optimizer the objective selects (Trust-Region-Reflective or L-BFGS-B).profile_likelihood_max_parallelcaps how many per-direction profile walks run concurrently on the scheduler (0-> all of them, one per direction per profiled parameter – the fully-parallel default; the walks that do not fit the cap queue and run as slots free, they are never dropped). Like the other gradient methods’ cycle budgets,profile_likelihood_max_iterations(the polish budget) is runtime-guarded – it defaults to the globalmax_iterationswhen unset – so it is a valid key but not a schema field.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Bayesian samplers¶
The BayesianAlgorithm base class — shared by all Bayesian sampler fit types.
Extracted from the algorithms package (M1 Step 3). Holds posterior/prior
bookkeeping and the rank-normalized split-R-hat / ESS convergence diagnostics
(Vehtari et al. 2021). The run loop and execution seam live in the Algorithm
base (..base); BayesianAlgorithm inherits them and adds no core.* call of
its own, so this module does not import the core seam.
- class pybnf.algorithms.samplers.base.BayesianAlgorithm(config)[source]¶
Superclass for Bayesian MCMC algorithms
- check_convergence(iteration, max_rhat)[source]¶
Check if R-hat has converged below threshold. Returns True if should stop.
- cleanup()[source]¶
Called when quitting due to error. Save the histograms in addition to the usual algorithm cleanup
- compute_ess()[source]¶
Bulk and tail effective sample size per parameter (Vehtari et al. 2021).
Thin glue over
pybnf.diagnostics.ess()(ADR-0009): reads this instance’s chain history and delegates the pure math. Returns(bulk_ess, tail_ess)arrays of shape(n_dim,), or(None, None).
- compute_rhat()[source]¶
Rank-normalized split-R-hat per parameter (Vehtari et al. 2021).
Thin glue over
pybnf.diagnostics.rhat()(ADR-0009): reads this instance’s chain history and delegates the pure math. Returns an(n_dim,)array, orNoneif there is insufficient history.
- evaluate_constraints(simdata, chain_index)[source]¶
Evaluate all constraints against simulation data and cache the pass/fail results for this chain.
- Parameters:
simdata – Simulation data dictionary
chain_index – Index of the chain that was accepted
- got_result(res)[source]¶
Called by the scheduler when a simulation is completed, with the pset that was run, and the resulting simulation data
- Parameters:
res (Result) – result from the completed simulation
- Returns:
List of PSet(s) to be run next or ‘STOP’ string.
- ln_prior(pset)[source]¶
Returns the value of the prior distribution for the given parameter set
- Parameters:
pset (PSet)
- Returns:
float value of ln times the prior distribution
- load_priors()[source]¶
Builds the data structures for the priors, based on the variables specified in the config.
- record_pointwise_loglik(res, chain_index)[source]¶
Cache the just-accepted pset’s per-observation log-likelihoods for this chain (ADR-0056), mirroring
evaluate_constraints(): computed here, whereres.simdatafor the accepted pset is in hand, and written later bysample_pset()for whichever pset is current at a sample iteration. A no-op unlessoutput_inference_datais set and the objective is a likelihood; any failure is logged, never fatal (the run must not die for a diagnostics sidecar).
- report_constraint_satisfaction(file_ext)[source]¶
Read the constraint samples file and write a summary of constraint satisfaction percentages. :param file_ext: String to append to the output file name
- report_convergence_diagnostics(iteration)[source]¶
Compute and report R-hat, ESS, and ESS/evaluation. Called every 10 iterations. Returns max_rhat for convergence checking, or None.
- sample_pset(pset, ln_prob, chain_index=None)[source]¶
Adds this pset to the set of sampled psets for the final distribution. :param pset: :type pset: PSet :param ln_prob - The probability of this PSet to record in the samples file. :type ln_prob: float :param chain_index: Index of the chain, used to look up cached constraint results. :type chain_index: int or None
- start_run(setup_samples=True)[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
- class pybnf.algorithms.samplers.base.MCMCFamilyConfig(*, step_size: float = 0.2, burn_in: int = 10000, sample_every: int = 100, output_hist_every: int = 100, hist_bins: int = 10, adaptive: int = 10000, credible_intervals: list = <factory>, beta: list = <factory>, continue_run: int = 0, rhat_threshold: float = 0.0, diagnostics_every: int = 0)[source]¶
Config shared by the whole Bayesian/MCMC family (mh, pt, am, dream, p_dream), co-located with the family base (ADR-0002, ADR-0006). (sa was once in this family; M2.2/ADR-0008 made it a standalone optimizer.)
Holds the defaulted keys the shared
BayesianAlgorithmbase reads (so they are common to every MCMC method) plus the β-ladderpostprocesshook. The per-method keys live on the leaf models that subclass this and co-locate with their algorithm class:BasicMCMCConfig(mh/pt/sa, in basic_mcmc.py),AdaptiveMCMCConfig(am, in adaptive_mcmc.py),DreamConfig(dream, in dream.py) andPDreamConfig(p_dream, in pdream.py).neg_bin_rstays inGlobalConfig– an objfunc param read regardless of fit_type. Values are byte-identical to the old global defaults; under narrowing (ADR-0013) these keys appear only in each MCMC fit_type’s own effective config. The per-fit_type validity of individual keys is reported bycheck_unused_keysinconfig.py.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- classmethod postprocess(conf_dict, fit_type)[source]¶
The β-ladder (ported from
Configuration.postprocess_mcmc_keys).Algorithms ‘mh’, ‘pt’, ‘am’, ‘dream’, ‘p_dream’ have similar but non-identical valid config keys; this builds
beta_list/reps_per_betaand reconcilesexchange_every/population_size. Mutatesconf_dictin place (operating on the RAW config dict, before defaults merge, so raw-presence checks like'beta' not in conf_dictmean “user did not set it”).config._build_configdispatches this uniformly; non-MCMC models inherit the no-oppostprocessfromPyBNFConfigModel(ADR-0006 #3).Only the config transformations live here now. The per-method unused-key warnings this hook used to also emit (
exchange_every/reps_per_betaon non-pt, the sa-onlycooling/beta_max, the DREAM-onlycrossover_number/zeta/lambda/gamma_probon mh/pt/am) moved to the unified, schema-derivedConfiguration.check_unused_keys(#401, ADR-0014), which now warns about every foreign key each MCMC fit does not own – more precisely than this hook’s hand-listed branches did.
BasicBayesMCMCAlgorithm — Metropolis-Hastings and Parallel Tempering (the
mh and pt fit types).
mh is deprecated but still runs; pt is a working method sharing this class.
(Simulated annealing, formerly sa on this class, is now a standalone
optimizer — see optimizers/simulated_annealing.py, M2.2/ADR-0008.) Subclasses the
sampler base (BayesianAlgorithm) and inherits the run loop + execution seam from
Algorithm.
- class pybnf.algorithms.samplers.basic_mcmc.BasicBayesMCMCAlgorithm(config)[source]¶
Implements a Bayesian Markov chain Monte Carlo simulation. This is essentially a non-parallel algorithm, but here, we run n instances in parallel, and pool all results. This will give you a best fit (which is maybe not great), but more importantly, generates an extra result file that gives the probability distribution of each variable. This distribution depends on the prior, which is specified according to the variable initialization rules.
- add_iterations(n)[source]¶
Adds n additional iterations to the algorithm. May be overridden in subclasses that don’t use self.max_iterations to track the iteration count
- choose_new_pset(oldpset, index)[source]¶
Helper function to perturb the old PSet, generating a new proposed PSet. The step is a fixed-magnitude (step_size) random-walk move; any component that would leave the box is reflected back inside (see FreeParameter._reflect). :param oldpset: The PSet to be changed :type oldpset: PSet :param index: The chain index, selecting that chain’s own Generator :type index: int :return: the new PSet
- cleanup()[source]¶
Called when quitting due to error. Save the histograms in addition to the usual algorithm cleanup
- got_result(res)[source]¶
Called by the scheduler when a simulation is completed, with the pset that was run, and the resulting simulation data :param res: PSet that was run in this simulation :type res: Result :return: List of PSet(s) to be run next.
- replica_exchange()[source]¶
Performs replica exchange for parallel tempering. Then proposes n new parameter sets to resume all chains after the exchange. :return: List of n PSets to run
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
- should_sample(index)[source]¶
Checks whether this replica index is one that gets sampled. For mcmc, always True. For pt, must be a replica at the max beta
- start_run()[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run. :return: list of PSets
- try_to_choose_new_pset(index)[source]¶
Helper function Advances the iteration number, and tries to choose a new parameter set for chain index i If that fails (e.g. due to a box constraint), keeps advancing iteration number and trying again. If it hits an iteration where it has to stop and wait (a replica exchange iteration or the end), returns None Otherwise returns the new PSet. :param index: :return:
- class pybnf.algorithms.samplers.basic_mcmc.BasicMCMCConfig(*, step_size: float = 0.2, burn_in: int = 10000, sample_every: int = 100, output_hist_every: int = 100, hist_bins: int = 10, adaptive: int = 10000, credible_intervals: list = <factory>, beta: list = <factory>, continue_run: int = 0, rhat_threshold: float = 0.0, diagnostics_every: int = 0, exchange_every: ~typing.Any = 20)[source]¶
Config for the basic-MCMC codes mh/pt, co-located with the method (ADR-0006). Adds the one key those samplers read (
exchange_every) on top of the shared family fields; the β-ladderpostprocesshook is inherited.exchange_everyis typedAnybecause the hook overwrites it withnp.inffor the non-PT (mh) method (it holds both an int and an infinity).(
cooling/beta_maxwere sa-only; M2.2/ADR-0008 moved them toSimulatedAnnealingConfigwhen sa became a standalone optimizer.)- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Adaptive_MCMC — the Adaptive Metropolis sampler (the am fit type).
PyBNF’s recommended Bayesian sampler. Extracted byte-identical (M1 Step 4). Subclasses the sampler base (BayesianAlgorithm) and inherits the run loop + execution seam from Algorithm.
- class pybnf.algorithms.samplers.adaptive_mcmc.AdaptiveMCMCConfig(*, step_size: float = 0.2, burn_in: int = 10000, sample_every: int = 100, output_hist_every: int = 100, hist_bins: int = 10, adaptive: int = 10000, credible_intervals: list = <factory>, beta: list = <factory>, continue_run: int = 0, rhat_threshold: float = 0.0, diagnostics_every: int = 0, stablizingCov: float = 0.001, calculate_covari: ~typing.Any = None)[source]¶
Config for adaptive MCMC (am), co-located with the method (ADR-0006). Adds the covariance-adaptation keys
Adaptive_MCMCreads on top of the shared family fields; the β-ladderpostprocesshook is inherited.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class pybnf.algorithms.samplers.adaptive_mcmc.Adaptive_MCMC(config)[source]¶
- got_result(res)[source]¶
Called by the scheduler when a simulation is completed, with the pset that was run, and the resulting simulation data :param res: PSet that was run in this simulation :type res: Result :return: List of PSet(s) to be run next.
- reset(bootstrap=None)[source]¶
Resets the Algorithm, keeping loaded variables and models
- Parameters:
bootstrap (int or None) – The bootstrap number (None if not bootstrapping)
- Returns:
A rejected bootstrap replicate (objective over
bootstrap_max_obj) is retried withbootstrap_attemptincremented by the caller; that advances the RNG sub-stream so the retry draws a fresh resample and fit rather than repeating the identical failing run.bootstrap_attempt == 0(the first try, and every non-bootstrap reset) reproduces the historical replicate-only seeding byte for byte.
DreamAlgorithm — the DREAM(ZS) sampler (the dream fit type).
DiffeRential Evolution Adaptive Metropolis drawing proposal donors from a growing ZS archive (Vrugt 2016). Extracted byte-identical (M1 Step 4). Subclasses the sampler base (BayesianAlgorithm) and inherits the run loop + execution seam from Algorithm; it makes no core.* call of its own.
- class pybnf.algorithms.samplers.dream.DreamAlgorithm(config)[source]¶
Implements a variant of the DREAM algorithm as described in Vrugt (2016) Environmental Modelling and Software.
Adapts Bayesian MCMC to use methods from differential evolution for accelerated convergence and more efficient sampling of parameter space
- calculate_new_pset(idx)[source]¶
Uses differential evolution-like update to calculate new PSet. Returns (PSet, cr_idx) or (None, cr_idx) if the proposal is out of bounds.
- Parameters:
idx – Index of PSet to update
- Returns:
tuple of (PSet or None, int)
- calculate_snooker_pset(idx)[source]¶
Snooker update proposal (ter Braak & Vrugt, 2008). Projects archive points onto the line through the current state and a reference archive point, then jumps along that axis.
Returns (PSet or None, log_correction) where log_correction is the log of the Hastings correction factor (d-1)*log(||Xp - Zc|| / ||X - Zc||).
- detect_and_reset_outliers()[source]¶
Detect outlier chains using the configured method on mean log-posteriors (last 50% of history). Reset outlier chains to copies of randomly selected non-outlier chains.
Methods: ‘iqr’ (interquartile range), ‘grubbs’ (Grubbs test at alpha=0.01).
- got_result(res)[source]¶
Called by the scheduler when a simulation is completed, with the pset that was run, and the resulting simulation data
- Parameters:
res (Result) – PSet that was run in this simulation
- Returns:
List of PSet(s) to be run next.
- start_run(setup_samples=True)[source]¶
Called by the scheduler at the start of a fitting run. Must return a list of PSets that the scheduler should run.
Algorithm subclasses optionally may set the .name field of the PSet objects to give a meaningful unique identifier such as ‘gen0ind42’. If so, they MUST BE UNIQUE, as this determines the folder name. Uniqueness will not be checked elsewhere.
- Returns:
list of PSets
- class pybnf.algorithms.samplers.dream.DreamConfig(*, step_size: float = 0.2, burn_in: int = 10000, sample_every: int = 100, output_hist_every: int = 100, hist_bins: int = 10, adaptive: int = 10000, credible_intervals: list = <factory>, beta: list = <factory>, continue_run: int = 0, rhat_threshold: float = 0.0, diagnostics_every: int = 0, gamma_prob: float = 0.1, zeta: float = 1e-06, lambda_: float = 0.1, crossover_number: int = 3, adaptive_step_size: ~typing.Any = True, archive_size: int | None = None, archive_thin_rate: int = 10, snooker_prob: float = 0.1, delta: int = 1, outlier_method: str = 'iqr')[source]¶
Config for DREAM(ZS) (dream), co-located with the method (ADR-0006). Adds the DREAM-specific keys on top of the shared family fields; the β-ladder
postprocesshook is inherited.PDreamConfig(p_dream) subclasses this and addsprecondition_adapt.adaptive_step_sizeis typedAnyso a user int 0/1 stays an int (matching the old behavior). Values byte-identical to the old global defaults.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- classmethod postprocess(conf_dict, fit_type)[source]¶
The MCMC β-ladder (inherited from
MCMCFamilyConfig) plus the DREAM-familystep_sizecoupling (ADR-0013): a user-setstep_sizepinsadaptive_step_sizeoff. Both keys are owned only by DREAM/P-DREAM, so the coupling is intra-family – it formerly lived as a global write inconfig.pythat, post-narrowing, would have stamped an orphanadaptive_step_sizeonto anymh/am/safit that setstep_size.PDreamConfiginherits this (it owns both keys too).
PDreamAlgorithm — Preconditioned DREAM (the p_dream fit type).
DREAM(ZS) with proposals computed in a covariance-whitened parameter space, for better sampling of correlated posteriors. Extracted byte-identical (M1 Step 4). Subclasses DreamAlgorithm (a sibling leaf in this sub-package).
- class pybnf.algorithms.samplers.pdream.PDreamAlgorithm(config)[source]¶
P-DREAM: Preconditioned DREAM.
Extends DREAM(ZS) by computing DE proposals in a covariance-whitened parameter space. An online covariance estimate C is learned from the chain history (as in Adaptive Metropolis). Donors are transformed to whitened coordinates z = L_inv @ x before computing DE differences, and crossover is applied in whitened space where dimensions are decorrelated.
The proposal remains symmetric (DE differences from an external archive), so standard Metropolis-Hastings acceptance is valid without additional Hastings correction.
After a configurable adaptation period, the covariance is updated every generation from the pooled chain history. Before adaptation begins, the sampler behaves identically to DREAM(ZS).
- calculate_new_pset(idx)[source]¶
DE proposal in whitened space.
When preconditioning is active: 1. Transform current state and archive donors to z = L_inv @ x 2. Compute DE difference in z-space 3. Apply crossover in z-space (dimensions are decorrelated) 4. Scale and add perturbation in z-space 5. Convert the total jump back: dx = L @ dz_total 6. Propose x_new = x_current + dx
Before preconditioning activates, falls back to standard DREAM proposals.
- class pybnf.algorithms.samplers.pdream.PDreamConfig(*, step_size: float = 0.2, burn_in: int = 10000, sample_every: int = 100, output_hist_every: int = 100, hist_bins: int = 10, adaptive: int = 10000, credible_intervals: list = <factory>, beta: list = <factory>, continue_run: int = 0, rhat_threshold: float = 0.0, diagnostics_every: int = 0, gamma_prob: float = 0.1, zeta: float = 1e-06, lambda_: float = 0.1, crossover_number: int = 3, adaptive_step_size: ~typing.Any = True, archive_size: int | None = None, archive_thin_rate: int = 10, snooker_prob: float = 0.1, delta: int = 1, outlier_method: str = 'iqr', precondition_adapt: ~typing.Any = None)[source]¶
Config for preconditioned DREAM (p_dream), co-located with the method (ADR-0006). Extends
DreamConfigwith the one preconditioning keyPDreamAlgorithmadds; everything else (incl. the β-ladder hook) is inherited.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
HMCSampler – the hmc fit type: blackjax NUTS on the analytical model’s JAX
log-density (issue #425, ADR-0059).
This is an opt-in reference sampler: a gradient-based NUTS run that exists to
evaluate PyBNF’s gradient-free samplers (am / dream / p_dream) against a
research-grade yardstick on the canonical stress geometries. It runs only on the
analytical / bring-your-own-log-density model (pybnf/analytical_model.py), never on a
simulator – a BNGL/SBML posterior provides no cheap gradient, and simulator-path HMC is
rejected outright by the ADR.
Unlike every other sampler, hmc does not dispatch parameter sets through the
dask -> execute() -> score-column -> DirectPassObjective loop: the gradient
cannot survive the per-pset dask round-trip. Instead it builds a JAX logdensity_fn in
process from (a) the model’s JAX NLL (AnalyticalModel.nll_jax) and (b) the priors’ JAX
log-densities (Prior.logpdf_jax), hands it to blackjax NUTS with window adaptation, and
writes the draws in the standard samples format so the ArviZ bridge (ADR-0055), the
LOO/WAIC sidecar machinery, and the rank-normalized split-R-hat / bulk-tail ESS diagnostics
all work unchanged.
It samples an unconstrained z in R^d (ADR-0059 item 5), mapped to sampling space u
(ADR-0010) by a per-parameter support-aware bijection u = b(z) (pybnf.priors.bijector),
with target
log pi(z) = sum_i [ prior_i.logpdf_jax(b_i(z_i)) + log|b_i’(z_i)| ] + ( -NLL( scale.inverse(b(z)) ) )
The prior is defined in u (ADR-0010), so there is no theta <-> u Jacobian – the only
change-of-variables term is the unconstraining bijection’s log|b'(z)|. For an unbounded prior
b is the identity (z == u) and this is exactly the density am samples, now
differentiated; for a positive/bounded/truncated prior b is a log/logit reparameterization that
lands strictly inside the open support for every finite z, so the -inf support wall (and the
NUTS divergence it once caused) is unreachable. Diagnostics and the samples file report u (the
recorded Ln_probability is un-Jacobianed back to log pi(u)), so HMC stays comparable to the
gradient-free samplers on the same posterior coordinate.
It samples the closed-form-truth and stress-geometry menu targets (gaussian /
rotated_gaussian / banana / multimodal), the full edition-2 prior catalog – every
family supplies a hand-written, scipy-logpdf-oracle-checked logpdf_jax (ADR-0059 item 4),
sampled divergence-free on any support via the bijection (item 5) – and log-scaled parameters
(lognormal_var / loguniform_var), whose u -> theta inverse traces through JAX
(Scale.inverse_jax). It samples every built-in menu target (rotated_quartic included now)
and bring-your-own ``expression`` targets – a user’s own PEtab-math NLL lambdified to JAX
(ExpressionModel.nll_jax, ADR-0059 item 2), so HMC is a reference sampler on arbitrary
closed-form log-densities, not just the canned menu. The only HMC work still deferred is the Python
callable = module:func BYO form (a general callable is not JAX-traceable; ADR-0050).
jax/blackjax are the optional pybnf[jax] extra (ADR-0019): only this module (and
the lazily-imported nll_jax / logpdf_jax) touches them, and a missing install
surfaces as a pointed PybnfError naming the extra, never a bare ImportError.
- class pybnf.algorithms.samplers.hmc.HMCConfig(*, step_size: float = 0.2, burn_in: int = 10000, sample_every: int = 100, output_hist_every: int = 100, hist_bins: int = 10, adaptive: int = 10000, credible_intervals: list = <factory>, beta: list = <factory>, continue_run: int = 0, rhat_threshold: float = 0.0, diagnostics_every: int = 0, num_samples: int = 1000, num_warmup: int = 1000, target_accept: float = 0.8)[source]¶
Config for the
hmcsampler, co-located with the method (ADR-0006).Reuses the global
population_sizeas the number of independent NUTS chains (so the samples-file chain namingiter<draw>run<chain>and the per-chain diagnostics carry over unchanged) and adds the three NUTS knobs on top of the shared MCMC family fields. Window adaptation replaces the gradient-freeburn_in/sample_everythinning, so those inherited keys are unused byhmc(NUTS draws are near-independent – every post-warmup draw is kept).- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- num_samples: int¶
Post-warmup draws kept per chain (each becomes one samples.txt row).
- num_warmup: int¶
Window-adaptation (warmup) steps per chain – dual-averaging step size + mass matrix.
- target_accept: float¶
NUTS dual-averaging target acceptance probability (Stan-like default).
- class pybnf.algorithms.samplers.hmc.HMCSampler(config)[source]¶
blackjax NUTS on the analytical model’s JAX log-density (ADR-0059).
Subclasses
BayesianAlgorithmto reuse prior loading, the samples-file setup and writer (start_run/sample_pset), and the R-hat/ESS diagnostics; overridesrun()to drive NUTS in process instead of the dask score-column loop.- divergences¶
Post-warmup divergent-transition count per chain (filled in by run()). A NUTS-specific reliability signal the gradient-free samplers have no analogue for: divergences flag regions the leapfrog integrator cannot traverse (a too-sharp curvature for the tuned step size), so a nonzero count – like a high split-R-hat – means HMC’s own draws are not yet trustworthy on this geometry (ADR-0059 gates the reference on its own diagnostics: split-R-hat / ESS / divergences).
- run(client=None, resume=None, debug=False)[source]¶
Run blackjax NUTS in process and write draws in the standard samples format.
client(the dask client the harness passes every sampler) is intentionally ignored: a single analytical NUTS chain is a tight numeric loop and the chains run as independent blackjax runs, so there is no per-pset dispatch (ADR-0059 item 1).
Model checking¶
ModelCheck – the check fit type, a first-class checking method.
A checking run (statistical model checking): evaluates the objective value and
constraint satisfaction for the given parameters without searching parameter
space. It registers in the checker family – a peer of optimizer and
sampler, not a utility afterthought – but, being neither an optimizer nor a
sampler, it lives at the algorithms package top level rather than under
optimizers/ or samplers/. Extracted byte-identical (M1 Step 4). It does not
subclass Algorithm, but it does use the execution machinery: the patched names
are resolved as core.Job / core.run_job through the core module object (ADR-0001
seam), and ConstraintCounter is read from this module’s namespace (tests patch
it here).