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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ jobs:
matrix:
include:
# Fast PR matrix
- os: ubuntu-latest
python: "3.14"
toxenv: base
- os: ubuntu-latest
python: "3.13"
toxenv: base
Expand All @@ -26,42 +29,42 @@ jobs:
python: "3.11"
toxenv: base
- os: ubuntu-latest
python: "3.11"
python: "3.13"
toxenv: visualization

# macOS sanity
- os: macos-latest
python: "3.11"
python: "3.13"
toxenv: mac

# Quality
- os: ubuntu-latest
python: "3.11"
python: "3.13"
toxenv: quality
- os: ubuntu-latest
python: "3.11"
python: "3.13"
toxenv: project
- os: ubuntu-latest
python: "3.11"
python: "3.13"
toxenv: doc
- os: ubuntu-latest
python: "3.11"
toxenv: migrate

- os: ubuntu-latest
python: "3.11"
python: "3.13"
toxenv: external-R
- os: ubuntu-latest
python: "3.11"
python: "3.13"
toxenv: external-other-simulators
- os: ubuntu-latest
python: "3.11"
python: "3.13"
toxenv: petab
- os: ubuntu-latest
python: "3.11"
python: "3.13"
toxenv: base-notebooks
- os: ubuntu-latest
python: "3.11"
python: "3.13"
toxenv: external-notebooks

steps:
Expand Down
283 changes: 175 additions & 108 deletions pyabc/inference_util/inference_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import uuid
from collections.abc import Callable
from datetime import datetime, timedelta
from functools import partial
from typing import TYPE_CHECKING

import numpy as np
Expand All @@ -23,6 +24,138 @@
logger = logging.getLogger('ABC')


def _simulate_one_from_prior(
model_prior: RV,
parameter_priors: list[Distribution],
models: list[Model],
summary_statistics: Callable,
):
"""Sample one particle from the prior."""
from ..population import Particle

# sample model
m = int(model_prior.rvs())
# sample parameter
theta = parameter_priors[m].rvs()
# simulate summary statistics
model_result = models[m].summary_statistics(0, theta, summary_statistics)
# sampled from prior, so all have uniform weight
weight = 1.0
# distance will be computed after initialization of the
# distance function
distance = np.inf
# all are happy and accepted
accepted = True

return Particle(
m=m,
parameter=theta,
weight=weight,
sum_stat=model_result.sum_stat,
distance=distance,
accepted=accepted,
proposal_id=0,
preliminary=False,
)


def _simulate_one(
*,
t: int,
m: np.ndarray,
p: np.ndarray,
model_prior: RV,
parameter_priors: list[Distribution],
model_perturbation_kernel: ModelPerturbationKernel,
transitions: list[Transition],
models: list[Model],
summary_statistics: Callable,
x_0: dict,
distance_function: Distance,
eps: Epsilon,
acceptor: Acceptor,
weight_function: Callable,
evaluate: bool,
proposal_id: int,
):
"""Sample one parameter and evaluate/simulate one particle."""
parameter = generate_valid_proposal(
t=t,
m=m,
p=p,
model_prior=model_prior,
parameter_priors=parameter_priors,
model_perturbation_kernel=model_perturbation_kernel,
transitions=transitions,
)
if evaluate:
particle = evaluate_proposal(
*parameter,
t=t,
models=models,
summary_statistics=summary_statistics,
distance_function=distance_function,
eps=eps,
acceptor=acceptor,
x_0=x_0,
weight_function=weight_function,
proposal_id=proposal_id,
)
else:
particle = only_simulate_data_for_proposal(
*parameter,
t=t,
models=models,
summary_statistics=summary_statistics,
weight_function=weight_function,
proposal_id=proposal_id,
)
return particle


def _prior_pdf(
m_ss: int,
theta_ss: Parameter,
model_prior: RV,
parameter_priors: list[Distribution],
) -> float:
"""Evaluate the prior density for a proposed sample."""
return model_prior.pmf(m_ss) * parameter_priors[m_ss].pdf(theta_ss)


def _transition_pdf(
m_ss: int,
theta_ss: Parameter,
transitions: list[Transition],
model_probabilities: pd.DataFrame,
model_perturbation_kernel: ModelPerturbationKernel,
) -> float:
"""Evaluate the transition density for a proposed sample."""
model_factor = sum(
row.p * model_perturbation_kernel.pmf(m_ss, m)
for m, row in model_probabilities.iterrows()
)
particle_factor = transitions[m_ss].pdf(theta_ss)

transition_pd = model_factor * particle_factor
if transition_pd == 0:
logger.debug('Transition density is zero!')
return transition_pd


def _weight_function(
m_ss: int,
theta_ss: Parameter,
acceptance_weight: float,
prior_pdf: Callable,
transition_pdf: Callable,
) -> float:
"""Calculate total weight from sampling and acceptance weight."""
prior_pd = prior_pdf(m_ss, theta_ss)
transition_pd = transition_pdf(m_ss, theta_ss)
return acceptance_weight * prior_pd / transition_pd


class AnalysisVars:
"""Contract object class for passing analysis variables.

Expand Down Expand Up @@ -97,38 +230,13 @@ def create_simulate_from_prior_function(
simulate_one:
A function that returns a sampled particle.
"""
# simulation function, simplifying some parts compared to later
from ..population import Particle

def simulate_one():
# sample model
m = int(model_prior.rvs())
# sample parameter
theta = parameter_priors[m].rvs()
# simulate summary statistics
model_result = models[m].summary_statistics(
0, theta, summary_statistics
)
# sampled from prior, so all have uniform weight
weight = 1.0
# distance will be computed after initialization of the
# distance function
distance = np.inf
# all are happy and accepted
accepted = True

return Particle(
m=m,
parameter=theta,
weight=weight,
sum_stat=model_result.sum_stat,
distance=distance,
accepted=accepted,
proposal_id=0,
preliminary=False,
)

return simulate_one
return partial(
_simulate_one_from_prior,
model_prior=model_prior,
parameter_priors=parameter_priors,
models=models,
summary_statistics=summary_statistics,
)


def generate_valid_proposal(
Expand Down Expand Up @@ -273,11 +381,11 @@ def create_prior_pdf(
prior_pdf: The prior density function.
"""

def prior_pdf(m_ss, theta_ss):
prior_pd = model_prior.pmf(m_ss) * parameter_priors[m_ss].pdf(theta_ss)
return prior_pd

return prior_pdf
return partial(
_prior_pdf,
model_prior=model_prior,
parameter_priors=parameter_priors,
)


def create_transition_pdf(
Expand All @@ -298,20 +406,12 @@ def create_transition_pdf(
transition_pdf: The transition density function.
"""

def transition_pdf(m_ss, theta_ss):
model_factor = sum(
row.p * model_perturbation_kernel.pmf(m_ss, m)
for m, row in model_probabilities.iterrows()
)
particle_factor = transitions[m_ss].pdf(theta_ss)

transition_pd = model_factor * particle_factor

if transition_pd == 0:
logger.debug('Transition density is zero!')
return transition_pd

return transition_pdf
return partial(
_transition_pdf,
transitions=transitions,
model_probabilities=model_probabilities,
model_perturbation_kernel=model_perturbation_kernel,
)


def create_weight_function(
Expand All @@ -332,27 +432,11 @@ def create_weight_function(
weight_function: The importance sample weight function.
"""

def weight_function(m_ss, theta_ss, acceptance_weight: float):
"""Calculate total weight, from sampling and acceptance weight.

Parameters
----------
m_ss: The model sample.
theta_ss: The parameter sample.
acceptance_weight: The acceptance weight sample. In most cases 1.

Returns
-------
weight: The total weight.
"""
# prior and transition density (can be equal)
prior_pd = prior_pdf(m_ss, theta_ss)
transition_pd = transition_pdf(m_ss, theta_ss)
# calculate weight
weight = acceptance_weight * prior_pd / transition_pd
return weight

return weight_function
return partial(
_weight_function,
prior_pdf=prior_pdf,
transition_pdf=transition_pdf,
)


def create_simulate_function(
Expand Down Expand Up @@ -431,42 +515,25 @@ def create_simulate_function(
prior_pdf=prior_pdf, transition_pdf=transition_pdf
)

# simulation function
def simulate_one():
parameter = generate_valid_proposal(
t=t,
m=m,
p=p,
model_prior=model_prior,
parameter_priors=parameter_priors,
model_perturbation_kernel=model_perturbation_kernel,
transitions=transitions,
)
if evaluate:
particle = evaluate_proposal(
*parameter,
t=t,
models=models,
summary_statistics=summary_statistics,
distance_function=distance_function,
eps=eps,
acceptor=acceptor,
x_0=x_0,
weight_function=weight_function,
proposal_id=proposal_id,
)
else:
particle = only_simulate_data_for_proposal(
*parameter,
t=t,
models=models,
summary_statistics=summary_statistics,
weight_function=weight_function,
proposal_id=proposal_id,
)
return particle

return simulate_one
return partial(
_simulate_one,
t=t,
m=m,
p=p,
model_prior=model_prior,
parameter_priors=parameter_priors,
model_perturbation_kernel=model_perturbation_kernel,
transitions=transitions,
models=models,
summary_statistics=summary_statistics,
x_0=x_0,
distance_function=distance_function,
eps=eps,
acceptor=acceptor,
weight_function=weight_function,
evaluate=evaluate,
proposal_id=proposal_id,
)


def only_simulate_data_for_proposal(
Expand Down
Loading