Skip to content
Open
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
27 changes: 25 additions & 2 deletions PKPD/gui/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ def __init__(self, title="", parent=None):

self.manual_state = False

# Set so open initially
self.on_pressed()

@QtCore.pyqtSlot()
def on_pressed(self):
checked = self.manual_state
Expand Down Expand Up @@ -760,7 +763,11 @@ def fill_parameter_slider_group(self):
# get parameter names
state_names = self.main_window.model.state_names
model_param_names = self.main_window.model.parameter_names # parameters except initial conditions
parameter_names = state_names + model_param_names # parameters including initial conditions

if self.main_window.model.infer_initial_conditions:
parameter_names = state_names + model_param_names # parameters including initial conditions
else:
parameter_names = model_param_names # parameters excluding initial conditions

# fill up grid with slider objects
# length of parameters so can fill up in correct order later
Expand Down Expand Up @@ -900,6 +907,7 @@ def _create_min_current_max_value_label(self, slider:QtWidgets.QSlider, paramete
decimal_places = 1 # to match slider precision
min_value.setValidator(QDoubleValidator(lower_bound, upper_bound, decimal_places))
max_value.setValidator(QDoubleValidator(lower_bound, upper_bound, decimal_places))
text_field.setValidator(QDoubleValidator(lower_bound, upper_bound, decimal_places))

# Align all centrally for consistency
text_field.setAlignment(QtCore.Qt.AlignCenter)
Expand All @@ -914,6 +922,8 @@ def _create_min_current_max_value_label(self, slider:QtWidgets.QSlider, paramete
min_value.editingFinished.connect(self._update_slider_boundaries)
max_value.editingFinished.connect(self._update_slider_boundaries)

text_field.editingFinished.connect(self._update_parameter_text_field)

# keep track of parameter values and min/max labels
self.parameter_text_field_container[parameter_id] = text_field
self.slider_min_max_label_container[parameter_id] = [min_value, max_value]
Expand Down Expand Up @@ -943,6 +953,19 @@ def _update_parameter_values(self):
elif self.enable_live_plotting and not self.is_single_output_model:
self._plot_multi_output_model()

def _update_parameter_text_field(self):
# Iterate over sliders
for slider_id, slider in enumerate(self.slider_container):
# Get Current Textbox value
# Round to 1dp to correspond to slider precision
new_value = round(number=float(self.parameter_text_field_container[slider_id].text()), ndigits=1)
# Check new value is in range ?
# Set new slider boundaries (set to min or max if out of range)
slider.setValue(new_value)
# Update Textfield to reflect this
self.parameter_text_field_container[slider_id].setText(str(slider.value()))


def _update_slider_boundaries(self):
""""
Updates slider boundaries to correspond to inputted boundaries in text box
Expand All @@ -954,7 +977,7 @@ def _update_slider_boundaries(self):
# Round to 1dp to correspond to slider precision
new_min = round(number=float(self.slider_min_max_label_container[slider_id][0].text()), ndigits=1)
new_max = round(number=float(self.slider_min_max_label_container[slider_id][1].text()), ndigits=1)
# Set new slider boundaries (if possible)
# Set new slider boundaries (by default both will be set to lowest val if min > max)
slider.setMinimum(round(number=new_min, ndigits=1))
slider.setMaximum(round(number=new_max, ndigits=1))
# Display new slider boundaries in text box
Expand Down
65 changes: 58 additions & 7 deletions PKPD/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class SingleOutputModel(AbstractModel):
employed. The sole difference to the MultiOutputProblem is that the simulate method returns a 1d array instead of a
2d array.
"""
def __init__(self, mmt_file:str) -> None:
def __init__(self, mmt_file: str) -> None:
"""Initialises the model class.

Arguments:
Expand All @@ -26,7 +26,22 @@ def __init__(self, mmt_file:str) -> None:
self.state_dimension = model.count_states()
self.output_name = self._get_default_output_name(model)
self.parameter_names = self._get_parameter_names(model)
self.number_parameters_to_fit = model.count_variables(inter=False, bound=False)
self.initial_conditions = np.zeros(self.state_dimension)

# Identify which states are NOT called drug
non_drug_states = [name.split('.')[-1] != 'drug' for name in self.state_names]

# Infer initial conditions if at least one state doesn't correspond to drug
# If all states correspond to drug - set these to zero when solving forward problem/inference
# TODO Could use non_drug_states to do inference only on non drug states?
self.infer_initial_conditions = (np.sum(non_drug_states) > 0)


# Check if initial conditions included in inference
if self.infer_initial_conditions: # include initial conditions
self.number_parameters_to_fit = model.count_variables(inter=False, bound=False)
else: # just infer parameters
self.number_parameters_to_fit = len(self.parameter_names)

# instantiate the simulation
self.simulation = myokit.Simulation(model, protocol)
Expand Down Expand Up @@ -117,10 +132,21 @@ def _set_parameters(self, parameters:np.ndarray) -> None:
Arguments:
parameters {np.ndarray} -- Parameters of the model. By convention [initial condition, model parameters].
"""
self.simulation.set_state(parameters[:self.state_dimension])
for param_id, value in enumerate(parameters[self.state_dimension:]):
# Check if initial conditions are included in inference
if self.infer_initial_conditions:
# No modification required
params = parameters
else:
# Set initial conditions to zero by default
params = np.concatenate([self.initial_conditions, parameters])

self.simulation.set_state(params[:self.state_dimension])
for param_id, value in enumerate(params[self.state_dimension:]):
self.simulation.set_constant(self.parameter_names[param_id], value)

def set_initial_conditions(self, initial_conditions: np.ndarray):
self.initial_conditions = initial_conditions


class MultiOutputModel(AbstractModel):
"""Model class inheriting from pints.ForwardModel. To solve the forward problem methods from the myokit package are
Expand All @@ -142,7 +168,20 @@ def __init__(self, mmt_file: str) -> None:
self.output_names = []
self.output_dimension = None
self.parameter_names = self._get_parameter_names(model)
self.number_parameters_to_fit = model.count_variables(inter=False, bound=False)
self.initial_conditions = np.zeros(self.state_dimension)

# Identify which states are NOT called drug
non_drug_states = [name.split('.')[-1] != 'drug' for name in self.state_names]

# Infer initial conditions if at least one state doesn't correspond to drug
# TODO Could use non_drug_states to do inference only on non drug states?
self.infer_initial_conditions = (np.sum(non_drug_states) > 0)

# Check if initial conditions included in inference
if self.infer_initial_conditions: # include initial conditions in number of variables
self.number_parameters_to_fit = model.count_variables(inter=False, bound=False)
else: # just infer parameters
self.number_parameters_to_fit = len(self.parameter_names)

# instantiate the simulation
self.simulation = myokit.Simulation(model, protocol)
Expand Down Expand Up @@ -211,8 +250,17 @@ def _set_parameters(self, parameters: np.ndarray) -> None:
Arguments:
parameters {np.ndarray} -- Parameters of the model. By convention [initial condition, model parameters].
"""
self.simulation.set_state(parameters[:self.state_dimension])
for param_id, value in enumerate(parameters[self.state_dimension:]):
# Check if inferring initial conditions
if self.infer_initial_conditions:
# No modification required
params = parameters
else:
# Set initial conditions to zero by default
params = np.concatenate([self.initial_conditions, parameters])

# Set parameters
self.simulation.set_state(params[:self.state_dimension])
for param_id, value in enumerate(params[self.state_dimension:]):
self.simulation.set_constant(self.parameter_names[param_id], value)

def set_output_dimension(self, data_dimension: int):
Expand Down Expand Up @@ -265,6 +313,9 @@ def set_output(self, output_names: List):
self.output_dimension = len(output_names)
self.output_names = output_names

def set_initial_conditions(self, initial_conditions: np.ndarray):
self.initial_conditions = initial_conditions


def set_unit_format():
"""
Expand Down
13 changes: 7 additions & 6 deletions tests/inference/test_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class TestSingleOutputProblem(unittest.TestCase):
# generating data
file_name = 'PKPD/modelRepository/1_bolus_linear.mmt'
one_comp_model = m.SingleOutputModel(file_name)
true_parameters_one_comp_model = [0, 1, 4] # [initial drug, CL, V]
true_parameters_one_comp_model = [1, 4] # [initial drug, CL, V]

# create protocol object
protocol = myokit.Protocol()
Expand All @@ -39,7 +39,8 @@ def test_find_optimal_parameter(self):
)

# start somewhere in parameter space (close to the solution for ease)
initial_parameters = np.array([0.1, 1.1, 4.1])
# TODO Test with more robust inference problem
initial_parameters = np.array([1.0, 4.0]) # Exact solution to avoid problems with Travis

# solve inverse problem
problem.find_optimal_parameter(initial_parameter=initial_parameters, number_of_iterations=1)
Expand Down Expand Up @@ -104,10 +105,10 @@ class TestMultiOutputProblem(unittest.TestCase):
# set dimensionality of data
two_comp_model.set_output_dimension(2)

# List of parameters: ['central_compartment.drug', 'dose_compartment.drug', 'peripheral_compartment.drug',
# 'central_compartment.CL', 'central_compartment.Kcp', 'central_compartment.V', 'dose_compartment.Ka',
# List of parameters:
# ['central_compartment.CL', 'central_compartment.Kcp', 'central_compartment.V',
# 'peripheral_compartment.Kpc', 'peripheral_compartment.V']
true_parameters = [1, 1, 1, 3, 5, 2, 2]
true_parameters = [1, 3, 5, 2, 2]

times = np.linspace(0.0, 24.0, 100)
model_result = two_comp_model.simulate(true_parameters, times)
Expand All @@ -124,7 +125,7 @@ def test_find_optimal_parameter(self):
)

# start somewhere in parameter space (close to the solution for ease)
initial_parameters = np.array([1, 1, 1, 3, 5, 2, 2])
initial_parameters = np.array([1, 3, 5, 2, 2])

# solve inverse problem
problem.find_optimal_parameter(initial_parameter=initial_parameters)
Expand Down
49 changes: 38 additions & 11 deletions tests/model/test_Model.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_init(self):
state_names = ['central_compartment.drug']
output_name = 'central_compartment.drug_concentration'
parameter_names = ['central_compartment.CL', 'central_compartment.V']
number_parameters_to_fit = 3
number_parameters_to_fit = 2 # since not fitting IC

# assert initialised values coincide
assert state_names == self.one_comp_model.state_names
Expand All @@ -35,7 +35,7 @@ def test_n_parameters(self):
"""
# Test case I: 1-compartment model
# expected
n_parameters = 3
n_parameters = 2 # since not fitting IC

# assert correct number of parameters is returned.
assert n_parameters == self.one_comp_model.n_parameters()
Expand All @@ -50,19 +50,31 @@ def test_n_outputs(self):
# assert correct number of outputs.
assert n_outputs == self.one_comp_model.n_outputs()

def test_set_initial_conditions(self):
"""
Tests whether set_initial_conditions functions sets correct initial conditions
Returns:
"""
old_ic = np.zeros(1)
assert old_ic == self.one_comp_model.initial_conditions # check correct default
new_ic = np.array([2])
self.one_comp_model.set_initial_conditions(new_ic)
assert new_ic == self.one_comp_model.initial_conditions # check set
self.one_comp_model.set_initial_conditions(old_ic)
assert old_ic == self.one_comp_model.initial_conditions # check set back

def test_simulate(self):
"""Tests whether the simulate method works as expected. Tests implicitly also whether the _set_parameters method
works properly.
"""
# Test case I: 1-compartment model
parameters = [0, 2, 4] # different from initialised parameters
parameters = [2, 4] # different from initialised parameters
times = np.arange(25)

# expected
model, protocol, _ = myokit.load(self.file_name)
model.set_state([parameters[0]])
model.set_value('central_compartment.CL', parameters[1])
model.set_value('central_compartment.V', parameters[2])
model.set_value('central_compartment.CL', parameters[0])
model.set_value('central_compartment.V', parameters[1])
simulation = myokit.Simulation(model, protocol)
myokit_result = simulation.run(duration=times[-1]+1,
log=['central_compartment.drug_concentration'],
Expand Down Expand Up @@ -109,7 +121,7 @@ def test_n_parameters(self):
"""
# Test case I: 1-compartment model
# expected
n_parameters = 7
n_parameters = 5 # since not including 2 states

# assert correct number of parameters is returned.
assert n_parameters == self.two_comp_model.n_parameters()
Expand All @@ -124,13 +136,29 @@ def test_n_outputs(self):
# assert correct number of outputs.
assert n_outputs == self.two_comp_model.n_outputs()

def test_set_initial_conditions(self):
"""
Tests whether set_initial_conditions functions sets correct initial conditions
Returns:
"""
old_ic = np.zeros(2)
for i in range(len(old_ic)):
assert old_ic[i] == self.two_comp_model.initial_conditions[i] # check correct default
new_ic = np.array([2,2])
self.two_comp_model.set_initial_conditions(new_ic)
for i in range(len(old_ic)):
assert new_ic[i] == self.two_comp_model.initial_conditions[i] # check set to new
self.two_comp_model.set_initial_conditions(old_ic)
for i in range(len(old_ic)):
assert old_ic[i] == self.two_comp_model.initial_conditions[i] # check set back to old

def test_simulate(self):
"""Tests whether the simulate method works as expected. Tests implicitly also whether the _set_parameters method
works properly.
"""
output_names = ['central_compartment.drug_concentration', 'peripheral_compartment.drug_concentration']
state_dimension = 2
parameters = [0, 0, 1, 3, 5, 2, 2] # states + parameters
parameters = [1, 3, 5, 2, 2] # parameters - not including states
parameter_names = ['central_compartment.CL',
'central_compartment.Kcp',
'central_compartment.V',
Expand All @@ -143,10 +171,9 @@ def test_simulate(self):
# initialise model
model, protocol, _ = myokit.load(self.file_name)

# set initial conditions and parameter values
model.set_state(parameters[:state_dimension])
# set parameter values
for parameter_id, name in enumerate(parameter_names):
model.set_value(name, parameters[state_dimension + parameter_id])
model.set_value(name, parameters[parameter_id])

# solve model
simulation = myokit.Simulation(model, protocol)
Expand Down