Skip to content

feat: Implement multiple model optimization#130

Draft
mhovd wants to merge 15 commits into
mainfrom
mmopt
Draft

feat: Implement multiple model optimization#130
mhovd wants to merge 15 commits into
mainfrom
mmopt

Conversation

@mhovd
Copy link
Copy Markdown
Collaborator

@mhovd mhovd commented May 16, 2025

Implements the MMopt algorithm for determining the optimal sample times

@mhovd
Copy link
Copy Markdown
Collaborator Author

mhovd commented May 16, 2025

This PR is a draft, as it requires the new changes to pharmsol

@github-actions
Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchmmopt
Testbedpmcore-runner
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
seconds (s)
(Result Δ%)
Upper Boundary
seconds (s)
(Limit %)
bimodal_ke_npag📈 view plot
🚷 view threshold
14.76 s
(-0.85%)Baseline: 14.88 s
25.35 s
(58.23%)
bimodal_ke_npod📈 view plot
🚷 view threshold
12.08 s
(+22.21%)Baseline: 9.88 s
13.59 s
(88.84%)
bimodal_ke_postprob📈 view plot
🚷 view threshold
3.70 s
(+9.81%)Baseline: 3.37 s
3.96 s
(93.37%)
🐰 View full continuous benchmarking report in Bencher

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 23, 2026

🐰 Bencher Report

Branchmmopt
Testbedmhovd-pgx
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
bimodal_ke_npag📈 view plot
🚷 view threshold
4,964.30 ms
(+0.48%)Baseline: 4,940.55 ms
5,168.07 ms
(96.06%)
bimodal_ke_npod📈 view plot
🚷 view threshold
1,406.10 ms
(0.00%)Baseline: 1,406.10 ms
3,865.83 ms
(36.37%)
bimodal_ke_postprob📈 view plot
🚷 view threshold
147.54 ms
(-65.52%)Baseline: 427.85 ms
1,862.47 ms
(7.92%)
🐰 View full continuous benchmarking report in Bencher

@mhovd mhovd marked this pull request as ready for review March 24, 2026 08:17
Copilot AI review requested due to automatic review settings March 24, 2026 08:17
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new mmopt module to compute Bayes-risk–based optimal sampling times (MMopt), plus examples and tests to validate core behavior and input validation.

Changes:

  • Introduces mmopt optimization and bayes_risk scoring APIs with MmoptResult.
  • Adds integration tests covering common scenarios and validation failures.
  • Adds runnable examples replicating (parts of) Bayard & Neely (2017) experiments.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/mmopt/mod.rs Implements MMopt search, Bayes risk calculation, and internal helpers/tests.
src/lib.rs Exposes the new mmopt module and re-exports it in the prelude.
tests/mmopt_tests.rs Adds integration tests for mmopt behavior and validation errors.
examples/mmopt.rs Example driver for paper replication (Section 4/6).
examples/bayes_risk.rs Example showing how to score a fixed design via bayes_risk.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mmopt/mod.rs
Comment on lines +114 to +121
return Err(anyhow::anyhow!(
"C({}, {}) = {} exceeds the maximum allowed combinations ({}). \
Reduce the number of candidate times or increase nsamp.",
times.len(),
nsamp,
n_combinations,
MAX_COMBINATIONS
));
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guidance in this error message is mathematically incorrect: increasing nsamp does not necessarily reduce C(m, nsamp) (it increases until nsamp reaches ~m/2). Consider rewording to avoid suggesting “increase nsamp” as a general remedy (e.g., only recommend reducing candidate times, or mention choosing nsamp farther from m/2).

Copilot uses AI. Check for mistakes.
Comment thread src/mmopt/mod.rs
.par_iter()
.map(|combo| {
let risk = calculate_risk(combo, &pred_matrix, &errormodel, &weights_vec);
(combo.clone(), risk)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This eagerly allocates all C(m, nsamp) combinations into memory and then clones each combo again inside the parallel map. Even with MAX_COMBINATIONS = 1_000_000, this can be very memory-heavy and slow. Consider iterating combinations lazily (streaming generator) and avoiding per-iteration cloning (e.g., keep a reference during reduction and only copy the best combo once at the end).

Suggested change
(combo.clone(), risk)
(combo, risk)

Copilot uses AI. Check for mistakes.
Comment thread src/mmopt/mod.rs
}

let weights_vec = weights.to_vec();

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstrings for mmopt/bayes_risk state that weights “must sum to ~1.0”, but the implementation only checks length. To keep the API contract consistent (and avoid returning Bayes risks on an invalid probability distribution), consider validating that weights are non-negative and sum to 1 within a tolerance, or relax/update the docs if normalization is intentionally left to callers.

Suggested change
// Validate that weights form a proper probability distribution
if weights_vec.iter().any(|w| *w < 0.0) {
return Err(anyhow::anyhow!(
"Weights must be non-negative, but found a negative value"
));
}
let weight_sum: f64 = weights_vec.iter().sum();
let tol: f64 = 1e-6;
if (weight_sum - 1.0).abs() > tol {
return Err(anyhow::anyhow!(
"Weights must sum to 1.0 within a tolerance of {}, but sum to {}",
tol,
weight_sum
));
}

Copilot uses AI. Check for mistakes.
Comment thread tests/mmopt_tests.rs
Comment on lines +238 to +250
// Only proceed if the subject actually has multiple occasions
if subject.occasions().len() > 1 {
let result = mmopt(
&theta,
&subject,
eq,
additive_error_model(),
0,
1,
&Weights::from_vec(vec![0.5, 0.5]),
);
assert!(result.is_err());
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if subject.occasions().len() > 1 guard can cause the test to silently pass without asserting anything if the builder behavior changes (or if occasions are unexpectedly collapsed). It’s safer to assert the precondition (e.g., assert!(... > 1)) and then always check that mmopt returns an error.

Suggested change
// Only proceed if the subject actually has multiple occasions
if subject.occasions().len() > 1 {
let result = mmopt(
&theta,
&subject,
eq,
additive_error_model(),
0,
1,
&Weights::from_vec(vec![0.5, 0.5]),
);
assert!(result.is_err());
}
// Assert the precondition that the subject has multiple occasions
assert!(subject.occasions().len() > 1);
let result = mmopt(
&theta,
&subject,
eq,
additive_error_model(),
0,
1,
&Weights::from_vec(vec![0.5, 0.5]),
);
assert!(result.is_err());

Copilot uses AI. Check for mistakes.
Comment thread examples/mmopt.rs
Comment on lines +94 to +97
/// Paper results (Table 6.2):
/// n=1: t* = {4.25}, Bayes Risk = 0.5474
/// n=2: t* = {1.0, 9.5}, Bayes Risk = 0.2947
/// n=3: t* = {1.0, 1.0, 10.5}, Bayes Risk = 0.2325
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The paper’s reported 3-sample design includes a repeated time (two samples at 1.0). The current mmopt implementation searches combinations without repetition, so it can never return duplicate times; this example text may therefore set expectations that can’t be met. Consider clarifying this limitation here (and/or updating mmopt to support repeated sampling times if that’s required for replication).

Suggested change
/// Paper results (Table 6.2):
/// n=1: t* = {4.25}, Bayes Risk = 0.5474
/// n=2: t* = {1.0, 9.5}, Bayes Risk = 0.2947
/// n=3: t* = {1.0, 1.0, 10.5}, Bayes Risk = 0.2325
/// Paper results (Table 6.2, allowing repeated sampling times):
/// n=1: t* = {4.25}, Bayes Risk = 0.5474
/// n=2: t* = {1.0, 9.5}, Bayes Risk = 0.2947
/// n=3: t* = {1.0, 1.0, 10.5}, Bayes Risk = 0.2325
///
/// Note: The current `mmopt` implementation searches over combinations of
/// distinct sampling times (no repetition). As a result, designs with
/// repeated times like the paper's n=3 optimum {1.0, 1.0, 10.5} cannot be
/// reproduced exactly here; instead, we obtain the best 3-point design with
/// all sampling times distinct.

Copilot uses AI. Check for mistakes.
Comment thread src/mmopt/mod.rs
@@ -0,0 +1,502 @@
use anyhow::{Ok, Result};
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anyhow does not export Ok; importing it here will fail to compile. Import only anyhow::Result (or just use the Ok(...) variant without importing it).

Suggested change
use anyhow::{Ok, Result};
use anyhow::Result;

Copilot uses AI. Check for mistakes.
@mhovd
Copy link
Copy Markdown
Collaborator Author

mhovd commented Apr 1, 2026

We need to discuss if this should be allowed to run on multiple subjects, and if the cost should be weighted

@mhovd mhovd marked this pull request as draft April 1, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants