diff --git a/PKPD/gui/simulation.py b/PKPD/gui/simulation.py index 629323f..308c1ec 100644 --- a/PKPD/gui/simulation.py +++ b/PKPD/gui/simulation.py @@ -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 @@ -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 @@ -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) @@ -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] @@ -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 @@ -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 diff --git a/PKPD/model/model.py b/PKPD/model/model.py index 265b542..5219408 100644 --- a/PKPD/model/model.py +++ b/PKPD/model/model.py @@ -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: @@ -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) @@ -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 @@ -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) @@ -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): @@ -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(): """ diff --git a/tests/inference/test_inference.py b/tests/inference/test_inference.py index 3904b55..f5abb3e 100644 --- a/tests/inference/test_inference.py +++ b/tests/inference/test_inference.py @@ -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() @@ -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) @@ -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) @@ -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) diff --git a/tests/model/test_Model.py b/tests/model/test_Model.py index fdd104e..d821a5b 100644 --- a/tests/model/test_Model.py +++ b/tests/model/test_Model.py @@ -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 @@ -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() @@ -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'], @@ -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() @@ -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', @@ -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)