vpop-calibration 2.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vpop_calibration/__init__.py +22 -0
- vpop_calibration/data_generation.py +186 -0
- vpop_calibration/diagnostics.py +162 -0
- vpop_calibration/model/__init__.py +3 -0
- vpop_calibration/model/data.py +420 -0
- vpop_calibration/model/gp.py +517 -0
- vpop_calibration/model/plot.py +243 -0
- vpop_calibration/nlme.py +840 -0
- vpop_calibration/ode.py +203 -0
- vpop_calibration/saem.py +945 -0
- vpop_calibration/structural_model.py +200 -0
- vpop_calibration/test/__init__.py +11 -0
- vpop_calibration/test/test_data.py +21 -0
- vpop_calibration/test/test_gp_flavors.py +89 -0
- vpop_calibration/test/test_gp_saem.py +175 -0
- vpop_calibration/test/test_ode_saem.py +121 -0
- vpop_calibration/utils.py +9 -0
- vpop_calibration/vpop.py +50 -0
- vpop_calibration-2.2.8.dist-info/METADATA +78 -0
- vpop_calibration-2.2.8.dist-info/RECORD +22 -0
- vpop_calibration-2.2.8.dist-info/WHEEL +4 -0
- vpop_calibration-2.2.8.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from .model.gp import GP
|
|
7
|
+
from .ode import OdeModel
|
|
8
|
+
from .utils import device
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StructuralModel:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
parameter_names,
|
|
15
|
+
output_names,
|
|
16
|
+
protocol_arms,
|
|
17
|
+
tasks,
|
|
18
|
+
task_idx_to_output_idx,
|
|
19
|
+
task_idx_to_protocol,
|
|
20
|
+
):
|
|
21
|
+
"""Initialize a structural model
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
parameter_names (list[str]): _description_
|
|
25
|
+
output_names (list[str]): _description_
|
|
26
|
+
protocol_arms (list[str]): _description_
|
|
27
|
+
tasks (list[str]): _description_
|
|
28
|
+
task_idx_to_output_idx (list[str]): _description_
|
|
29
|
+
task_idx_to_protocol (list[str]): _description_
|
|
30
|
+
"""
|
|
31
|
+
self.parameter_names: list[str] = parameter_names
|
|
32
|
+
self.nb_parameters: int = len(self.parameter_names)
|
|
33
|
+
self.output_names: list[str] = output_names
|
|
34
|
+
self.nb_outputs: int = len(self.output_names)
|
|
35
|
+
self.protocols: list[str] = protocol_arms
|
|
36
|
+
self.nb_protocols: int = len(self.protocols)
|
|
37
|
+
self.tasks: list[str] = tasks
|
|
38
|
+
self.task_idx_to_output_idx: dict[int, int] = task_idx_to_output_idx
|
|
39
|
+
self.task_idx_to_protocol: dict[int, str] = task_idx_to_protocol
|
|
40
|
+
|
|
41
|
+
def simulate(
|
|
42
|
+
self,
|
|
43
|
+
X: torch.Tensor,
|
|
44
|
+
prediction_index: tuple[torch.Tensor, torch.Tensor, torch.Tensor],
|
|
45
|
+
chunks: list[int],
|
|
46
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
47
|
+
raise ValueError("Not implemented")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class StructuralGp(StructuralModel):
|
|
51
|
+
def __init__(self, gp_model: GP):
|
|
52
|
+
"""Create a structural model from a GP
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
gp_model (GP): The trained GP
|
|
56
|
+
"""
|
|
57
|
+
# list the GP parameters, except time, as it will be handled differently in the NLME model
|
|
58
|
+
parameter_names = [p for p in gp_model.data.parameter_names if p != "time"]
|
|
59
|
+
super().__init__(
|
|
60
|
+
parameter_names,
|
|
61
|
+
gp_model.data.output_names,
|
|
62
|
+
gp_model.data.protocol_arms,
|
|
63
|
+
gp_model.data.tasks,
|
|
64
|
+
gp_model.data.task_idx_to_output_idx,
|
|
65
|
+
gp_model.data.task_idx_to_protocol,
|
|
66
|
+
)
|
|
67
|
+
self.gp_model = gp_model
|
|
68
|
+
|
|
69
|
+
def simulate(
|
|
70
|
+
self,
|
|
71
|
+
X: torch.Tensor,
|
|
72
|
+
prediction_index: tuple[torch.Tensor, torch.Tensor, torch.Tensor],
|
|
73
|
+
chunks: list[int],
|
|
74
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
75
|
+
# X contains [nb_patients, nb_timesteps, nb_params + 1]
|
|
76
|
+
# Simulate the GP
|
|
77
|
+
(nb_patients, nb_timesteps, nb_params) = X.shape
|
|
78
|
+
X_vertical = X.view(-1, nb_params)
|
|
79
|
+
out_cat, var_cat = self.gp_model.predict_wide_scaled(X_vertical)
|
|
80
|
+
out_wide = out_cat.view(nb_patients, nb_timesteps, -1)
|
|
81
|
+
var_wide = var_cat.view(nb_patients, nb_timesteps, -1)
|
|
82
|
+
|
|
83
|
+
# Retrieve the necessary rows and columns to transform into a single column tensor
|
|
84
|
+
y = out_wide[prediction_index]
|
|
85
|
+
var = var_wide[prediction_index]
|
|
86
|
+
return y, var
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class StructuralOdeModel(StructuralModel):
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
ode_model: OdeModel,
|
|
93
|
+
protocol_design: pd.DataFrame,
|
|
94
|
+
init_conditions: np.ndarray,
|
|
95
|
+
):
|
|
96
|
+
self.ode_model = ode_model
|
|
97
|
+
protocol_arms = protocol_design["protocol_arm"].drop_duplicates().to_list()
|
|
98
|
+
self.protocol_design = protocol_design
|
|
99
|
+
output_names: list[str] = self.ode_model.variable_names
|
|
100
|
+
tasks: list[str] = [
|
|
101
|
+
output + "_" + protocol
|
|
102
|
+
for protocol in protocol_arms
|
|
103
|
+
for output in output_names
|
|
104
|
+
]
|
|
105
|
+
# Map tasks to output names
|
|
106
|
+
task_to_output = {
|
|
107
|
+
output_name + "_" + protocol_arm: output_name
|
|
108
|
+
for output_name in output_names
|
|
109
|
+
for protocol_arm in protocol_arms
|
|
110
|
+
}
|
|
111
|
+
# Map task index to output index
|
|
112
|
+
task_idx_to_output_idx = {
|
|
113
|
+
tasks.index(k): output_names.index(v) for k, v in task_to_output.items()
|
|
114
|
+
}
|
|
115
|
+
# Map task to protocol arm
|
|
116
|
+
task_to_protocol = {
|
|
117
|
+
output_name + "_" + protocol_arm: protocol_arm
|
|
118
|
+
for output_name in output_names
|
|
119
|
+
for protocol_arm in protocol_arms
|
|
120
|
+
}
|
|
121
|
+
# Map task index to protocol arm
|
|
122
|
+
task_idx_to_protocol = {tasks.index(k): v for k, v in task_to_protocol.items()}
|
|
123
|
+
|
|
124
|
+
# list the structural model parameters: the protocol overrides are ignored
|
|
125
|
+
self.protocol_overrides = self.protocol_design.drop(
|
|
126
|
+
columns="protocol_arm"
|
|
127
|
+
).columns.to_list()
|
|
128
|
+
parameter_names = list(
|
|
129
|
+
set(self.ode_model.param_names) - set(self.protocol_overrides)
|
|
130
|
+
)
|
|
131
|
+
self.nb_protocol_overrides = len(self.protocol_overrides)
|
|
132
|
+
|
|
133
|
+
super().__init__(
|
|
134
|
+
parameter_names,
|
|
135
|
+
output_names,
|
|
136
|
+
protocol_arms,
|
|
137
|
+
tasks,
|
|
138
|
+
task_idx_to_output_idx,
|
|
139
|
+
task_idx_to_protocol,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
self.init_cond_df = pd.DataFrame(
|
|
143
|
+
data=[init_conditions], columns=self.ode_model.initial_cond_names
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def simulate(
|
|
147
|
+
self,
|
|
148
|
+
X: torch.Tensor,
|
|
149
|
+
prediction_index: tuple[torch.Tensor, torch.Tensor, torch.Tensor],
|
|
150
|
+
chunks: list[int],
|
|
151
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
152
|
+
(nb_patients, nb_timesteps, nb_params) = X.shape
|
|
153
|
+
list_X = [ind_X for ind_X in X]
|
|
154
|
+
patient_index_full, rows_full, tasks_full = prediction_index
|
|
155
|
+
list_rows = torch.split(rows_full, chunks)
|
|
156
|
+
list_tasks = torch.split(tasks_full, chunks)
|
|
157
|
+
|
|
158
|
+
input_df_list = []
|
|
159
|
+
for ind_X, ind_rows, ind_tasks in zip(list_X, list_rows, list_tasks):
|
|
160
|
+
temp_id = str(uuid.uuid4())
|
|
161
|
+
# Extract the parameters and time values
|
|
162
|
+
params = ind_X.index_select(0, ind_rows).cpu().detach().numpy()
|
|
163
|
+
# Extract the task order
|
|
164
|
+
task_index = ind_tasks.cpu().detach().numpy()
|
|
165
|
+
# Format the data inputs
|
|
166
|
+
# This step is where the order of parameters is implicit
|
|
167
|
+
input_df_temp = pd.DataFrame(
|
|
168
|
+
data=params, columns=self.parameter_names + ["time"]
|
|
169
|
+
)
|
|
170
|
+
# The passed params include the _global_ time steps
|
|
171
|
+
# Filter the time steps that we actually want for this patient
|
|
172
|
+
input_df_temp = input_df_temp.iloc[ind_rows.cpu().numpy()]
|
|
173
|
+
# Add the task index as a temporary column
|
|
174
|
+
input_df_temp["task_index"] = task_index
|
|
175
|
+
# Deduce protocol arm and output name from task index
|
|
176
|
+
input_df_temp["protocol_arm"] = input_df_temp["task_index"].apply(
|
|
177
|
+
lambda t: self.task_idx_to_protocol[t]
|
|
178
|
+
)
|
|
179
|
+
input_df_temp["output_name"] = input_df_temp["task_index"].apply(
|
|
180
|
+
lambda t: self.output_names[self.task_idx_to_output_idx[t]]
|
|
181
|
+
)
|
|
182
|
+
# Remove the unnecessary task index column
|
|
183
|
+
input_df_temp = input_df_temp.drop(columns=["task_index"])
|
|
184
|
+
input_df_temp["id"] = temp_id
|
|
185
|
+
# Add the protocol overrides
|
|
186
|
+
if self.nb_protocol_overrides > 0:
|
|
187
|
+
input_df_temp = input_df_temp.merge(
|
|
188
|
+
self.protocol_design, how="left", on=["protocol_arm"]
|
|
189
|
+
)
|
|
190
|
+
# Add the initial conditions
|
|
191
|
+
input_df_temp = input_df_temp.merge(self.init_cond_df, how="cross")
|
|
192
|
+
input_df_list.append(input_df_temp)
|
|
193
|
+
|
|
194
|
+
full_input = pd.concat(input_df_list)
|
|
195
|
+
# Simulate the ODE model
|
|
196
|
+
output_df = self.ode_model.simulate_model(full_input)
|
|
197
|
+
# Convert back to tensor
|
|
198
|
+
out_tensor = torch.as_tensor(output_df["predicted_value"].values, device=device)
|
|
199
|
+
out_var = torch.zeros_like(out_tensor, device=device)
|
|
200
|
+
return out_tensor, out_var
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
|
|
4
|
+
# Initialize the seeds for all random operators used in the tests
|
|
5
|
+
np_rng = np.random.default_rng(42)
|
|
6
|
+
np.random.seed(42)
|
|
7
|
+
torch.manual_seed(0)
|
|
8
|
+
saem_mi_maxfun = 1
|
|
9
|
+
multithreaded = False
|
|
10
|
+
|
|
11
|
+
__all__ = ["np_rng", "saem_mi_maxfun", "multithreaded"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from vpop_calibration.model.data import TrainingDataSet
|
|
4
|
+
from vpop_calibration.test import *
|
|
5
|
+
|
|
6
|
+
training_df = pd.DataFrame(
|
|
7
|
+
{
|
|
8
|
+
"id": ["1", "1", "2", "2"],
|
|
9
|
+
"protocol_arm": ["arm-A"] * 4,
|
|
10
|
+
"output_name": ["s1", "s2", "s1", "s2"],
|
|
11
|
+
"k1": [1.0, 1.0, 2.0, 2.0],
|
|
12
|
+
"value": [0.0, 1.0, 2.0, 3.0],
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_loading():
|
|
18
|
+
TrainingDataSet(training_df, ["k1"], 1.0)
|
|
19
|
+
TrainingDataSet(training_df, ["k1"], 1.0, data_already_normalized=True)
|
|
20
|
+
TrainingDataSet(training_df, ["k1"], 1.0, log_inputs=["k1"], log_outputs=["s1"])
|
|
21
|
+
TrainingDataSet(training_df.drop(columns={"protocol_arm"}), ["k1"], 0.5)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pickle
|
|
4
|
+
|
|
5
|
+
from vpop_calibration import *
|
|
6
|
+
from vpop_calibration.test import *
|
|
7
|
+
|
|
8
|
+
# Create a dummy training data frame
|
|
9
|
+
patients = ["patient-01", "patient-02"]
|
|
10
|
+
nb_patients = len(patients)
|
|
11
|
+
obsIds = ["obs-01", "obs-02"]
|
|
12
|
+
protocol_arms = ["arm-A", "arm-B"]
|
|
13
|
+
time_steps = np.arange(0, 10.0, 1.0)
|
|
14
|
+
patient_descriptors = ["k1", "k2", "k3"]
|
|
15
|
+
gp_params = [*patient_descriptors, "time"]
|
|
16
|
+
training_df = pd.DataFrame({"id": patients})
|
|
17
|
+
for descriptor in patient_descriptors:
|
|
18
|
+
training_df[descriptor] = np_rng.normal(0, 1, nb_patients)
|
|
19
|
+
training_df = training_df.merge(
|
|
20
|
+
pd.DataFrame({"protocol_arm": protocol_arms}), how="cross"
|
|
21
|
+
)
|
|
22
|
+
training_df = training_df.merge(pd.DataFrame({"time": time_steps}), how="cross")
|
|
23
|
+
training_df = training_df.merge(pd.DataFrame({"output_name": obsIds}), how="cross")
|
|
24
|
+
training_df["value"] = np_rng.normal(0, 1, training_df.shape[0])
|
|
25
|
+
training_df_bootstrapped = training_df.sample(frac=0.5, random_state=np_rng)
|
|
26
|
+
|
|
27
|
+
implemented_kernels = ["RBF", "SMK", "Matern"]
|
|
28
|
+
implemented_var_strat = ["IMV", "LMCV"]
|
|
29
|
+
implemented_mll = ["ELBO", "PLL"]
|
|
30
|
+
deep_or_not = [True, False]
|
|
31
|
+
|
|
32
|
+
model_file = "vpop_calibration/test/gp_model_for_tests.pkl"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def gp_init_flavor(var_strat, kernel, deep_kernel, mll):
|
|
36
|
+
gp = GP(
|
|
37
|
+
training_df,
|
|
38
|
+
gp_params,
|
|
39
|
+
var_strat=var_strat,
|
|
40
|
+
mll=mll,
|
|
41
|
+
kernel=kernel,
|
|
42
|
+
deep_kernel=deep_kernel,
|
|
43
|
+
nb_latents=2,
|
|
44
|
+
nb_features=5,
|
|
45
|
+
num_mixtures=3,
|
|
46
|
+
nb_training_iter=2,
|
|
47
|
+
)
|
|
48
|
+
gp.train()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_all_gp_flavors():
|
|
52
|
+
for deep_kernel in deep_or_not:
|
|
53
|
+
for kernel in implemented_kernels:
|
|
54
|
+
for var_strat in implemented_var_strat:
|
|
55
|
+
for mll in implemented_mll:
|
|
56
|
+
gp_init_flavor(var_strat, kernel, deep_kernel, mll)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_batching_1():
|
|
60
|
+
gp = GP(training_df, gp_params, nb_training_iter=2)
|
|
61
|
+
gp.train(mini_batching=True, mini_batch_size=8)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_batching_2():
|
|
65
|
+
gp = GP(training_df, gp_params, nb_training_iter=2)
|
|
66
|
+
gp.train(mini_batching=True, mini_batch_size=None)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_eval_with_valid():
|
|
70
|
+
gp = GP(training_df, gp_params, nb_training_iter=2)
|
|
71
|
+
gp.eval_perf()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_eval_no_valid():
|
|
75
|
+
gp = GP(training_df, gp_params, nb_training_iter=2, training_proportion=1)
|
|
76
|
+
gp.eval_perf()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_gp_incomplete_data():
|
|
80
|
+
gp = GP(training_df_bootstrapped, gp_params, nb_training_iter=2)
|
|
81
|
+
gp.train()
|
|
82
|
+
gp.train(mini_batching=True, mini_batch_size=8)
|
|
83
|
+
gp.eval_perf()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_gp_pickle():
|
|
87
|
+
gp = GP(training_df, gp_params)
|
|
88
|
+
with open(model_file, "wb") as file:
|
|
89
|
+
pickle.dump(gp, file)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import pickle
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from vpop_calibration import *
|
|
7
|
+
from vpop_calibration.test import *
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def equations_with_abs(t, y, k_a, k_12, k_21, k_el):
|
|
11
|
+
# y[0] is A_absorption, y[1] is A_central, y[2] is A_peripheral
|
|
12
|
+
A_absorption, A_central, A_peripheral = y[0], y[1], y[2]
|
|
13
|
+
dA_absorption_dt = -k_a * A_absorption
|
|
14
|
+
dA_central_dt = (
|
|
15
|
+
k_a * A_absorption + k_21 * A_peripheral - k_12 * A_central - k_el * A_central
|
|
16
|
+
)
|
|
17
|
+
dA_peripheral_dt = k_12 * A_central - k_21 * A_peripheral
|
|
18
|
+
|
|
19
|
+
ydot = [dA_absorption_dt, dA_central_dt, dA_peripheral_dt]
|
|
20
|
+
return ydot
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
variable_names = ["A0", "A1", "A2"]
|
|
24
|
+
parameter_names = ["k_a", "k_12", "k_21", "k_el"]
|
|
25
|
+
|
|
26
|
+
tmax = 24.0
|
|
27
|
+
initial_conditions = np.array([10.0, 0.0, 0.0])
|
|
28
|
+
|
|
29
|
+
protocol_design = pd.DataFrame(
|
|
30
|
+
{"protocol_arm": ["arm-A", "arm-B"], "k_el": [0.5, 10.0]}
|
|
31
|
+
)
|
|
32
|
+
nb_protocols = len(protocol_design)
|
|
33
|
+
|
|
34
|
+
pk_two_compartments_model = OdeModel(
|
|
35
|
+
equations_with_abs, variable_names, parameter_names, multithreaded=multithreaded
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
model_file = "vpop_calibration/test/gp_model_for_tests.pkl"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_gp_training():
|
|
42
|
+
# Define the ode model
|
|
43
|
+
|
|
44
|
+
nb_timesteps = 3
|
|
45
|
+
time_steps = np.linspace(0.0, tmax, nb_timesteps)
|
|
46
|
+
|
|
47
|
+
log_nb_patients = 1
|
|
48
|
+
param_ranges = {
|
|
49
|
+
"k_12": {"low": -2.0, "high": 0.0, "log": True},
|
|
50
|
+
"k_21": {"low": -1.0, "high": 0.3, "log": True},
|
|
51
|
+
"k_a": {"low": -1.0, "high": 0.0, "log": True},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
dataset = simulate_dataset_from_ranges(
|
|
55
|
+
pk_two_compartments_model,
|
|
56
|
+
log_nb_patients,
|
|
57
|
+
param_ranges,
|
|
58
|
+
initial_conditions,
|
|
59
|
+
protocol_design,
|
|
60
|
+
None,
|
|
61
|
+
None,
|
|
62
|
+
time_steps,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
learned_ode_params = list(param_ranges.keys())
|
|
66
|
+
descriptors = learned_ode_params + ["time"]
|
|
67
|
+
|
|
68
|
+
# initiate our GP class
|
|
69
|
+
myGP = GP(
|
|
70
|
+
dataset,
|
|
71
|
+
descriptors,
|
|
72
|
+
var_strat="IMV", # either IMV (Independent Multitask Variational) or LMCV (Linear Model of Coregionalization Variational)
|
|
73
|
+
kernel="RBF", # Either RBF or SMK
|
|
74
|
+
data_already_normalized=False, # default
|
|
75
|
+
nb_inducing_points=10,
|
|
76
|
+
mll="ELBO", # default, otherwise PLL
|
|
77
|
+
nb_training_iter=1,
|
|
78
|
+
training_proportion=0.7,
|
|
79
|
+
learning_rate=0.1,
|
|
80
|
+
lr_decay=0.99,
|
|
81
|
+
jitter=1e-6,
|
|
82
|
+
log_inputs=learned_ode_params,
|
|
83
|
+
)
|
|
84
|
+
myGP.train()
|
|
85
|
+
myGP.plot_loss()
|
|
86
|
+
myGP.plot_obs_vs_predicted("training")
|
|
87
|
+
myGP.plot_individual_solution(0)
|
|
88
|
+
myGP.plot_all_solutions("training")
|
|
89
|
+
with open(model_file, "wb") as file:
|
|
90
|
+
pickle.dump(myGP, file)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_gp_saem():
|
|
94
|
+
time_span_rw = (0, 24)
|
|
95
|
+
nb_steps_rw = 2
|
|
96
|
+
|
|
97
|
+
# For each output and for each patient, give a list of time steps to be simulated
|
|
98
|
+
time_steps_rw = np.linspace(time_span_rw[0], time_span_rw[1], nb_steps_rw).tolist()
|
|
99
|
+
|
|
100
|
+
# Parameter definitions
|
|
101
|
+
true_log_MI = {"k_21": 0.0}
|
|
102
|
+
true_log_PDU = {
|
|
103
|
+
"k_12": {"mean": -1.0, "sd": 0.25},
|
|
104
|
+
}
|
|
105
|
+
error_model_type = "additive"
|
|
106
|
+
true_res_var = [0.5, 0.02, 0.1]
|
|
107
|
+
true_covariate_map = {
|
|
108
|
+
"k_12": {"foo": {"coef": "cov_foo_k12", "value": 0.2}},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Create a patient data frame
|
|
112
|
+
# It should contain at the very minimum one `id` per patient
|
|
113
|
+
nb_patients = 3
|
|
114
|
+
patients_df = pd.DataFrame({"id": [str(uuid.uuid4()) for _ in range(nb_patients)]})
|
|
115
|
+
patients_df["protocol_arm"] = np_rng.binomial(1, 0.5, nb_patients)
|
|
116
|
+
patients_df["protocol_arm"] = patients_df["protocol_arm"].apply(
|
|
117
|
+
lambda x: "arm-A" if x == 0 else "arm-B"
|
|
118
|
+
)
|
|
119
|
+
patients_df["k_a"] = np_rng.lognormal(-1, 0.1, nb_patients)
|
|
120
|
+
patients_df["foo"] = np_rng.lognormal(0.1, 0.1, nb_patients)
|
|
121
|
+
|
|
122
|
+
print(f"Simulating {nb_patients} patients on {nb_protocols} protocol arms")
|
|
123
|
+
obs_df = simulate_dataset_from_omega(
|
|
124
|
+
pk_two_compartments_model,
|
|
125
|
+
protocol_design,
|
|
126
|
+
time_steps_rw,
|
|
127
|
+
initial_conditions,
|
|
128
|
+
true_log_MI,
|
|
129
|
+
true_log_PDU,
|
|
130
|
+
error_model_type,
|
|
131
|
+
true_res_var,
|
|
132
|
+
true_covariate_map,
|
|
133
|
+
patients_df,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Initial pop estimates
|
|
137
|
+
# Parameter definitions
|
|
138
|
+
init_log_MI = {"k_21": -1.0}
|
|
139
|
+
init_log_PDU = {
|
|
140
|
+
"k_12": {"mean": -0.1, "sd": 0.1},
|
|
141
|
+
}
|
|
142
|
+
error_model_type = "additive"
|
|
143
|
+
init_res_var = [0.1, 0.05, 0.5]
|
|
144
|
+
init_covariate_map = {
|
|
145
|
+
"k_12": {"foo": {"coef": "cov_foo_k12", "value": -0.1}},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
with open(model_file, "rb") as file:
|
|
149
|
+
myGP = pickle.load(file)
|
|
150
|
+
# Create a structural model
|
|
151
|
+
structural_gp = StructuralGp(myGP)
|
|
152
|
+
# Create a NLME moedl
|
|
153
|
+
nlme_surrogate = NlmeModel(
|
|
154
|
+
structural_gp,
|
|
155
|
+
patients_df,
|
|
156
|
+
init_log_MI,
|
|
157
|
+
init_log_PDU,
|
|
158
|
+
init_res_var,
|
|
159
|
+
init_covariate_map,
|
|
160
|
+
error_model_type,
|
|
161
|
+
)
|
|
162
|
+
# Create an optimizer: here we use SAEM
|
|
163
|
+
optimizer = PySaem(
|
|
164
|
+
nlme_surrogate,
|
|
165
|
+
obs_df,
|
|
166
|
+
nb_phase1_iterations=1,
|
|
167
|
+
nb_phase2_iterations=0,
|
|
168
|
+
optim_max_fun=saem_mi_maxfun,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
optimizer.run()
|
|
172
|
+
optimizer.continue_iterating(nb_add_iters_ph1=1, nb_add_iters_ph2=1)
|
|
173
|
+
optimizer.plot_convergence_history()
|
|
174
|
+
plot_map_estimates(nlme_surrogate)
|
|
175
|
+
check_surrogate_validity_gp(nlme_surrogate)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from vpop_calibration import *
|
|
6
|
+
from vpop_calibration.test import *
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def equations_with_abs(t, y, k_a, k_12, k_21, k_el):
|
|
10
|
+
# y[0] is A_absorption, y[1] is A_central, y[2] is A_peripheral
|
|
11
|
+
A_absorption, A_central, A_peripheral = y[0], y[1], y[2]
|
|
12
|
+
dA_absorption_dt = -k_a * A_absorption
|
|
13
|
+
dA_central_dt = (
|
|
14
|
+
k_a * A_absorption + k_21 * A_peripheral - k_12 * A_central - k_el * A_central
|
|
15
|
+
)
|
|
16
|
+
dA_peripheral_dt = k_12 * A_central - k_21 * A_peripheral
|
|
17
|
+
|
|
18
|
+
ydot = [dA_absorption_dt, dA_central_dt, dA_peripheral_dt]
|
|
19
|
+
return ydot
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
variable_names = ["A0", "A1", "A2"]
|
|
23
|
+
parameter_names = ["k_a", "k_12", "k_21", "k_el"]
|
|
24
|
+
|
|
25
|
+
tmax = 24.0
|
|
26
|
+
initial_conditions = np.array([10.0, 0.0, 0.0])
|
|
27
|
+
|
|
28
|
+
protocol_design = pd.DataFrame(
|
|
29
|
+
{"protocol_arm": ["arm-A", "arm-B"], "k_el": [0.5, 10.0]}
|
|
30
|
+
)
|
|
31
|
+
nb_protocols = len(protocol_design)
|
|
32
|
+
|
|
33
|
+
pk_two_compartments_model = OdeModel(
|
|
34
|
+
equations_with_abs, variable_names, parameter_names, multithreaded=multithreaded
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
model_file = "vpop_calibration/test/gp_model_for_tests.pkl"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_ode_saem():
|
|
41
|
+
time_span_rw = (0, 24)
|
|
42
|
+
nb_steps_rw = 2
|
|
43
|
+
|
|
44
|
+
# For each output and for each patient, give a list of time steps to be simulated
|
|
45
|
+
time_steps_rw = np.linspace(time_span_rw[0], time_span_rw[1], nb_steps_rw).tolist()
|
|
46
|
+
|
|
47
|
+
# Parameter definitions
|
|
48
|
+
true_log_MI = {"k_21": 0.0}
|
|
49
|
+
true_log_PDU = {
|
|
50
|
+
"k_12": {"mean": -1.0, "sd": 0.25},
|
|
51
|
+
}
|
|
52
|
+
error_model_type = "additive"
|
|
53
|
+
true_res_var = [0.5, 0.02, 0.1]
|
|
54
|
+
true_covariate_map = {
|
|
55
|
+
"k_12": {"foo": {"coef": "cov_foo_k12", "value": 0.2}},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Create a patient data frame
|
|
59
|
+
# It should contain at the very minimum one `id` per patient
|
|
60
|
+
nb_patients = 3
|
|
61
|
+
patients_df = pd.DataFrame({"id": [str(uuid.uuid4()) for _ in range(nb_patients)]})
|
|
62
|
+
patients_df["protocol_arm"] = np_rng.binomial(1, 0.5, nb_patients)
|
|
63
|
+
patients_df["protocol_arm"] = patients_df["protocol_arm"].apply(
|
|
64
|
+
lambda x: "arm-A" if x == 0 else "arm-B"
|
|
65
|
+
)
|
|
66
|
+
patients_df["k_a"] = np_rng.lognormal(-1, 0.1, nb_patients)
|
|
67
|
+
patients_df["foo"] = np_rng.lognormal(0.1, 0.1, nb_patients)
|
|
68
|
+
|
|
69
|
+
print(f"Simulating {nb_patients} patients on {nb_protocols} protocol arms")
|
|
70
|
+
obs_df = simulate_dataset_from_omega(
|
|
71
|
+
pk_two_compartments_model,
|
|
72
|
+
protocol_design,
|
|
73
|
+
time_steps_rw,
|
|
74
|
+
initial_conditions,
|
|
75
|
+
true_log_MI,
|
|
76
|
+
true_log_PDU,
|
|
77
|
+
error_model_type,
|
|
78
|
+
true_res_var,
|
|
79
|
+
true_covariate_map,
|
|
80
|
+
patients_df,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Initial pop estimates
|
|
84
|
+
# Parameter definitions
|
|
85
|
+
init_log_MI = {"k_12": -1.0}
|
|
86
|
+
init_log_PDU = {
|
|
87
|
+
"k_21": {"mean": -1.0, "sd": 0.2},
|
|
88
|
+
}
|
|
89
|
+
error_model_type = "additive"
|
|
90
|
+
init_res_var = [0.1, 0.05, 0.5]
|
|
91
|
+
init_covariate_map = {
|
|
92
|
+
"k_21": {"foo": {"coef": "cov_foo_k12", "value": -0.1}},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Create a structural model
|
|
96
|
+
structural_ode = StructuralOdeModel(
|
|
97
|
+
pk_two_compartments_model, protocol_design, initial_conditions
|
|
98
|
+
)
|
|
99
|
+
# Create a NLME moedl
|
|
100
|
+
nlme = NlmeModel(
|
|
101
|
+
structural_ode,
|
|
102
|
+
patients_df,
|
|
103
|
+
init_log_MI,
|
|
104
|
+
init_log_PDU,
|
|
105
|
+
init_res_var,
|
|
106
|
+
init_covariate_map,
|
|
107
|
+
error_model_type,
|
|
108
|
+
)
|
|
109
|
+
# Create an optimizer: here we use SAEM
|
|
110
|
+
optimizer = PySaem(
|
|
111
|
+
nlme,
|
|
112
|
+
obs_df,
|
|
113
|
+
nb_phase1_iterations=1,
|
|
114
|
+
nb_phase2_iterations=0,
|
|
115
|
+
optim_max_fun=saem_mi_maxfun,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
optimizer.run()
|
|
119
|
+
optimizer.continue_iterating(nb_add_iters_ph1=0, nb_add_iters_ph2=1)
|
|
120
|
+
optimizer.plot_convergence_history()
|
|
121
|
+
plot_map_estimates(nlme)
|
vpop_calibration/vpop.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import uuid
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.stats.qmc import Sobol, scale
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def generate_vpop_from_ranges(
|
|
8
|
+
log_nb_individuals: int, param_ranges: dict[str, dict[str, float | bool]]
|
|
9
|
+
) -> pd.DataFrame:
|
|
10
|
+
"""Generate a vpop of patients from parameter ranges using Sobol sequences
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
log_nb_individuals (int): The vpop size will be 2^log_nb_individuals
|
|
14
|
+
param_ranges (dict[str, dict[str, float | bool]]): One entry for each parameter to be explored
|
|
15
|
+
- `param_name`: {`low`: float, `high`: float, `log`: bool}. Turn `log` to true to define log-scaled ranges
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
pd.DataFrame: A set of patients with a generated `id`, and a column per descriptor
|
|
19
|
+
|
|
20
|
+
Note:
|
|
21
|
+
This method may be called with an empty dict, to return a list of patient ids.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
nb_individuals = np.power(2, log_nb_individuals)
|
|
25
|
+
params_to_explore = list(param_ranges.keys())
|
|
26
|
+
nb_parameters = len(params_to_explore)
|
|
27
|
+
if nb_parameters != 0:
|
|
28
|
+
|
|
29
|
+
# Create a sobol sampler to generate parameter values
|
|
30
|
+
sobol_engine = Sobol(d=nb_parameters, scramble=True)
|
|
31
|
+
sobol_sequence = sobol_engine.random_base2(log_nb_individuals)
|
|
32
|
+
samples = scale(
|
|
33
|
+
sobol_sequence,
|
|
34
|
+
[param_ranges[param_name]["low"] for param_name in params_to_explore],
|
|
35
|
+
[param_ranges[param_name]["high"] for param_name in params_to_explore],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Handle log-scaled parameters
|
|
39
|
+
for j, param_name in enumerate(params_to_explore):
|
|
40
|
+
if param_ranges[param_name]["log"] == True:
|
|
41
|
+
samples[:, j] = np.exp(samples[:, j])
|
|
42
|
+
# Create the full data frame of patient descriptors
|
|
43
|
+
patients_df = pd.DataFrame(data=samples, columns=params_to_explore)
|
|
44
|
+
else:
|
|
45
|
+
# No parameter requested, create empty data frame
|
|
46
|
+
patients_df = pd.DataFrame()
|
|
47
|
+
|
|
48
|
+
ids = [str(uuid.uuid4()) for _ in range(nb_individuals)]
|
|
49
|
+
patients_df.insert(0, "id", ids)
|
|
50
|
+
return patients_df
|