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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ set(SPDLOG_INSTALL ON)
FetchContent_Declare(
respond
GIT_REPOSITORY https://github.com/SyndemicsLab/respond.git
GIT_TAG 2bc260ef749a76b7eba73a4d64e9d38c48ed2bdc # v2.5.1
GIT_TAG cd2c0061a5e7ce27d7bdec17eacf96ef6fc5ad75 # main (Post PR 153)
OVERRIDE_FIND_PACKAGE
)
set(RESPOND_BUILD_DOCS OFF)
Expand Down
234 changes: 184 additions & 50 deletions benchmarks/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,53 +159,188 @@
);
"""

_SEED_DATA = """
INSERT INTO cohort (id, description, background_mortality_sample, behavior_transition_sample, initial_population_sample, intervention_transition_sample, overdose_sample, overdose_fatality_sample, population_change_sample, smr_sample)
VALUES (1, "Benchmark Cohort", 1, 1, 1, 1, 1, 1, 1, 1);

INSERT INTO intervention (id, name)
VALUES (1, "no_treatment"), (2, "early_buprenorphine"), (3, "buprenorphine"), (4, "post_buprenorphine");

INSERT INTO behavior (id, name)
VALUES (1, "active_injection"), (2, "nonactive_injection");

INSERT INTO initial_population (sample, intervention, behavior, count)
VALUES (1,1,1,100),(1,1,2,150),(1,2,1,200),(1,2,2,250),(1,3,1,0),(1,3,2,0),(1,4,1,0),(1,4,2,0);

INSERT INTO population_change (sample, intervention, behavior, time, count)
VALUES (1,1,1,1,100),(1,1,2,1,150),(1,2,1,1,200),(1,2,2,1,250),(1,3,1,1,0),(1,3,2,1,0),(1,4,1,1,0),(1,4,2,1,0);

INSERT INTO intervention_transition (sample, behavior, time, initial_intervention, new_intervention, probability)
VALUES (1,1,1,1,1,0.8),(1,2,1,1,1,0.7),(1,1,1,1,2,0.2),(1,2,1,1,2,0.3),
(1,1,1,1,3,0.0),(1,2,1,1,3,0.0),(1,1,1,1,4,0.0),(1,2,1,1,4,0.0),
(1,1,1,2,1,0.0),(1,2,1,2,1,0.0),(1,1,1,2,2,0.7),(1,2,1,2,2,0.6),
(1,1,1,2,3,0.2),(1,2,1,2,3,0.1),(1,1,1,2,4,0.1),(1,2,1,2,4,0.3),
(1,1,1,3,1,0.0),(1,2,1,3,1,0.0),(1,1,1,3,2,0.0),(1,2,1,3,2,0.0),
(1,1,1,3,3,0.8),(1,2,1,3,3,0.8),(1,1,1,3,4,0.2),(1,2,1,3,4,0.2),
(1,1,1,4,1,0.8),(1,2,1,4,1,0.8),(1,1,1,4,2,0.0),(1,2,1,4,2,0.0),
(1,1,1,4,3,0.0),(1,2,1,4,3,0.0),(1,1,1,4,4,0.2),(1,2,1,4,4,0.2);

INSERT INTO behavior_transition (sample, intervention, time, initial_behavior, new_behavior, probability)
VALUES (1,1,1,1,1,0.8),(1,1,1,1,2,0.2),(1,1,1,2,1,0.1),(1,1,1,2,2,0.9),
(1,2,1,1,1,0.9),(1,2,1,1,2,0.1),(1,2,1,2,1,0.7),(1,2,1,2,2,0.3),
(1,3,1,1,1,0.3),(1,3,1,1,2,0.7),(1,3,1,2,1,0.4),(1,3,1,2,2,0.6),
(1,4,1,1,1,0.3),(1,4,1,1,2,0.7),(1,4,1,2,1,0.2),(1,4,1,2,2,0.8);

INSERT INTO smr (sample, intervention, behavior, time, ratio)
VALUES (1,1,1,1,2.0),(1,1,2,1,2.1),(1,2,1,1,2.0),(1,2,2,1,2.1),
(1,3,1,1,2.0),(1,3,2,1,2.1),(1,4,1,1,2.0),(1,4,2,1,2.1);

INSERT INTO background_mortality (sample, time, probability)
VALUES (1,1,0.25);

INSERT INTO overdose (sample, intervention, behavior, time, probability)
VALUES (1,1,1,1,0.8),(1,1,2,1,0.7),(1,2,1,1,0.8),(1,2,2,1,0.7),
(1,3,1,1,0.8),(1,3,2,1,0.7),(1,4,1,1,0.8),(1,4,2,1,0.7);

INSERT INTO overdose_fatality (sample, intervention, behavior, time, probability)
VALUES (1,1,1,1,0.1),(1,1,2,1,0.2),(1,2,1,1,0.1),(1,2,2,1,0.2),
(1,3,1,1,0.1),(1,3,2,1,0.2),(1,4,1,1,0.1),(1,4,2,1,0.2);
"""

def _seed_benchmark_database(conn: sqlite3.Connection) -> None:
"""Seed a valid benchmark database across timesteps 1..52.

The benchmark is intentionally generated programmatically so that
time-varying rows exist for multiple parameter-change schedules without
requiring a huge static SQL file.
"""
cursor = conn.cursor()

cursor.execute(
"""
INSERT INTO cohort (
id, description, background_mortality_sample,
behavior_transition_sample, initial_population_sample,
intervention_transition_sample, overdose_sample,
overdose_fatality_sample, population_change_sample, smr_sample
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(1, "Benchmark Cohort", 1, 1, 1, 1, 1, 1, 1, 1),
)

cursor.executemany(
"INSERT INTO intervention (id, name) VALUES (?, ?)",
[
(1, "no_treatment"),
(2, "early_buprenorphine"),
(3, "buprenorphine"),
(4, "post_buprenorphine"),
],
)
cursor.executemany(
"INSERT INTO behavior (id, name) VALUES (?, ?)",
[
(1, "active_injection"),
(2, "nonactive_injection"),
],
)
cursor.executemany(
"INSERT INTO initial_population (sample, intervention, behavior, count) VALUES (?, ?, ?, ?)",
[
(1, 1, 1, 100),
(1, 1, 2, 150),
(1, 2, 1, 200),
(1, 2, 2, 250),
(1, 3, 1, 0),
(1, 3, 2, 0),
(1, 4, 1, 0),
(1, 4, 2, 0),
],
)

behavior_transition_rows = []
intervention_transition_rows = []
population_change_rows = []
smr_rows = []
background_mortality_rows = []
overdose_rows = []
overdose_fatality_rows = []

for t in range(1, 53):
population_change_rows.extend([
(1, 1, 1, t, 100),
(1, 1, 2, t, 150),
(1, 2, 1, t, 200),
(1, 2, 2, t, 250),
(1, 3, 1, t, 0),
(1, 3, 2, t, 0),
(1, 4, 1, t, 0),
(1, 4, 2, t, 0),
])

background_mortality_rows.append((1, t, 0.25))

smr_rows.extend([
(1, 1, 1, t, 2.0),
(1, 1, 2, t, 2.1),
(1, 2, 1, t, 2.0),
(1, 2, 2, t, 2.1),
(1, 3, 1, t, 2.0),
(1, 3, 2, t, 2.1),
(1, 4, 1, t, 2.0),
(1, 4, 2, t, 2.1),
])

overdose_rows.extend([
(1, 1, 1, t, 0.8),
(1, 1, 2, t, 0.7),
(1, 2, 1, t, 0.8),
(1, 2, 2, t, 0.7),
(1, 3, 1, t, 0.8),
(1, 3, 2, t, 0.7),
(1, 4, 1, t, 0.8),
(1, 4, 2, t, 0.7),
])

overdose_fatality_rows.extend([
(1, 1, 1, t, 0.1),
(1, 1, 2, t, 0.2),
(1, 2, 1, t, 0.1),
(1, 2, 2, t, 0.2),
(1, 3, 1, t, 0.1),
(1, 3, 2, t, 0.2),
(1, 4, 1, t, 0.1),
(1, 4, 2, t, 0.2),
])

for intervention in range(1, 5):
for initial_behavior in (1, 2):
for new_behavior in (1, 2):
behavior_transition_rows.append(
(
1,
intervention,
t,
initial_behavior,
new_behavior,
{
1: {(1, 1): 0.8, (1, 2): 0.2, (2, 1): 0.1, (2, 2): 0.9},
2: {(1, 1): 0.9, (1, 2): 0.1, (2, 1): 0.7, (2, 2): 0.3},
3: {(1, 1): 0.3, (1, 2): 0.7, (2, 1): 0.4, (2, 2): 0.6},
4: {(1, 1): 0.3, (1, 2): 0.7, (2, 1): 0.2, (2, 2): 0.8},
}[intervention][(initial_behavior, new_behavior)],
)
)

for behavior in (1, 2):
for initial_intervention in range(1, 5):
for new_intervention in range(1, 5):
intervention_transition_rows.append(
(
1,
behavior,
t,
initial_intervention,
new_intervention,
{
1: {
(1, 1): 0.8, (1, 2): 0.2, (1, 3): 0.0, (1, 4): 0.0,
(2, 1): 0.0, (2, 2): 0.7, (2, 3): 0.2, (2, 4): 0.1,
(3, 1): 0.0, (3, 2): 0.0, (3, 3): 0.8, (3, 4): 0.2,
(4, 1): 0.8, (4, 2): 0.0, (4, 3): 0.0, (4, 4): 0.2,
},
2: {
(1, 1): 0.7, (1, 2): 0.3, (1, 3): 0.0, (1, 4): 0.0,
(2, 1): 0.0, (2, 2): 0.6, (2, 3): 0.1, (2, 4): 0.3,
(3, 1): 0.0, (3, 2): 0.0, (3, 3): 0.8, (3, 4): 0.2,
(4, 1): 0.8, (4, 2): 0.0, (4, 3): 0.0, (4, 4): 0.2,
},
}[behavior][(initial_intervention, new_intervention)],
)
)

cursor.executemany(
"INSERT INTO population_change (sample, intervention, behavior, time, count) VALUES (?, ?, ?, ?, ?)",
population_change_rows,
)
cursor.executemany(
"INSERT INTO intervention_transition (sample, behavior, time, initial_intervention, new_intervention, probability) VALUES (?, ?, ?, ?, ?, ?)",
intervention_transition_rows,
)
cursor.executemany(
"INSERT INTO behavior_transition (sample, intervention, time, initial_behavior, new_behavior, probability) VALUES (?, ?, ?, ?, ?, ?)",
behavior_transition_rows,
)
cursor.executemany(
"INSERT INTO smr (sample, intervention, behavior, time, ratio) VALUES (?, ?, ?, ?, ?)",
smr_rows,
)
cursor.executemany(
"INSERT INTO background_mortality (sample, time, probability) VALUES (?, ?, ?)",
background_mortality_rows,
)
cursor.executemany(
"INSERT INTO overdose (sample, intervention, behavior, time, probability) VALUES (?, ?, ?, ?, ?)",
overdose_rows,
)
cursor.executemany(
"INSERT INTO overdose_fatality (sample, intervention, behavior, time, probability) VALUES (?, ?, ?, ?, ?)",
overdose_fatality_rows,
)
conn.commit()


# ---------------------------------------------------------------------------
Expand All @@ -219,8 +354,7 @@ def benchmark_db(tmp_path_factory) -> Path:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.executescript(_DB_SCHEMA)
cursor.executescript(_SEED_DATA)
conn.commit()
_seed_benchmark_database(conn)
conn.close()
return db_path

Expand All @@ -232,7 +366,7 @@ def benchmark_config(tmp_path_factory) -> str:
cfg = ConfigParser()
cfg["simulation"] = {
"duration": "52",
"parameter_change_times": "52",
"parameter_change_times": "1",
"stratify_entering_cohort": "false",
}
cfg["output"] = {
Expand Down
26 changes: 15 additions & 11 deletions benchmarks/test_bench_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,21 @@


def _write_histories_to_csv(sim: Simulation, out_dir: Path) -> None:
"""Write each model's densified state histories to a separate CSV file.
"""Write each model's recorded state histories to a separate CSV file.

Output format per file:
history_name, timestep, state_0, state_1, ..., state_n

Files are named ``<model_name>_histories.csv`` and written to *out_dir*.
"""
all_model_histories = sim.get_model_histories()
for model_name, model_history in all_model_histories.items():
for model_idx, model_name in enumerate(sim.get_model_names()):
model_history = sim.get_model_history(model_idx)
rows: list[dict] = []
for hist_name, state_vectors in model_history.items():
for t, vec in enumerate(state_vectors):
row = {"history_name": hist_name, "timestep": t}
for hist_name, history in model_history.items():
state_map = history.get_state_map()
for timestep in history.get_recorded_timesteps():
vec = state_map[timestep]
row = {"history_name": hist_name, "timestep": int(timestep)}
row.update({f"state_{i}": float(v) for i, v in enumerate(vec)})
rows.append(row)
if rows:
Expand All @@ -68,14 +70,16 @@ def _write_histories_to_csv(sim: Simulation, out_dir: Path) -> None:
def test_bench_load_data(benchmark, benchmark_db, benchmark_config):
"""Benchmark SQLite data loading and Simulation construction.

Measures the time to call build_simulation() from scratch on each round,
including all database reads and model/transition setup.
Each round builds a fresh Input instance so the measurement captures the
cold load path rather than reusing cached parameter arrays or sample ids.
"""
input_data = Input(db_path=benchmark_db, conf_path=benchmark_config)

def _build_from_scratch():
input_data = Input(db_path=benchmark_db, conf_path=benchmark_config)
return build_simulation(input_data, cohort_ids=[1])

benchmark.pedantic(
build_simulation,
kwargs={'input_data': input_data, 'cohort_ids': [1]},
_build_from_scratch,
rounds=10,
iterations=1,
)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ explicit = true

[tool.pytest]
minversion = "9.0"
testpaths = ["tests"]
testpaths = ["tests", "benchmarks"]
addopts = [
"-ra",
"--strict",
Expand Down Expand Up @@ -211,7 +211,7 @@ ignore = [
]
typing-modules = ["respondpy._core"]
isort.required-imports = ["from __future__ import annotations"]
exclude = ["tests/*"]
exclude = ["tests/*", "benchmarks/*", "docs/*"]

[tool.ruff.lint.mccabe]
max-complexity = 13
Expand Down
18 changes: 11 additions & 7 deletions src/respondpy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from __future__ import annotations

import copy
from collections.abc import Sequence

from .data import Input, Parameter, ParameterType, validate_time_list
Expand Down Expand Up @@ -116,16 +117,19 @@ def build_model(

duration = int(input_data.config.get("simulation", "duration"))
schedule_times = [1, *change_times]
timestep_templates: dict[int, Timestep] = {}

for model_timestep in range(1, duration+1):
parameter_time = max(t for t in schedule_times if t <= model_timestep)
model.add_timestep(build_timestep(
input_data,
cohort_id,
parameter_time,
log_name=log_name,
log_file=log_file,
))
if parameter_time not in timestep_templates:
timestep_templates[parameter_time] = build_timestep(
input_data,
cohort_id,
parameter_time,
log_name=log_name,
log_file=log_file,
)
model.add_timestep(copy.copy(timestep_templates[parameter_time]))
return model


Expand Down
Loading