From ddb289f75d65722b6bc7fb1bb069bfded4dc19a8 Mon Sep 17 00:00:00 2001 From: Dilan Pathirana <59329744+dilpath@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:15:15 +0100 Subject: [PATCH 1/2] Add post--model-selection analysis methods (#125) * track iterations; fix `ModelHash.get_model`; add `Model.__repr__` * add analysis module * fixme: use pypesto PR branch * unfix pypesto in tox.ini to use pyproject.toml * sort iterations in `group_by_iteration` * add `VIRTUAL_INITIAL_MODEL_HASH` * method to convert Models to dataframe; update plotting code * update vis notebook * doc analysis methods briefly * move models criterion getter to `Models`; add `Models.values()` for backwards compatibility; update plot code * test case 0009: add caveat * update expected test case results --------- Co-authored-by: Daniel Weindl --- doc/analysis.rst | 11 + doc/api.rst | 1 + .../calibrated_models/calibrated_models.yaml | 70 +++- doc/examples/example_cli_famos.ipynb | 71 +--- .../model_selection/calibrated_M1_4.yaml | 1 + .../model_selection/calibrated_M1_7.yaml | 1 + .../model_selection/calibrated_models_1.yaml | 3 + doc/examples/visualization.ipynb | 237 ++--------- doc/examples/workflow_cli.ipynb | 396 ++---------------- doc/examples/workflow_python.ipynb | 190 ++------- doc/index.rst | 1 + petab_select/__init__.py | 1 + petab_select/analyze.py | 161 +++++++ petab_select/candidate_space.py | 20 +- petab_select/constants.py | 1 + petab_select/model.py | 23 +- petab_select/models.py | 132 +++++- petab_select/plot.py | 222 ++++------ petab_select/problem.py | 28 +- petab_select/ui.py | 40 +- pyproject.toml | 3 +- test/analyze/input/models.yaml | 66 +++ test/analyze/test_analyze.py | 81 ++++ test/candidate_space/test_famos.py | 9 +- test/cli/input/models.yaml | 10 + test/pypesto/generate_expected_models.py | 91 ++-- test_cases/0001/expected.yaml | 7 +- test_cases/0002/expected.yaml | 11 +- test_cases/0003/expected.yaml | 7 +- test_cases/0004/expected.yaml | 11 +- test_cases/0005/expected.yaml | 11 +- test_cases/0006/expected.yaml | 9 +- test_cases/0007/expected.yaml | 7 +- test_cases/0008/expected.yaml | 7 +- test_cases/0009/README.md | 5 + test_cases/0009/expected.yaml | 23 +- tox.ini | 2 - 37 files changed, 868 insertions(+), 1102 deletions(-) create mode 100644 doc/analysis.rst create mode 100644 petab_select/analyze.py create mode 100644 test/analyze/input/models.yaml create mode 100644 test/analyze/test_analyze.py create mode 100644 test_cases/0009/README.md diff --git a/doc/analysis.rst b/doc/analysis.rst new file mode 100644 index 00000000..ee8aa461 --- /dev/null +++ b/doc/analysis.rst @@ -0,0 +1,11 @@ +Analysis +======== + +After using PEtab Select to perform model selection, you may want to operate on all "good" calibrated models. +The PEtab Select Python library provides some methods to help with this. Please request any missing methods. + +See the Python API docs for the :class:`petab_select.Models` class, which provides some methods. In particular, :attr:`petab_select.Models.df` can be used +to get a quick overview over all models, as a pandas dataframe. + +Additionally, see the Python API docs for the ``petab_select.analysis`` module, which contains some methods to subset and group models, +or compute "weights" (e.g. Akaike weights). diff --git a/doc/api.rst b/doc/api.rst index 6f111328..5a86acf2 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -7,6 +7,7 @@ petab-select Python API :toctree: generated petab_select + petab_select.analyze petab_select.candidate_space petab_select.constants petab_select.criteria diff --git a/doc/examples/calibrated_models/calibrated_models.yaml b/doc/examples/calibrated_models/calibrated_models.yaml index e77c5243..8b96bafd 100644 --- a/doc/examples/calibrated_models/calibrated_models.yaml +++ b/doc/examples/calibrated_models/calibrated_models.yaml @@ -2,7 +2,8 @@ AICc: 37.97523003111246 NLLH: 17.48761501555623 estimated_parameters: - sigma_x2: 4.462298385653177 + sigma_x2: 4.462298422134608 + iteration: 1 model_hash: M_0-000 model_id: M_0-000 model_subspace_id: M_0 @@ -17,11 +18,12 @@ petab_yaml: petab_problem.yaml predecessor_model_hash: virtual_initial_model - criteria: - AICc: -0.1754060811089051 - NLLH: -4.0877030405544525 + AICc: -0.17540608110890332 + NLLH: -4.087703040554452 estimated_parameters: k3: 0.0 - sigma_x2: 0.12242920113658744 + sigma_x2: 0.12242920113658338 + iteration: 2 model_hash: M_1-000 model_id: M_1-000 model_subspace_id: M_1 @@ -36,11 +38,12 @@ petab_yaml: petab_problem.yaml predecessor_model_hash: M_0-000 - criteria: - AICc: -0.27451405630430337 - NLLH: -4.137257028152152 + AICc: -0.27451438069575573 + NLLH: -4.137257190347878 estimated_parameters: - k2: 0.10147827639089564 - sigma_x2: 0.12142256779953603 + k2: 0.10147824307890803 + sigma_x2: 0.12142219599557078 + iteration: 2 model_hash: M_2-000 model_id: M_2-000 model_subspace_id: M_2 @@ -55,11 +58,12 @@ petab_yaml: petab_problem.yaml predecessor_model_hash: M_0-000 - criteria: - AICc: -0.7053270517931587 - NLLH: -4.352663525896579 + AICc: -0.7053270766271886 + NLLH: -4.352663538313594 estimated_parameters: - k1: 0.20160888007873565 - sigma_x2: 0.11713858557052499 + k1: 0.20160925279667963 + sigma_x2: 0.11714017664827497 + iteration: 2 model_hash: M_3-000 model_id: M_3-000 model_subspace_id: M_3 @@ -74,12 +78,13 @@ petab_yaml: petab_problem.yaml predecessor_model_hash: M_0-000 - criteria: - AICc: 9.294672948206841 - NLLH: -4.352663525896579 + AICc: 9.294672923372811 + NLLH: -4.352663538313594 estimated_parameters: - k1: 0.20160888007873565 + k1: 0.20160925279667963 k3: 0.0 - sigma_x2: 0.11713858557052499 + sigma_x2: 0.11714017664827497 + iteration: 3 model_hash: M_5-000 model_id: M_5-000 model_subspace_id: M_5 @@ -94,12 +99,13 @@ petab_yaml: petab_problem.yaml predecessor_model_hash: M_3-000 - criteria: - AICc: 7.852170288089528 - NLLH: -5.073914855955236 + AICc: 7.8521704398854 + NLLH: -5.0739147800573 estimated_parameters: - k1: 0.20924739987621038 - k2: 0.0859065470362628 - sigma_x2: 0.1038731029818225 + k1: 0.20924804320838675 + k2: 0.0859052351446815 + sigma_x2: 0.10386846319370771 + iteration: 3 model_hash: M_6-000 model_id: M_6-000 model_subspace_id: M_6 @@ -113,3 +119,25 @@ k3: 0 petab_yaml: petab_problem.yaml predecessor_model_hash: M_3-000 +- criteria: + AICc: 35.94352968170024 + NLLH: -6.028235159149878 + estimated_parameters: + k1: 0.6228488917665873 + k2: 0.020189424009226256 + k3: 0.0010850434974038557 + sigma_x2: 0.08859278245811462 + iteration: 4 + model_hash: M_7-000 + model_id: M_7-000 + model_subspace_id: M_7 + model_subspace_indices: + - 0 + - 0 + - 0 + parameters: + k1: estimate + k2: estimate + k3: estimate + petab_yaml: petab_problem.yaml + predecessor_model_hash: M_3-000 diff --git a/doc/examples/example_cli_famos.ipynb b/doc/examples/example_cli_famos.ipynb index c413cb75..b5895ac9 100644 --- a/doc/examples/example_cli_famos.ipynb +++ b/doc/examples/example_cli_famos.ipynb @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "1f04dce0", "metadata": {}, "outputs": [], @@ -44,7 +44,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "a81560e6", "metadata": {}, "outputs": [], @@ -109,69 +109,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "bb1a5144", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Executing iteration 1\n", - "Executing iteration 2\n", - "Executing iteration 3\n", - "Executing iteration 4\n", - "Executing iteration 5\n", - "Executing iteration 6\n", - "Executing iteration 7\n", - "Executing iteration 8\n", - "Executing iteration 9\n", - "Executing iteration 10\n", - "Executing iteration 11\n", - "Executing iteration 12\n", - "Executing iteration 13\n", - "Executing iteration 14\n", - "Executing iteration 15\n", - "Executing iteration 16\n", - "Executing iteration 17\n", - "Executing iteration 18\n", - "Executing iteration 19\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "petab_select/petab_select/candidate_space.py:1142: RuntimeWarning: Model `model_subspace_1-0001011010010010` has been previously excluded from the candidate space so is skipped here.\n", - " return_value = self.inner_candidate_space.consider(model)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Executing iteration 20\n", - "Executing iteration 21\n", - "Executing iteration 22\n", - "Executing iteration 23\n", - "Executing iteration 24\n", - "Executing iteration 25\n", - "Executing iteration 26\n", - "Executing iteration 27\n", - "Executing iteration 28\n", - "Executing iteration 29\n", - "Executing iteration 30\n", - "Executing iteration 31\n", - "Executing iteration 32\n", - "Executing iteration 33\n", - "Executing iteration 34\n", - "Executing iteration 35\n", - "Executing iteration 36\n", - "Executing iteration 37\n", - "Model selection has terminated.\n" - ] - } - ], + "outputs": [], "source": [ "%%bash -s \"$petab_select_problem_yaml\" \"$output_path_str\"\n", "petab_select_problem_yaml=$1\n", @@ -217,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "93caf071", "metadata": {}, "outputs": [], @@ -227,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "cb61d0f7", "metadata": {}, "outputs": [], diff --git a/doc/examples/model_selection/calibrated_M1_4.yaml b/doc/examples/model_selection/calibrated_M1_4.yaml index b64f141b..c1e94f10 100644 --- a/doc/examples/model_selection/calibrated_M1_4.yaml +++ b/doc/examples/model_selection/calibrated_M1_4.yaml @@ -3,6 +3,7 @@ criteria: estimated_parameters: k2: 0.15 k3: 0.0 +iteration: 1 model_hash: M1_4-000 model_id: M1_4-000 model_subspace_id: M1_4 diff --git a/doc/examples/model_selection/calibrated_M1_7.yaml b/doc/examples/model_selection/calibrated_M1_7.yaml index a3829482..48c64c67 100644 --- a/doc/examples/model_selection/calibrated_M1_7.yaml +++ b/doc/examples/model_selection/calibrated_M1_7.yaml @@ -4,6 +4,7 @@ estimated_parameters: k1: 0.25 k2: 0.1 k3: 0.0 +iteration: 2 model_hash: M1_7-000 model_id: M1_7-000 model_subspace_id: M1_7 diff --git a/doc/examples/model_selection/calibrated_models_1.yaml b/doc/examples/model_selection/calibrated_models_1.yaml index f34ae7bb..9e3a39f0 100644 --- a/doc/examples/model_selection/calibrated_models_1.yaml +++ b/doc/examples/model_selection/calibrated_models_1.yaml @@ -1,6 +1,7 @@ - criteria: AIC: 180 estimated_parameters: {} + iteration: 1 model_hash: M1_0-000 model_id: M1_0-000 model_subspace_id: M1_0 @@ -17,6 +18,7 @@ - criteria: AIC: 100 estimated_parameters: {} + iteration: 1 model_hash: M1_1-000 model_id: M1_1-000 model_subspace_id: M1_1 @@ -33,6 +35,7 @@ - criteria: AIC: 50 estimated_parameters: {} + iteration: 1 model_hash: M1_2-000 model_id: M1_2-000 model_subspace_id: M1_2 diff --git a/doc/examples/visualization.ipynb b/doc/examples/visualization.ipynb index 8b86ad63..13f36b4f 100644 --- a/doc/examples/visualization.ipynb +++ b/doc/examples/visualization.ipynb @@ -17,146 +17,37 @@ "\n", "All dependencies for these plots can be installed with `pip install petab_select[plot]`.\n", "\n", - "Here, some calibrated models that were saved to disk with `petab_select.model.models_to_yaml_list` are loaded and used as input. This is the result of a forward selection with the problem provided in `calibrated_models`." + "In this notebook, some calibrated models that were saved to disk with the `to_yaml` method of a `Models` object, are loaded and used as input here. This is the result of a forward selection with the problem provided in `calibrated_models`. Note that a `Models` object is expect here; if you have a list of models `model_list`, simply use `models = Models(model_list)`." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "ca6ce5b4", "metadata": {}, "outputs": [], "source": [ + "import matplotlib\n", + "\n", "import petab_select\n", "import petab_select.plot\n", + "from petab_select import VIRTUAL_INITIAL_MODEL_HASH\n", "\n", - "models = petab_select.models_from_yaml_list(\n", - " model_list_yaml=\"calibrated_models/calibrated_models.yaml\"\n", + "models = petab_select.Models.from_yaml(\n", + " \"calibrated_models/calibrated_models.yaml\"\n", ")" ] }, { "cell_type": "code", - "execution_count": 2, - "id": "2574e65a-1f16-4205-8c23-b65ba78f9a1a", + "execution_count": null, + "id": "54532b75-53e4-4670-8e64-21e7adda0c0e", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
 Model hashAICc criterionPredecessor model hashEstimated parameters
0M_0-00037.975230virtual_initial_model-sigma_x2
1M_1-000-0.175406M_0-000k3, sigma_x2
2M_2-000-0.274514M_0-000k2, sigma_x2
3M_3-000-0.705327M_0-000k1, sigma_x2
4M_5-0009.294673M_3-000k1, k3, sigma_x2
5M_6-0007.852170M_3-000k1, k2, sigma_x2
\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "import matplotlib\n", - "import pandas as pd\n", - "\n", - "pd.DataFrame(\n", - " {\n", - " \"Model hash\": [model.model_id for model in models],\n", - " \"AICc criterion\": [\n", - " model.get_criterion(petab_select.Criterion.AICC)\n", - " for model in models\n", - " ],\n", - " \"Predecessor model hash\": [\n", - " model.predecessor_model_hash for model in models\n", - " ],\n", - " \"Estimated parameters\": [\n", - " \", \".join(model.get_estimated_parameter_ids_all())\n", - " for model in models\n", - " ],\n", - " }\n", - ").style.background_gradient(\n", + "models.df.style.background_gradient(\n", " cmap=matplotlib.colormaps.get_cmap(\"summer\"),\n", - " subset=[\"AICc criterion\"],\n", + " subset=[petab_select.Criterion.AICC],\n", ")" ] }, @@ -170,7 +61,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "09c9df1d", "metadata": {}, "outputs": [], @@ -182,9 +73,9 @@ " \"1\" if value == petab_select.ESTIMATE else \"0\"\n", " for value in model.parameters.values()\n", " )\n", - "labels[petab_select.ModelHash(petab_select.VIRTUAL_INITIAL_MODEL, \"\")] = (\n", - " \"\\n\".join(petab_select.VIRTUAL_INITIAL_MODEL.split(\"_\")).title()\n", - ")\n", + "labels[VIRTUAL_INITIAL_MODEL_HASH] = \"\\n\".join(\n", + " petab_select.VIRTUAL_INITIAL_MODEL.split(\"_\")\n", + ").title()\n", "\n", "# Custom colors for some models\n", "colors = {\n", @@ -210,21 +101,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "96d99572-f74d-4e25-8237-0aa158eb29f6", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "petab_select.plot.upset(models=models, criterion=petab_select.Criterion.AICC);" ] @@ -234,7 +114,7 @@ "id": "32de6556", "metadata": {}, "source": [ - "## Selected models\n", + "## Best model from each iteration\n", "\n", "This shows strict improvements in the criterion, and the corresponding model, across all iterations of model selection.\n", "\n", @@ -243,23 +123,12 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "56b4a27b", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "petab_select.plot.line_selected(\n", + "petab_select.plot.line_best_by_iteration(\n", " models=models,\n", " criterion=petab_select.Criterion.AICC,\n", " labels=labels,\n", @@ -278,21 +147,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "862a78ef", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA7YAAAOwCAYAAAAKo+iFAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACtV0lEQVR4nOzdd3iUVf7+8XvSKCEJBGbonSBFpKOQUCV0pAqCFIGALMUuinVBUbEtAopkqNKkg0gNJUCigFQpIr0rSA2hpc3vj/0mP1lQCSQ5U96v69rLZebhmRvWJdz5nOcci8PhcAgAAAAAABflZToAAAAAAAAPgmILAAAAAHBpFFsAAAAAgEuj2AIAAAAAXBrFFgAAAADg0ii2AAAAAACXRrEFAAAAALg0ii0AAAAAwKX53MtFKSkpOnPmjAICAmSxWDI7EwAAAADAwzkcDl29elWFChWSl9ffz2TvqdieOXNGRYsWzZBwAAAAAADcq5MnT6pIkSJ/e809FduAgIC0GwYGBj54MgAAAAAA/kZcXJyKFi2a1kf/zj0V29Tlx4GBgRRbAAAAAECWuZfHYdk8CgAAAADg0ii2AAAAAACXRrEFAAAAALg0ii0AAAAAwKVRbAEAAAAALo1iCwAAAABwaRRbAAAAAIBLo9gCAAAAAFwaxRYAAAAA4NIotgAAAAAAl0axBQAAAAC4NIotAAAAAMClUWwBAAAAAC6NYgsAAAAAcGkUWwAAAACAS6PYAgAAAABcGsUWAAAAAODSKLYAAAAAAJdGsQUAAAAAuDSKLQAAAADApVFsAQAAAAAujWILAAAAAHBpFFsAAAAAgEuj2AIAAAAAXBrFFgAAAADg0ii2AAAAAACXRrEFAAAAALg0ii0AAAAAwKVRbAEAAAAALo1iCwAAAABwaRRbAAAAAIBLo9gCAAAAAFwaxRYAAAAA4NIotgAAAAAAl0axBQAAAAC4NIotAAAAAMClUWwBAAAAAC6NYgsAAAAAcGkUWwAAAACAS6PYAgAAAABcGsUWAAAAAODSfEwHAACYcfDgQV29etV0DKcSEBCgkJAQ0zEAAEA6UWwBwAMdPHhQZcuWNR3DKR04cIByCwCAi6HYAoAHSp3UDho0SIULFzacxjmcPn1aY8eOZYoNAIALotgCgAcrXLiwSpUqZToGAADAA2HzKAAAAACAS6PYAgAAAABcGsUWAAAAAODSKLYAAAAAAJdGsQUAAAAAuDSKLQAAAADApVFsAQAAAAAujWILAAAAAHBpFFsAAAAAgEuj2AIAAAAAXBrFFgAAAADg0ii2AAAAAACXRrEFAAAAALg0ii0AAAAAwKVRbAEAAAAALo1iCwAAAABwaRRbAAAAAIBLo9gCAAAAAFwaxRYAAAAA4NIotgAAAAAAl0axBQAAAAC4NIotAAAAAMClUWwBAAAAAC6NYgsAAAAAcGk+pgMAAJxXdHS0xo0bJ0kaNmyYypUrd9v7DodDAwcO1IULF1StWjW99tpr93zvixcvaurUqfr555/lcDhUsWJF9ejRQ/nz57/tulWrVmnPnj06dOiQLly4oPr162vAgAF3vee1a9c0Y8YMbdmyRQkJCSpdurS6d++uUqVKpfNXDgAAXAkTWwDAP/L19VVMTMwdr+/bt08XLlyQr69vuu538+ZNDR8+XL/88ovatm2rJ598UkePHtWwYcN09erV26797rvvtHfvXhUtWlTe3t5/ec+UlBR99NFHiomJUdOmTfX0008rLi5Ow4cP12+//ZaufAAAwLVQbAEA/6hq1aratGmTkpOTb3s9NjZWpUqVUu7cudN1v5UrV+q3337TkCFD1KZNG7Vs2VJvvvmmLl26pO+///62a999911NmDBBQ4cOlY/PXy802rx5sw4cOKABAwboySefVNOmTfXuu+/Ky8tLc+fOTVc+AADgWii2AIB/FBoaqvj4eP38889pryUlJWnTpk0KDQ1N9/02b96s0qVLq0yZMmmvFS5cWA8//LB+/PHH2661Wq2yWCz/eM9NmzYpKChItWrVSnstMDBQjz32mLZu3arExMR05wQAAK6BYgsA+EdWq1UhISGKjY1Ne23Hjh26fv266tSpk657paSk6MSJE3d97rVMmTI6e/asbty4ke6Mx44dU8mSJeXldfuXtjJlyujWrVssRwYAwI1RbAEA9yQsLExbt25VQkKCJCkmJkYVKlRQcHBwuu4THx+vxMRE5cmT5473Upc0X7p0Kd35Ll26dNd7pr528eLFdN8TAAC4BootAOCe1K5dWwkJCdq2bZtu3Lih7du339cy5NRifLfnZf38/G67Jr33vds9Uze2YikyAADui+N+AAD3JDAwUJUqVVJsbKwSEhKUkpKixx57LN33SS2vSUlJd7yXWmhTr0nvfe92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zZOQkKApU6aoe/fuyp49u9EscD5eXl5q2bKlVq9erV27dqlJkyZ69913VaRIEQ0cOFAHDhwwHRGAh6HYugibzabLly8rISHBdBQAQAabP3++WrZsqXr16mnVqlXKkyeP6Uj67rvvdO7cOfXt29d0FDi5Rx55RBMnTtSJEyc0ZMgQzZs3Tw899JBatWqlNWvW8BwugCxBsXURVqtVknT+/HnDSQAAGWnChAnq1KmTOnTooEWLFilnzpymI0mS7Ha7ateurYcffth0FLgIm82md999V8ePH9fkyZN18uRJNW7cWJUrV9akSZN08+ZN0xEBuDGKrYuw2WySxHJkAHAjH3/8sfr27av+/ftr+vTpTnNO7LFjxxQVFcW0Fvcle/bseuaZZ7Rz506tWbNGJUqUUEREhIoXL65///vfOnv2rOmIANwQxdZFpBZbNpACANfncDj02muv6bXXXtPbb7+tsWPHysvLeb4kT5w4UQEBAerUqZPpKHBhFotFjRo10nfffadff/1VTz75pD755BMVK1ZMvXr10q5du0xHBOBGnOerKP5W6lJkJrYA4NqSk5PVr18/ffzxxxo1apSGDx8ui8ViOlaapKQkTZo0SV27dpW/v7/pOHATISEhGjt2rE6dOqX3339fa9asUZUqVdSoUSMtWbJEKSkppiMCcHEUWxeRM2dO+fv7U2wBwIXdunVLnTp10uTJkzV16lQ9//zzpiPdYdmyZTpz5gxn1yJT5MmTR6+++qqOHDmi2bNn68aNG3riiSf00EMPaezYsYqPjzcdEYCLoti6EM6yBQDXdfXqVbVs2VJLly7VggUL1KNHD9OR7sput6t69eqqWrWq6ShwYz4+PurUqZN+/PFH/fjjj6pevbpeeOEFFS1aVEOGDNGJEydMRwTgYii2LoSzbAHANV24cEGNGzfWTz/9pJUrV+qJJ54wHemuTp06pWXLlrFpFLLUY489pm+//VZHjhxR3759FRkZqVKlSqlz587atGmT6XgAXATF1oVQbAHA9Zw6dUp169bV0aNHtW7dOtWvX990pL80efJkZc+eXV26dDEdBR6oWLFi+vjjj3Xq1Cl98cUX2rFjh2rXrq3HHntMs2fPVlJSkumIAJwYxdaFsBQZAFzLgQMHFBoaqmvXrikmJkbVqlUzHekvJScna8KECXrqqacUGBhoOg48WK5cuTRw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jh3aunUrm0YBmaxEiRL69NNPderUKX322WfavHmzHn30UYWGhmrevHlKSkoyHRFwSxRbF5R6li0A4MHExcWpefPmWrlypRYvXqyuXbuajpRp7Ha7ChYsqJYtW5qOAniEgIAAPffcczpw4IAWLVokX19fPfnkkypTpow+++wzXblyxXREwK1QbF0QE1sAeHB//PGHGjVqpB07digqKkotWrQwHSnTXLt2TTNmzFDv3r3l4+NjOg7gUby9vdWmTRtFR0dr27Ztql+/voYOHaoiRYroueee0+HDh01HBNwCxdYFUWwB4MGcOHFCdevW1alTp7R+/XqFhYWZjpSp5s6dq7i4OPXp08d0FMCjVatWTVOnTtXx48f1wgsvaNasWQoJCVHbtm21fv16ORwO0xEBl0WxdUEsRQaA+7d//36Fhobq1q1biomJUeXKlU1HynSRkZEKDw9XyZIlTUcBIKlgwYJ67733dOLECUVGRurQoUNq0KCBqlevrm+++Ua3bt0yHRFwORRbF5Q6seW7egCQPlu3blXdunUVFBSk2NhYlSlTxnSkTLd37179+OOP6tevn+koAP5Hjhw5FBERod27d2vlypUqUKCAevbsqRIlSui9995jkAGkA8XWBdlsNiUmJrLpAACkw7p169SwYUOVKVNGGzZsUKFChUxHyhJ2u11Wq1VPPPGE6SgA/oLFYlGTJk20bNky7du3T23atNGHH36ookWLKiIiQnv27DEdEXB6FFsXZLVaJYnv4gHAPVq0aJGaN2+u2rVrKyoqSsHBwaYjZYmbN29q2rRpeuaZZ+Tn52c6DoB7UL58eX399dc6efKk3n33XS1fvlyVKlVSkyZNtHz5cqWkpJiOCDgliq0LstlsksQGUgBwD6ZMmaIOHTroiSee0JIlS5QrVy7TkbLMggULdPHiRUVERJiOAiCd8ubNq6FDh+ro0aOaMWOGLl26pBYtWqhixYr6+uuvdf36ddMRAadCsXVBqcWWiS0A/L3PP/9cvXr1UkREhGbNmqVs2bKZjpSlIiMjVb9+fZUtW9Z0FAD3yc/PT127dtWWLVu0ceNGVaxYUQMHDlTRokU1dOhQnT592nREwClQbF1QcHCwvLy8mNgCwF9wOBx688039fLLL2vo0KH6+uuv5e3tbTpWljpw4IDWr1/PplGAm7BYLAoLC9O8efN06NAh9ezZU19++aVKlCihp59+Wj/99JPpiIBRFFsX5OXlpXz58lFsAeAukpOT9a9//UsffPCBPv30U33wwQeyWCymY2W5CRMmKDg4WO3btzcdBUAGK1mypD7//HOdOnVKn376qX788UfVqlVLYWFhmj9/vpKTk01HBLIcxdZFcZYtANwpISFBXbt2ld1u16RJk/Tyyy+bjmREQkKCpkyZou7duyt79uym4wDIJIGBgXr++ed18OBBLViwQN7e3urYsaPKlCmjzz//nBM04FEoti4q9SxbAMB/Xbt2Ta1bt9aiRYs0b9489erVy3QkYxYvXqw//vhDffv2NR0FQBbw9vZWu3bttH79em3dulVhYWF6/fXXVaRIET3//PM6fPiw6YhApqPYuiiKLQD8fxcvXlTjxo31ww8/aMWKFWrXrp3pSEbZ7XbVqVNHFStWNB0FQBarXr26pk2bpmPHjun555/XjBkzFBISonbt2mnDhg1yOBymIwKZgmLroliKDAD/debMGdWvX18HDx7UunXr1LBhQ9ORjDp69KiioqKY1gIerlChQnr//fd14sQJjR8/XgcOHFD9+vXTim9CQoLpiECGoti6KCa2ACAdOnRIoaGhunz5smJiYlSjRg3TkYybOHGiAgMD9eSTT5qOAsAJ5MyZU3379tWePXu0YsUK2Ww29ejRQ8WLF9f777+v8+fPm44IZAiKrYuy2Ww6f/68UlJSTEcBACN27dqlsLAw+fn5KTY2VuXKlTMdybikpCRNmjRJTz/9tPz9/U3HAeBELBaLmjZtqhUrVmjv3r164oknNGLECBUtWlT9+vXT3r17TUcEHgjF1kVZrVYlJyfr0qVLpqMAQJaLiYlR/fr1VbhwYcXExKhYsWKmIzmFpUuX6rfffuPsWgB/q0KFCho/frxOnjypt99+W99//70efvjhtOLLc7hwRRRbF2Wz2SSJ5cgAPM6yZcvUpEkTVa1aVevWrZPVajUdyWnY7XbVqFFDVapUMR0FgAvIly+f3njjDR07dkzTpk3T+fPn1bx5c1WsWFHjx4/X9evXTUcE7hnF1kWlFls2kALgSWbOnKk2bdooPDxcy5cvV2BgoOlITuPkyZNavnw5m0YBSDc/Pz9169ZNW7du1YYNG1SuXDn961//UtGiRfXGG2/o9OnTpiMC/8jHdADcn9QJBRNbAJ7iyy+/1ODBg9WjRw9NmDBBPj58CfuzSZMmKUeOHOrSpYvpKABclMViUd26dVW3bl0dOXJEY8aM0dixY/XJJ5+oc+fOevHFF1W9enXTMe/JwYMHdfXqVdMxnEpAQIBCQkJMx8g0Fsc9LKKPi4tTUFCQrly5wnfHnYTD4ZCfn5+++OILDRgwwHQcAMg0DodD7733nt599129+OKL+vTTT+XlxYKjP0tOTlbJkiXVpEkTTZgwwXQcAG4kLi5OkyZN0ujRo3X06FGFhYXpxRdfVJs2beTt7W063l0dPHhQZcuWNR3DKR04cMClym16eijf7nZRFotFNpuNpcgA3FpKSopefPFFjR49WiNGjNDQoUNlsVhMx3I6q1at0smTJ9k0CkCGCwwM1AsvvKDBgwdr8eLF+s9//qMOHTqoRIkSeu6559SnTx+nG3ylTmoHDRqkwoULG07jHE6fPq2xY8e69RSbYuvCrFYrS5EBuK3ExET17t1bM2bM0Lhx49S/f3/TkZyW3W7XI488opo1a5qOAsBNeXt7q3379mrfvr22bt2qUaNGaciQIXr33XfVp08fDR48WKVKlTId8zaFCxd2ukzIPKzlcmE2m41iC8At3bhxQx06dNDs2bM1a9YsSu3f+O2337RkyRL17duXaTaALFGjRg1Nnz5dx44d0+DBg/XNN98oJCRE7du318aNGzkuCEZQbF2Y1WplKTIAt3PlyhU1bdpUa9as0ZIlS9S5c2fTkZzalClT5OPjo6efftp0FAAepnDhwhoxYoROnjypr776Sr/88ovq1aunmjVravr06UpISDAdER6EYuvCmNgCcDdnz55VgwYNtHv3bkVFRalp06amIzm1lJQUTZgwQZ06dVKePHlMxwHgoXLmzKlnn31We/fu1fLly5U3b151795dJUqU0IgRI3T+/HnTEe9bdHS0Onfu7JR/5x42bJiGDRtmOobToNi6MIotAHdy7Ngx1a1bV2fPntWGDRtUp04d05Gc3rp163TkyBHOrgXgFLy8vNSsWTOtXLlSe/bsUatWrfT++++raNGievbZZ7Vv3z7TESVJH3/8sbp3764bN2785TWjR49W165d073Z0sKFC/XTTz89aETcB4qtC7Narbp48aKSkpJMRwGAB7Jv3z6FhYUpOTlZMTExqlSpkulILsFut6t8+fIKDQ01HQUAblOxYkVFRkbqxIkTevPN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769x48bpm2++UXx8vH755RelpaWpZ8+eOnr0qNXRAcB2KLYAAADXWePGjfXUU09p69at2rNnjx5//HGtW7dOd9xxh4YMGSI3NzcVFxdr37596t27N8uSAaCMKLYAAADlqE2bNvrb3/6m1NRUffPNNyosLCw5KujcuXPatWuXevTooZycHIuTAoB9UGwBAAAs4Obmpl9++UVFRUWlxl0ul3766SeFhYVZlAwA7MfD6gAAAABV1dq1a0v+7O7uLofDoaCgIHl6eioiIsLCZABgLxRbAAAAi7zyyisaOXKkmjdvrsDAQHl6elodCQBsiWILAABgET8/P/n5+VkdAwBsj+/YAgAAAABsjWILAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1jvsBAACoQtatW6cPP/xQkvTyyy+rTZs2pa67XC49+uijOnbsmMLCwjRlypTLum9GRobi4+OVmpqqAwcOqLCwUO+9995FjzPatGmTtm7dqtTUVGVlZSk0NFQvvfTSRe9bWFiouLg4ffvtt8rNzVWzZs00YsQItWvXrow/OYDKjCe2AAAAVZCnp6c2btx4wfju3bt17NgxeXp6lul+ycnJWrFihfLz8xUYGPi7c+Pj47VlyxY1aNBAtWvX/t25H3zwgb7++mtFRERo7Nixcnd317Rp07R3794y5QNQuVFsAQAAqqAOHTooMTFR586dKzWekJCg4OBg1a1bt0z369ixo2JiYjR9+nRFRET87txHH31UMTExevHFF1WvXr1LzktNTdWmTZsUGRmpUaNGqU+fPnrhhRfUsGFDmaZZpnwAKjeKLQAAQBXUvXt35ebmaseOHSVjRUVFSkxMVPfu3ct8vzp16qhmzZqXNbdhw4Zyd//jv4YmJibK3d1dd9xxR8mYl5eXevXqpeTkZP36669lzgmgcqLYAgAAVEGNGjVSSEiIEhISSsa2bdumvLw8devWzcJk/+fgwYMKCAhQrVq1So23atVKknTo0CErYgGogCi2AAAAVVRERIS2bNmigoICSdLGjRsVGhqq+vXrW5zsNydOnLjoUuXzY8ePHy/vSAAqKIotAABAFdW1a1cVFBRo69atys/PV1JS0hUtQ75eCgsL5eFx4SEe5ze2Ol/IAYDjfgAAAKooHx8f3XzzzUpISFBBQYGKi4vVpUsXq2OV8PT0VFFR0QXjhYWFkn77vi0ASDyxBQAAqNK6d++u7du3Kz4+Xu3bt//D43fKU7169XTixIkLxs+PVZQl0wCsR7EFAACowjp16iQ3NzelpKT84TE95a158+bKzMxUXl5eqfHU1FRJUrNmzayIBaACotgCAABUYTVq1ND48eM1dOhQhYeHWx2nlM6dO6u4uFhr1qwpGSssLNS6devUqlUrNWzY0MJ0ACoSvmMLAABQxfXo0eOq75GXl6cVK1ZIkpKTkyVJq1atUq1atVS7dm3169evZO7u3bu1Z88eSVJOTo7Onj2rBQsWSJLatm2r0NBQSVJISIi6dOmi2bNn69SpU/L399f69et19OhRPfzww1edGUDlQbEFAADAVcvNzVVcXFypsWXLlkn67czc/yy2u3bt0vz580vNPf/aoUOHlhRbSXr00UcVFxenb7/9VqdPn1ZQUJD+/Oc/l5oDAG4ul8v1R5Oys7Pl6+urU6dOycfHpzxyAQAAoAySkpIUHh6u1157TcHBwVbHqTD279+v559/Xlu3blVYWJjVcQCUQVl6KN+xBQAAAADYGkuRAQAAcEm5ubkXPUv2PHd3d1b0AbAcxRYAAACX9Oabb2r37t2XvN6oUSPNmDGjHBMBwIUotgAAALik0aNHKzc395LXvby8yjENAFwcxRYAAACXxEZUAOyAzaMAAAAAALZGsQUAAAAA2BrFFgAAAABgaxRbAAAAAICtUWwBAAAAALZGsQUAAAAA2BrFFgAAAABgaxRbAAAAAICtUWwBAAAAALZGsQUAAAAA2BrFFgAAAABgaxRbAAAAAICtUWwBAAAAALZGsQUAAAAA2BrFFgAAAABgaxRbAAAAAICtUWwBAAAAALZGsQUAAAAA2BrFFgAAAABgaxRbAAAAAICtUWwBAAAAALZGsQUAAAAA2BrFFgAAAABgax5WBwAAAMC1c+TIEasjVCj8PoCqgWILAABQCXh7e0uSZsyYYXGSiun87wdA5USxBQAAqARCQkKUnJysnJwcq6NUON7e3goJCbE6BoDriGILAABQSVDeAFRVbB4FAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1ii0AAAAAwNYotgAAAAAAW6PYAgAAAABsjWILAAAAALA1j8uZ5HK5JEnZ2dnXNQwAAAAAANL/9c/zffT3XFaxzcnJkSQ1bdr0KmIBAAAAAFA2OTk58vX1/d05bq7LqL/FxcXKyMiQt7e33NzcrllAAAAAAAAuxuVyKScnR40bN5a7++9/i/ayii0AAAAAABUVm0cBAAAAAGyNYgsAAAAAsDWKLQAAAADA1ii2AAAAAABbo9gCAAAAAGyNYgsAAAAAsDWKLQAAAADA1v4/GJ6dbbgrgSoAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "petab_select.plot.graph_history(\n", " models=models,\n", @@ -314,21 +172,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "bce41584", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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6urqFh/Dhg0bdN111ykyMlI9evTQz372M5WVlTWY9+mnn+rHP/6xOnXqpCFDhqioqMhv/8cff6xx48YpPDxc0dHRuvvuu/Xll182+bpPP/20BgwYoE6dOik6Olq33npri48BwLkRZAA0aerUqVq1apXv8fPPP68pU6Y0mPfAAw/otdde0wsvvKAPPvhA/fv3V1pamk6cOCFJKi8v1y233KIJEyaopKRE//Ef/6Hf/OY3fmuUlZUpPT1dGRkZ2r17t15++WVt2bJFs2bNanH91dXVys7O1q5du1RYWKiQkBDdfPPNqq+v95s3f/583X///frwww+VkpKiCRMm6Pjx45KkyspKjR49WldddZV27dqlDRs2qKKiQrfddlujr7lr1y7Nnj1bjzzyiEpLS7VhwwaNHDmyxccA4Dzs/OltAMEpMzPTmjhxonXs2DHL6XRahw4dsg4dOmR16tTJ+uKLL6yJEydamZmZlmVZ1unTp62OHTtaa9as8T2/trbWiouLs5YtW2ZZlmXl5ORYiYmJfq+xYMECS5J18uRJy7Isa9q0adY999zjN+cf//iHFRISYp05c8ayLMvq27ev9fjjj5+37qZ88cUXliRrz549lmVZ1sGDBy1J1pIlS3xz6urqrD59+lhLly61LMuyfve731ljx471W6e8vNySZJWWllqWZVk//elPrTlz5liWZVmvvfaa5XK5LI/H02QdAFoPP9QAoEm9evXS+PHjtXr1almWpfHjx6tnz55+c8rKylRXV6ef/OQnvrGOHTtq2LBh2rdvnyRp3759Gj58uN/zUlJS/B5/9NFH2r17t9asWeMbsyxL9fX1OnjwoAYPHtzs+vfv369FixZpx44d+vLLL32fxBw+fFhDhgxptJbQ0FBdc801vto/+ugjbdq0SeHh4Q3WLysr0+WXX+43duONN6pv37669NJLlZ6ervT0dN18883q0qVLs+sHcH4EGQDnNHXqVN/pnby8vDZ7ndOnT+vee+/V7NmzG+xr6cXFEyZMUN++ffXss88qLi5O9fX1GjJkiGpra5tV14QJE7R06dIG+2JjYxuMdevWTR988IE2b96sv//971q0aJEeeugh7dy5U5GRkS06DgBN4xoZAOeUnp6u2tpa1dXVKS0trcH+yy67TGFhYXr//fd9Y3V1ddq5c6cSExMlSYMHD9Y///lPv+dt377d7/HVV1+tvXv3qn///g22sLCwZtd9/PhxlZaWauHChRozZowGDx6skydPNjr3u7V8/fXXKi4u9n0CdPXVV+uTTz7RJZdc0qCurl27NrpeaGioUlNTtWzZMu3evVuHDh3Su+++2+xjAHB+BBkA59ShQwft27dPe/fuVYcOHRrs79q1q2bOnKn58+drw4YN2rt3r6ZPn66vvvpK06ZNkyTNmDFD+/fv1/z581VaWqr8/HytXr3ab50FCxZo69atmjVrlkpKSrR//3799a9/bfHFvt27d1ePHj30zDPP6MCBA3r33XeVnZ3d6Ny8vDytW7dOn376qbKysnTy5ElNnTpV0jf3zTlx4oTuvPNO7dy5U2VlZdq4caOmTJmis2fPNljrjTfe0JNPPqmSkhJ99tlnevHFF1VfX6+BAwe26DgAnBtBBsB5uVwuuVyuJvcvWbJEGRkZuvvuu3X11VfrwIED2rhxo7p37y7pm1NDr732mtavX6/k5GStXLlSf/zjH/3WSEpKUlFRkf7v//5P119/va666iotWrRIcXFxLao5JCREBQUFKi4u1pAhQzRv3jw9+uijTda/ZMkSJScna8uWLfrb3/7muxYoLi5O77//vs6ePauxY8fqyiuv1Ny5cxUZGamQkIZvoZGRkVq7dq1Gjx6twYMHa+XKlXrppZd0xRVXtOg4AJybw7Isy+4iAAAAWoJPZAAAgLEIMgAAwFgEGQAAYCyCDAAAMBZBBgAAGIsgAwAAjEWQAQAAxiLIAAAAYxFkAACAsQgyAADAWAQZAABgrP8HjWjJuiKnxqYAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "petab_select.plot.bar_criterion_vs_models(\n", " models=models,\n", @@ -354,21 +201,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "824e2e6a", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjUAAAGxCAYAAACa3EfLAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA0BklEQVR4nO3de3wU9b3/8feGwCaQ7IZArk0ICCHcQREholSuAXs4UOG0Xk4LlNpWA5WLSnO8AFIbUEtRD17aKnh6RDxaQOsFirQJ5VoIAqIQmogSSxIEIZsEsgnJ9/eHP7asJLAJSXYzvJ4+5iHznZnvfHYhzJvvfHfWZowxAgAAaOGC/F0AAABAYyDUAAAASyDUAAAASyDUAAAASyDUAAAASyDUAAAASyDUAAAASyDUAAAASwj2dwFNraamRseOHVN4eLhsNpu/ywEAAD4wxqi0tFTx8fEKCvJtDMbyoebYsWNKTEz0dxkAAKABCgoKlJCQ4NO+ARNqFi9erIyMDN13331atmyZJKmiokJz587V6tWr5Xa7lZaWpueee04xMTE+9xseHi7p6zfF4XA0RekAAKCRuVwuJSYmeq7jvgiIULNr1y69+OKL6tevn1f77Nmz9e677+qNN96Q0+nUjBkzdNttt2nr1q0+933+lpPD4SDUAADQwtRn6ojfJwqXlZXprrvu0u9+9zu1b9/e015SUqKXXnpJS5cu1YgRIzRw4ECtWLFC27Zt044dO/xYMQAACER+DzXp6en6zne+o1GjRnm15+TkqKqqyqu9R48e6tSpk7Zv315nf263Wy6Xy2sBAADW59fbT6tXr9aePXu0a9eui7YVFRWpTZs2ioiI8GqPiYlRUVFRnX1mZmZq4cKFjV0qAAAIcH4bqSkoKNB9992nV199VSEhIY3Wb0ZGhkpKSjxLQUFBo/UNAAACl99CTU5Ojo4fP67rrrtOwcHBCg4OVnZ2tp555hkFBwcrJiZGlZWVOn36tNdxxcXFio2NrbNfu93umRTM5GAAAK4efrv9NHLkSH300UdebdOmTVOPHj00b948JSYmqnXr1tq0aZMmTZokScrNzdXRo0eVmprqj5IBAEAA81uoCQ8PV58+fbza2rVrpw4dOnjap0+frjlz5igyMlIOh0MzZ85UamqqhgwZ4o+SAQBAAAuI59TU5Te/+Y2CgoI0adIkr4fvAQAAfJPNGGP8XURTcrlccjqdKikpYX4NAAAtREOu3wE9UhOIKmoqtN+9X59UfqKzNWcVFhSmPvY+6mPvo9a21v4uDwCAqxahph5Ka0r1RukbKqspk9HXA1xf1XylzWc362P3x5ocPlkhQY338XQAAOA7vz9RuCVZX77eK9Bc6Kuar5R1Jqv5iwIAAJIINT47WX1Sx84dqzXQSJKR0eGqwyqvKW/mygAAgESo8VnhucLL7mNk9GX1l81QDQAA+CZCjY+CfHyrbPL9K9IBAEDjIdT4KKF1wmX3CVawYoPr/goHAADQdAg1PnIEOZTcOvmSIzF97X1lt9mbsSoAAHAeoaYeRrYbqZhWMZL+dZvp/P+7BHfR0NChfqsNAICrHc+pqQe7za7/CP8PfVr1qQ5WHlR5TbkcQQ71tvdWp+BOstmYTwMAgL8QauopyBakbm26qVubbv4uBQAAXIDbTwAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBIINQAAwBL8Gmqef/559evXTw6HQw6HQ6mpqXr//fc922+55RbZbDav5Wc/+5kfKwYAAIEq2J8nT0hI0OLFi5WcnCxjjF555RVNmDBBH374oXr37i1Juvvuu/XYY495jmnbtq2/ygUAAAHMr6Fm/PjxXuuPP/64nn/+ee3YscMTatq2bavY2Fh/lAcAAFqQgJlTU11drdWrV6u8vFypqame9ldffVUdO3ZUnz59lJGRoTNnzvixSgAAEKj8OlIjSR999JFSU1NVUVGhsLAwrV27Vr169ZIk3XnnnUpKSlJ8fLz279+vefPmKTc3V2vWrKmzP7fbLbfb7Vl3uVxN/hoAAID/2Ywxxp8FVFZW6ujRoyopKdGbb76p3//+98rOzvYEmwv95S9/0ciRI5WXl6euXbvW2t+CBQu0cOHCi9pLSkrkcDgavX4AAND4XC6XnE5nva7ffg813zRq1Ch17dpVL7744kXbysvLFRYWpvXr1ystLa3W42sbqUlMTCTUAADQgjQk1Pj99tM31dTUeIWSC+3du1eSFBcXV+fxdrtddru9KUoDAAABzK+hJiMjQ+PGjVOnTp1UWlqqVatWKSsrSxs2bFB+fr5WrVqlW2+9VR06dND+/fs1e/ZsDRs2TP369fNn2QAAIAD5NdQcP35cP/zhD1VYWCin06l+/fppw4YNGj16tAoKCvTBBx9o2bJlKi8vV2JioiZNmqSHH37YnyUDAIAAFXBzahpbQ+7JAQAA/2rI9TtgnlMDAABwJQg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEgg1AADAEvwaap5//nn169dPDodDDodDqampev/99z3bKyoqlJ6erg4dOigsLEyTJk1ScXGxHysGAACByq+hJiEhQYsXL1ZOTo52796tESNGaMKECfr4448lSbNnz9af/vQnvfHGG8rOztaxY8d02223+bNkAAAQoGzGGOPvIi4UGRmpJ598UpMnT1ZUVJRWrVqlyZMnS5IOHTqknj17avv27RoyZIhP/blcLjmdTpWUlMjhcDRl6QAAoJE05PodMHNqqqurtXr1apWXlys1NVU5OTmqqqrSqFGjPPv06NFDnTp10vbt2+vsx+12y+VyeS0AAMD6/B5qPvroI4WFhclut+tnP/uZ1q5dq169eqmoqEht2rRRRESE1/4xMTEqKiqqs7/MzEw5nU7PkpiY2MSvAAAABAK/h5qUlBTt3btXO3fu1D333KMpU6bok08+aXB/GRkZKikp8SwFBQWNWC0AAAhUwf4uoE2bNurWrZskaeDAgdq1a5eefvppff/731dlZaVOnz7tNVpTXFys2NjYOvuz2+2y2+1NXTYAAAgwfh+p+aaamhq53W4NHDhQrVu31qZNmzzbcnNzdfToUaWmpvqxQgAAEIj8OlKTkZGhcePGqVOnTiotLdWqVauUlZWlDRs2yOl0avr06ZozZ44iIyPlcDg0c+ZMpaam+vzJJwAAcPXwa6g5fvy4fvjDH6qwsFBOp1P9+vXThg0bNHr0aEnSb37zGwUFBWnSpElyu91KS0vTc88958+SAQBA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Dg7WoEGDPOs9evRQRESEDh48qBtuuEFlZWVasGCB3n33XRUWFurcuXM6e/asjh496tXP9ddff1Hfr7/+up555hnl5+errKxM586dk8Ph8Nqnc+fOXq8rJiZGrVq1uujP6fn3cN++fSorK7toPtXZs2eVn59f5+v09bikpCRFRUV51vv376+RI0eqb9++SktL05gxYzR58mS1b9++znMBviDU4Ko2bNgwpaWlKSMjQ1OnTvXaFhQUdNHF68K5C+e1bt3aa91ms9XaVlNT43NdZWVlGj9+vJYsWXLRtri4OM+v27Vr53OfV6KsrEyS9O677+pb3/qW1za73S5JGjdunD7//HO999572rhxo0aOHKn09HQ99dRT9TrX5d7P859QO/9+rl69Wvfff79+/etfKzU1VeHh4XryySe1c+fOy76mgQMH6tVXX71o24UX4KZw//33a+PGjXrqqafUrVs3hYaGavLkyRdNov3m7+/27dt11113aeHChUpLS5PT6dTq1av161//2mu/+v6ZLCsrU1xcnGeu0YUuNZHb1+O++TpatWqljRs3atu2bfrzn/+sZ599Vg899JB27tzpmX8FNAShBle9xYsXa8CAAUpJSfFqj4qKUlFRkYwxngtpYz5bZseOHRo2bJgk6dy5c8rJydGMGTMkSdddd53++Mc/qnPnzgoObviPqcPhUHx8vLZu3apvf/vbnvatW7fqhhtu8LmfXr16yW636+jRo179fFNUVJSmTJmiKVOm6Oabb9YDDzygp556Sm3atJH09cTbxrZ161bdeOONuvfeez1t3xxdaNOmzUXnvu666/T6668rOjr6opGO8+Li4rRz586Lfp+uu+66S9Z07tw57d692/Me5+bm6vTp0+rZs6en5qlTp+q73/2upK/DwYUTlOuybds2JSUl6aGHHvK0ff7555c97nKuu+46FRUVKTg42Gsi/IXqeg8vd1xdbDabhg4dqqFDh+rRRx9VUlKS1q5dqzlz5jTwVQB8+glQ3759ddddd+mZZ57xar/lllv05Zdf6oknnlB+fr6WL1/u9ambK7V8+XKtXbtWhw4dUnp6uk6dOqUf/ehHkqT09HR99dVXuuOOO7Rr1y7l5+drw4YNmjZtWr2DwQMPPKAlS5bo9ddfV25urn7xi19o7969uu+++3zuIzw8XPfff79mz56tV155Rfn5+dqzZ4+effZZvfLKK5KkRx99VG+99Zby8vL08ccf65133vFcxKOjoxUaGqr169eruLhYJSUl9XoNl5KcnKzdu3drw4YNOnz4sB555BHt2rXLa5/OnTtr//79ys3N1YkTJ1RVVaW77rpLHTt21IQJE/S3v/1NR44cUVZWln7+85/riy++kCTdd999Wrx4sdatW6dDhw7p3nvv1enTpy9bU+vWrTVz5kzt3LlTOTk5mjp1qoYMGeIJOcnJyVqzZo327t2rffv26c477/RpJC85OVlHjx7V6tWrlZ+fr2eeeUZr166t/5v2DaNGjVJqaqomTpyoP//5z/rss8+0bds2PfTQQ9q9e7ekr9/DI0eOaO/evTpx4oTcbrdPx9Vm586d+tWvfqXdu3fr6NGjWrNmjb788kvPnxegoQg1gKTHHnvsootKz5499dxzz2n58uXq37+//v73v1/R3JtvWrx4sRYvXqz+/ftry5Ytevvtt9WxY0dJ8oyuVFdXa8yYMerbt69mzZqliIgIr3kRvvj5z3+uOXPmaO7cuerbt6/Wr1+vt99+W8nJyfXqZ9GiRXrkkUeUmZmpnj17auzYsXr33Xc9twvatGmjjIwM9evXT8OGDVOrVq20evVqSV/PMXnmmWf04osvKj4+XhMmTKjXuS/lpz/9qW677TZ9//vf1+DBg3Xy5EmvURtJuvvuu5WSkqLrr79eUVFR2rp1q9q2bavNmzerU6dOuu2229SzZ09Nnz5dFRUVnpGbuXPn6gc/+IGmTJniubV1fnTlUtq2bat58+bpzjvv1NChQxUWFqbXX3/ds33p0qVq3769brzxRo0fP15paWmXHf2RpH//93/X7NmzNWPGDA0YMEDbtm3TI488Us937GI2m03vvfeehg0bpmnTpql79+66/fbb9fnnn3vmME2aNEljx47V8OHDFRUVpddee82n42rjcDi0efNm3Xrrrerevbsefvhh/frXv9a4ceOu+LXg6mYzl5vxBgDw2cqVKzVr1iyfRnQANC5GagAAgCUQagAAgCVw+wkAAFgCIzUAAMASCDUAAMASCDUAAMASCDUAAMASCDUAAMASCDUAAMASCDUAAMASCDUAAMASCDUAAMAS/h+a8TAoH6v8pwAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "petab_select.plot.scatter_criterion_vs_n_estimated(\n", " models=models,\n", @@ -389,26 +225,15 @@ "\n", "This shows the relative change in parameters of each model, compared to its predecessor model.\n", "\n", - "N.B.: this may give a misleading impression of the models calibrated in each iteration, since it's only based on \"predecessor model\" relationships. In this example, each layer is indeed an iteration." + "Each column is an iteration." ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "5ce191fc", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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44036oMPPpAkVapUKXc9hRYAJeHqq6/Whx9+KCn7A6ijOAcBsM24aHErxjgAAAAALmOMUUxMjC688EL99NNPGjZsmObPn5+v6AIAxSk2NlaNGzfWL7/8os8++0zTp09XjRo1bMcCAJyCY9w6TRgAAADwU5MnT1abNm00Y8YMtW7d2nYcAH5m1qxZat26tSZOnKg2bdrYjgPAzyUmJioyMlI//PGDQsNDbcfJdTjpsK5vcr0SEhIUERFhO04uir0AAACAi2RlZalZs2aqVq2aZs2aZTsOAD/j9Xp12WWXKSgoSAsWLJDjOLYjAfBzR4u9s1bPcl2x94Zzb3BdsZeZvQAAAICLfPbZZ1q9erUGDBhgOwoAPzR+/HgtX75cAwYMoNALAEVo6NChatasmSIiIhQREaGWLVvq+++/z33+yJEj6ty5sypVqqSwsDC1bdtWu3fvLvRxKPYCAAAALpGamqru3bvr/vvv1yWXXGI7DgA/k5GRoa5du+q2227TVVddZTsOAPiUmjVrasCAAVq+fLmWLVum6667TnfddZdWrVolSXrppZf07bffaty4cZo7d6527Nihe+65p9DH4QZtAAAAgEt88MEH2rVrl/r06WM7CgA/NGzYMG3YsEETJkywHQUAjmFyFrc4miUxMTHf+uDgYAUHBx+z/R133JHv5759+2ro0KFatGiRatasqU8++USjR4/WddddJ0kaMWKEmjRpokWLFqlFixYFzkVnLwAAAOAChw4dUr9+/dSpUyc1aNDAdhwAfiY5OVk9e/bUI488ovPPP992HAAoNWrVqqXIyMjcr/79+5/yNVlZWRo7dqwOHz6sli1bavny5crIyNANN9yQu03jxo1Vu3ZtLVy4sFB56OwFAAAAXOD1119XWlqaunfvbjsKAD/0zjvv6ODBg+rVq5ftKABQqmzdujXfDdqO19V71O+//66WLVvqyJEjCgsL08SJE3Xuuefq119/VVBQkMqXL59v+ypVqmjXrl2FykOxFwAAALBs+/btGjx4sF555RVVrVrVdhwAfmbfvn0aOHCgOnfurDp16tiOAwDHZYyRMS4a45CT5egN1wqiUaNG+vXXX5WQkKDx48fr0Ucf1dy5c4s0F8VeAAAAwLKePXuqXLlyevXVV21HAeCH+vbtK8dxFBsbazsKAPi0oKAg1a9fX5J08cUXa+nSpRo8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srJ0wQUXqEqVKpo1a5Ycx7EdCYAfMcbosssuU0BAgBYuXMg5CACKUGJioiIjIzV80XCVC3PPiJyU5BQ93uJxJSQkKCIiwnacXHzcCAAAgFLv888/16pVqzRixAiKLABK3Pjx47Vs2TLNmTOHcxAAFBNjjNzUs+qmLHlxfRsAAABKtSNHjqh79+669957demll9qOA8DPZGRkqGvXrrrlllt09dVX244DAPBzdPYCAACgVPvggw+0Y8cO9e3b13YUAH7ok08+0fr16zVu3DjbUQAAoNgLAACA0uvQoUPq27ev/vOf/6hhw4a24wDwM4cPH1bPnj318MMP64ILLrAdBwB8mslZ3MJNWfJijAMAAABKrYEDByotLU09evSwHQWAH3rnnXd04MAB9e7d23YUAIDL9e/fX5deeqnCw8NVuXJltWnTRmvWrMm3zTXXXCPHcfJ9PfXUU4U6DsVeAAAAlEo7duzQO++8oxdffFHVqlWzHQeAn9m3b58GDhyop59+WmeffbbtOAAAl5s7d646d+6sRYsWaebMmcrIyNCNN96ow4cP59uuU6dO2rlzZ+7XwIEDC3UcxjgAAACgVOrZs6fKli2r1157zXYUAH6oX79+Msaoa9eutqMAgF8wxsgY94xOKGyWadOm5ft55MiRqly5spYvX66rrroqd325cuVUtWrV085FZy8AAABKnTVr1uiTTz5R165dFRkZaTsOAD/z119/6YMPPtCrr76qs846y3YcAIBFiYmJ+b7S0tIK9LqEhARJUsWKFfOtHzVqlKKiotS0aVPFxMQoJSWlUHno7AUAAECp07VrV1WvXl3PPPOM7SgA/FCPHj1UoUIFvfTSS7ajAAAsq1WrVr6fe/Toofj4+JO+xuv16sUXX1SrVq3UtGnT3PUPPfSQ6tSpo+rVq2vFihXq0qWL1qxZowkTJhQ4D8VeAAAAlCqLFy/W119/rZEjRyokJMR2HAB+5vfff9dnn32m999/X2FhYbbjAIDfMDmLWxzNsnXrVkVEROSuDw4OPuVrO3furJUrV2r+/Pn51j/xxBO5359//vmqVq2arr/+em3YsEH16tUrUC6KvQAAACg1jDGKjo5W06ZN9e9//9t2HAB+KDY2Vuecc446depkOwoAwAUiIiLyFXtP5dlnn9WUKVM0b9481axZ86TbXn755ZKk9evXU+wFAACA75k+fbrmzJmjb7/9VgEBAbbjAPAzP/74o6ZMmaKxY8cqMDDQdhwAQClijNFzzz2niRMnas6cOapbt+4pX/Prr79KkqpVq1bg4zjGTbexAwAAAE7A6/XqoosuUnh4uObNmyfHcWxHAuBHjDFq1aqV0tLStHTpUnk83O8cAEpCYmKiIiMj9dGCj1Q2rKztOLlSk1P1RKsnlJCQUKDO3meeeUajR4/W5MmT1ahRo9z1kZGRKlu2rDZs2KDRo0fr1ltvVaVKlbRixQq99NJLqlmzpubOnVvgXHT2AgAAoFQYM2aMfvvtNy1YsIBCL4AS980332jhwoWaMWMGhV4AQKENHTpUknTNNdfkWz9ixAh16NBBQUFBmjVrlt555x0dPnxYtWrVUtu2bRUXF1eo49DZCwAAANdLS0tT48aNdcEFF2jSpEm24wDwM5mZmWrWrJlq1KihmTNn2o4DAH7FVzp7SwqdvQAAAHC9Dz/8UFu2bNF3331nOwoAP/TZZ5/pjz/+0Oeff247CgD4LZOzuIWbsuTFtScAAABwtcTERPXu3VsdOnTQueeeazsOAD+Tmpqq7t27q127drr44ottxwEA4KQo9gIAAMDVBg0apKSkJMXHx9uOAsAPvf/++9q9e7f69OljOwoAAKfEGAcAAAC41u7duzVo0CA999xzqlWrlu04APzMwYMH1a9fP3Xq1En169e3HQcA/JoxRm669ZibsuRFZy8AAABcq3fv3ipTpoxiYmJsRwHgh15//XWlp6ere/futqMAAFAgFHsBAADgShs2bNCHH36o6OhoVaxY0XYcAH5m+/btGjx4sF5++WVVrVrVdhwAAAqEMQ4AAABwpW7duqly5cp6/vnnbUcB4Ifi4+MVGhqqV1991XYUAIAkk7O4hZuy5EWxFwAAAK7z888/a8yYMfroo49Urlw523EA+Jk///xTw4cP16BBgxQREWE7DgAABcYYBwAAALhOTEyMGjVqpMcee8x2FAB+KDY2VrVq1dLTTz9tOwoAAIVCZy8AAABc5YcfftCMGTP09ddfq0wZ/roKoGQtWrRIEydO1Geffabg4GDbcQAAOYwxMsY9oxPclCUvx7g1GQAAAPyOMUaXXXaZAgICtHDhQjmOYzsSAD9ijNE111yjgwcP6pdfflFAQIDtSADg9xITExUZGakhPw5R2bCytuPkSk1O1TNXPqOEhARXjfyhVQIAAACuMX78eC1btkxz5syh0AugxH3//feaN2+evvvuOwq9AIBSiWIvAAAAXCEjI0OxsbG65ZZbdPXVV9uOA8DPZGVlKTo6WldddZVuueUW23EAAP9gcha3cFOWvCj2AgAAwBU++eQTbdiwQePHj7cdBYAfGj16tH7//XdGyAAASjWP7QAAAADA4cOH1bNnTz388MO64IILbMcB4GfS0tLUrVs33X333WrRooXtOAAAnDY6ewEAAGDdO++8o/3796tXr162owDwQ0OHDtXWrVv1/fff244CADgBY4yMcc/oBDdlyYvOXgAAAFi1b98+vf7663rmmWdUt25d23EA+JnExET17dtXjz/+uJo0aWI7DgAAZ4RiLwAAAKzq16+fJKlr166WkwDwR2+++aaSk5MVHx9vOwoAAGeMMQ4AAACw5q+//tIHH3yguLg4nXXWWbbjAPAzu3bt0qBBg/TCCy+oRo0atuMAAE7C5Cxu4aYsedHZCwAAAGu6d++uChUq6KWXXrIdBYAf6t27t4KCgtSlSxfbUQAAKBJ09gIAAMCK33//XZ9//rnef/99hYWF2Y4DwM+sX79eH330kfr166cKFSrYjgMAQJGg2AsAAAArYmJidM4556hTp062owDwQ3FxcapataqeffZZ21EAAAVgjJEx7hmd4KYseVHsBQAAQImbN2+evvvuO40ZM0aBgYG24wDwM8uXL9eXX36pYcOGqWzZsrbjAABQZJjZCwAAgBJljFGXLl100UUX6f7777cdB4Afio6OVpMmTfToo4/ajgIAQJGisxcAAAAlavLkyVq0aJFmzJghj4feAwAla+bMmZo1a5YmTpyoMmV4SwwApYXJWdzCTVny4m/XAAAAKDGZmZmKjY3VDTfcoNatW9uOA8DPeL1eRUdHq2XLlrrrrrtsxwEAoMjxMSYAAABKzKeffqo//vhDn3/+ue0oAPzQuHHj9PPPP2vevHlyHMd2HAAAihzFXgAAAJSI1NRU9ejRQ/fff78uvvhi23EA+Jn09HR17dpVt912m6688krbcQAAhWSMkTHuGZ3gpix5UewFAABAiXjvvfe0e/du9enTx3YUAH5o2LBh2rhxoyZOnGg7CgAAxYaZvQAAACh2Bw8eVP/+/dWpUyc1aNDAdhwAfiY5OVm9evXSI488ovPPP992HAAAig2d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VapUSWFhYWrbtq12795dqONQ7AUAAPBxxhhFR0frvPPOU/v27W3HgYs4wY6cDo5URzLzjMxsI1WRnMccOaH/KPauzn7ezM4p9u7U3z/vzLNPjyPn347UVDKLjcwMI5WTnEcdOVEUe/1VbGyszj77bD3xxBO2owAASinjwqUw5s6dq86dO2vRokWaOXOmMjIydOONN+rw4cO527z00kv69ttvNW7cOM2dO1c7duzQPffcU6jjlDn1JgAAACjNZsyYof/973+aPHmyAgICbMfBaXAudORceOpCqeelwvdyOJGOnHYF2PfdHunuAu6zrCPnLke6q9Bx4IPmz5+vb7/9VqNHj1ZQUJDtOAAAFKnExMR8PwcHBys4OPiY7aZNm5bv55EjR6py5cpavny5rrrqKiUkJOiTTz7R6NGjdd1110mSRowYoSZNmmjRokVq0aJFgfLQ2QsAAODDvF6vunTpolatWumOO+6wHQeAnzHGqEuXLrrwwgvVrl0723EAAChytWrVUmRkZO5X//79C/S6hIQESVLFihUlScuXL1dGRoZuuOGG3G0aN26s2rVra+HChQXOQ2cvAACADxs7dqx+++03/fjjj3IcLqH3FyYlZ9TCiXh0zJgGoDh8++23+umnnzR9+nR5PPQaAQBOnzFGxhRudEJxOppl69atioiIyF1/vK7ef/J6vXrxxRfVqlUrNW3aVJK0a9cuBQUFqXz58vm2rVKlinbt2lXgXBR7AQAAfFR6erri4uJ0xx136F//+pftOChB5ksjbT7JBuUl5yWKvSheWVlZiomJ0XXXXafWrVvbjgMAQLGIiIjIV+wtiM6dO2vlypWaP39+keeh2AsAAOCjPvzwQ/3111/65ptvbEdBCXNucqTUk2wQWGJR4Mc+++wzrV69WiNHjuTKAgAAcjz77LOaMmWK5s2bp5o1a+aur1q1qtLT03Xo0KF83b27d+9W1apVC7x/ir0AAAA+KCkpSb1791b79u1zLw2D/3CqU1iDXampqerevbvuu+8+XXrppbbjAAB8gMlZ3KKwWYwxeu655zRx4kTNmTNHdevWzff8xRdfrMDAQP3www9q27atJGnNmjXasmWLWrZsWeDjUOwFAADwQYMGDVJiYqJ69uxpOwoAP/TBBx9o586d6tu3r+0oAAC4QufOnTV69GhNnjxZ4eHhuXN4IyMjVbZs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Ro0atiMBx7Vz50699dZbevHFF/n3FABQ6hljZIx7Rie4KUteHtsBAAAAfFX37t1VoUIFvfTSS7ajoIjMmzdP1157rerWrat58+ZRQIOr9e7dW8HBwerSpYvtKAAAoIRQ7AUAACgGK1as0BdffKHu3bsrNDTUdhwUge+++0433XSTLrnkEs2aNUuVKlWyHQk4oXXr1umjjz5STEyMypcvbzsOAAAoIRR7AQAAikFMTIzq1aunTp062Y6CIjBmzBi1adNGN998s7777juFh4fbjgScVFxcnKpVq6Znn33WdhQAAIqEceHiRszsBQAAKGJz587V1KlTNXbsWAUGBtqOgzM0dOhQde7cWe3bt9ewYcNUpgx/hYa7LVu2TF999ZU++eQTlS1b1nYcAABQgujsBQAAKELGGHXp0kUXX3yx7rvvPttxcAaMMerXr5+eeeYZPf/88xo+fDiFXpQK0dHROvfcc9W+fXvbUQAAQAnjb6sAAABFaNKkSVq8eLFmzpwpj4fP1UsrY4xee+01vfnmm+rVq5fi4uLkOI7tWMApzZw5Uz/88IMmTZrEhxMAAJ9jjDtHJ7gJ//cHAAAoIpmZmYqNjVXr1q11ww032I6D05SVlaUnn3xSn3zyid59910999xztiMBBeL1etWlSxddccUVuvPOO23HAQAAFlDsBQAAKCIjR47Un3/+qVGjRtmOgtOUlpamhx9+WJMmTdLnn3+uf//737YjAQX21Vdf6ZdfftGPP/5IJzoAAH6KYi8AAEARSElJUXx8vNq1a6eLLrrIdhychuTkZN1zzz2aN2+eJkyYQGckSpX09HR17dpVt99+u/71r3/ZjgMAQJEzOYtbuClLXhR7AQAAisB7772n3bt3q0+fPraj4DQcOHBAt912m1auXKnvv/9e1157re1IQKF8/PHH2rRpkyZPnmw7CgAAsIhiLwAAwBk6cOCABgwYoCeeeEL169e3HQeFtHPnTt14443auXOn/ve//+mSSy6xHQkolOTkZPXq1Uvt27dX06ZNbccBAAAWUewFAAA4QwMGDFBGRoa6detmOwoKadOmTWrdurWOHDmiH3/8UU2aNLEdCSi0t956SwkJCerVq5ftKAAAFBtjjIxxz+gEN2XJy2M7AAAAQGm2bds2vffee3r55ZdVtWpV23FQCKtWrVKrVq3kOI4WLFhAoRel0t69e/XGG2+oc+fOql27tu04AADAMoq9AAAAZyA+Pl5hYWH673//azsKCmHx4sW66qqrVLlyZc2fP1916tSxHQk4LX369JHH41FsbKztKAAAwAUo9gIAAJym1atXa8SIEYqLi1NERITtOCigH374Qddff72aNGmiOXPmqEqVKrYjAadl06ZNGjp0qLp06aJKlSrZjgMAQLEyLlzciGIvAADAaeratatq166tp556ynYUFNDEiRN166236sorr9T06dNVvnx525GA09atWzdVqlRJL7zwgu0oAADAJbhBGwAAwGn46aefNGnSJH3++ecKDg62HQcFMHLkSHXs2FH33nuvPv/8cwUFBdmOBJy23377TaNHj9bQoUMVGhpqOw4AAHAJir0AAACFZIxRdHS0mjVrpoceesh2HBTAO++8o5deeklPPPGEhgwZooCAANuRgDMSExOj+vXr6/HHH7cdBQCAEmGMkTHuGZ3gpix5UewFAAAopKlTp+rHH3/U1KlT5fEwFcvNjDGKj49Xr1691KVLF/Xv31+O49iOBZyROXPm6Pvvv9e4ceMUGBhoOw4AAHARx7i1DA0AAOBCWVlZat68uSpVqqT//e9/FA5dzOv16sUXX9R7772nAQMGqEuXLrYjAWfMGKMWLVrIGKPFixdzDgIA+LzExERFRkaq41cdFVTOPWO40lPS9cn9nyghIcFVN2umsxcAAKAQRo0apZUrV2rhwoUUWVwsIyNDjz/+uEaNGqUPP/xQTzzxhO1IQJGYMGGClixZoh9++IFzEADAr5icxS3clCUvir0AAAAFdOTIEXXr1k333HOPWrRoYTsOTiA1NVXt2rXTtGnTNGbMGLVr1852JKBIZGZmqmvXrrrpppt03XXX2Y4DAABciGIvAABAAQ0dOlTbtm3T9OnTbUfBCSQmJurOO+/UkiVL9M033+jmm2+2HQkoMiNGjNCaNWs0ZswY21EAAIBLUewFAAAogISEBPXp00cdO3ZU48aNbcfBcezbt08333yz1q9fr5kzZ6pVq1a2IwFFJiUlRT169NBDDz2kCy+80HYcAABKnDFGbrr1mJuy5EWxFwAAoADeeOON3GIL3Gfbtm1q3bq1Dhw4oLlz5+qCCy6wHQkoUu+++6727dun3v/f3n3HVVn+fxx/HzaioLhQM0eOXKk5ytTUEnffsuEqZ/mrTNvlzIHb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AAAgB6PYCwAAHJphGOrTp48qV66sl156yew4Dmfy5Mnq2bOnOnfurK+++kpubrw9BDKqX79+Kl26tLp37252FAAAnJZhGDIM67ROsFKW1Hg3DwAAHNqvv/6q4OBgLV26VK6urmbHcRiGYWj48OH66KOP9Pbbb2vcuHFycaHDF5BRGzZs0M8//6w5c+bI3d3d7DgAACCHsxlWLUMDAADcht1u18MPP6w8efJo/fr1stlsZkdyCIZh6P3339cnn3yioUOHqn///rx2wF0wDEN169ZVbGystm7dygUTAACyQGRkpPz8/PTsd8/KPZd1LqzGR8frh84/KCIiQr6+vmbHScHKXgAA4LDmzJmjP//8U6GhoRQr71BCQoL+7//+T99++60mTpyonj17mh0JcFhLly7Vxo0btXLlSgq9AABkMePawyqslCU1ir0AAMAhxcbGasCAAXr66af12GOPmR3HIcTGxqpDhw5asmSJZs6cSY9j4B4kJCSob9++aty4sQIDA82OAwAAIIliLwAAcFBffvmljh8/rmXLlpkdxSFcuXJFrVu31oYNG7R48WI99dRTZkcCHNqMGTO0b98+zZw50+woAAAAKSj2AgAAh3P58mUNGzZMXbp0UcWKFc2OY3kXL15UixYttHfvXq1YsUINGzY0OxLg0GJiYjRw4EC1bdtWNWrUMDsOAAA5gmEYstKtx6yUJTWKvQAAwOGMGzdOkZGRGjx4sNlRLO/06dNq0qSJTp8+rTVr1qhmzZpmRwIc3qRJk3T27FkNGzbM7CgAAABpUOwFAAAO5ezZsxo7dqx69eql4sWLmx3H0v755x8FBgYqLi5OGzZsUIUKFcyOBDi8S5cuaeTIkerevbvKlCljdhwAAIA0KPYCAACHMmzYMLm5ualv375mR7G0PXv2qEmTJvLx8VFISIhKlChhdiTAKYwePVqxsbEaOHCg2VEAAMhRjGsPq7BSltRczA4AAABwpw4fPqwvvvhCffr0kb+/v9lxLGvz5s16/PHHVahQIQq9QCY6efKkPv30U7377rsKCAgwOw4AAEA6FHsBAIDD+Oijj1SoUCG9+eabZkexrN9++01PPvmkKlasqODgYBUuXNjsSIDTGDx4sHx8fPTBBx+YHQUAAOCGaOMAAAAcwo4dOzRnzhxNnTpVuXLlMjuOJf3www9q3769nnjiCS1atIjX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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# # Customize the colors\n", "# criterion_values = [model.get_criterion(petab_select.Criterion.AICC) for model in models]\n", diff --git a/doc/examples/workflow_cli.ipynb b/doc/examples/workflow_cli.ipynb index 9acf36b8..6ddd902a 100644 --- a/doc/examples/workflow_cli.ipynb +++ b/doc/examples/workflow_cli.ipynb @@ -8,12 +8,12 @@ "# Example usage with the CLI\n", "This notebook demonstrates usage of `petab_select` to perform model selection with commands.\n", "\n", - "Note that the criterion values in this notebook are for demonstrative purposes only, and are not real (the models were not calibrated)." + "Note that the criterion values in this notebook are for demonstrative purposes only, and are not real. An additional point is that models store the iteration where they were calibrated, but the iteration counter is stored in the candidate space. Hence, when the candidate space (or method) changes in this notebook, the iteration counter is reset." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "18dbcbbb", "metadata": {}, "outputs": [], @@ -63,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "eab391ee", "metadata": {}, "outputs": [], @@ -90,63 +90,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "1f6ac569", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "- criteria: {}\n", - " estimated_parameters: {}\n", - " model_hash: M1_0-000\n", - " model_id: M1_0-000\n", - " model_subspace_id: M1_0\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: 0\n", - " k2: 0\n", - " k3: 0\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: null\n", - "- criteria: {}\n", - " estimated_parameters: {}\n", - " model_hash: M1_1-000\n", - " model_id: M1_1-000\n", - " model_subspace_id: M1_1\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: 0.2\n", - " k2: 0.1\n", - " k3: estimate\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: null\n", - "- criteria: {}\n", - " estimated_parameters: {}\n", - " model_hash: M1_2-000\n", - " model_id: M1_2-000\n", - " model_subspace_id: M1_2\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: 0.2\n", - " k2: estimate\n", - " k3: 0\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: null\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(output_path / \"uncalibrated_models_1.yaml\") as f:\n", " print(f.read())" @@ -168,7 +115,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "73665662-60ea-425c-843e-24a98c64c6a6", "metadata": {}, "outputs": [], @@ -202,66 +149,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "703da45d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "- criteria:\n", - " AIC: 180\n", - " estimated_parameters: {}\n", - " model_hash: M1_0-000\n", - " model_id: M1_0-000\n", - " model_subspace_id: M1_0\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: 0\n", - " k2: 0\n", - " k3: 0\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: null\n", - "- criteria:\n", - " AIC: 100\n", - " estimated_parameters: {}\n", - " model_hash: M1_1-000\n", - " model_id: M1_1-000\n", - " model_subspace_id: M1_1\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: 0.2\n", - " k2: 0.1\n", - " k3: estimate\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: null\n", - "- criteria:\n", - " AIC: 50\n", - " estimated_parameters: {}\n", - " model_hash: M1_2-000\n", - " model_id: M1_2-000\n", - " model_subspace_id: M1_2\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: 0.2\n", - " k2: estimate\n", - " k3: 0\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: null\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(\"model_selection/calibrated_models_1.yaml\") as f:\n", " print(f.read())" @@ -277,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "22dfcc1f", "metadata": {}, "outputs": [], @@ -321,48 +212,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "dd2f8850", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "- criteria: {}\n", - " estimated_parameters: {}\n", - " model_hash: M1_4-000\n", - " model_id: M1_4-000\n", - " model_subspace_id: M1_4\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: 0.2\n", - " k2: estimate\n", - " k3: estimate\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: M1_2-000\n", - "- criteria: {}\n", - " estimated_parameters: {}\n", - " model_hash: M1_6-000\n", - " model_id: M1_6-000\n", - " model_subspace_id: M1_6\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: estimate\n", - " k2: estimate\n", - " k3: 0\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: M1_2-000\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(output_path / \"uncalibrated_models_2.yaml\") as f:\n", " print(f.read())" @@ -378,7 +231,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "29cb0d84-4399-4e6b-895c-e92f9cc82d68", "metadata": {}, "outputs": [], @@ -412,36 +265,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "54c5b027", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "criteria:\n", - " AIC: 15\n", - "estimated_parameters:\n", - " k2: 0.15\n", - " k3: 0.0\n", - "model_hash: M1_4-000\n", - "model_id: M1_4-000\n", - "model_subspace_id: M1_4\n", - "model_subspace_indices:\n", - "- 0\n", - "- 0\n", - "- 0\n", - "parameters:\n", - " k1: 0\n", - " k2: estimate\n", - " k3: estimate\n", - "petab_yaml: ../model_selection/petab_problem.yaml\n", - "predecessor_model_hash: null\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(\"model_selection/calibrated_M1_4.yaml\") as f:\n", " print(f.read())" @@ -449,7 +276,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "818e59e4", "metadata": {}, "outputs": [], @@ -475,33 +302,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "9f393030", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "- criteria: {}\n", - " estimated_parameters: {}\n", - " model_hash: M1_7-000\n", - " model_id: M1_7-000\n", - " model_subspace_id: M1_7\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: estimate\n", - " k2: estimate\n", - " k3: estimate\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: M1_4-000\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(output_path / \"uncalibrated_models_3.yaml\") as f:\n", " print(f.read())" @@ -509,7 +313,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "a4084bd1-5bd7-4e12-8146-67137da4909a", "metadata": {}, "outputs": [], @@ -536,37 +340,10 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "9ef2fe2f", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "criteria:\n", - " AIC: 20\n", - "estimated_parameters:\n", - " k1: 0.25\n", - " k2: 0.1\n", - " k3: 0.0\n", - "model_hash: M1_7-000\n", - "model_id: M1_7-000\n", - "model_subspace_id: M1_7\n", - "model_subspace_indices:\n", - "- 0\n", - "- 0\n", - "- 0\n", - "parameters:\n", - " k1: estimate\n", - " k2: estimate\n", - " k3: estimate\n", - "petab_yaml: ../model_selection/petab_problem.yaml\n", - "predecessor_model_hash: null\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(\"model_selection/calibrated_M1_7.yaml\") as f:\n", " print(f.read())" @@ -574,7 +351,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "35ed7ceb-6783-4956-9951-dbc55bfa9239", "metadata": {}, "outputs": [], @@ -592,19 +369,10 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "5fe1e848-e112-4ad2-ae09-57cdb7506ff8", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[]\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(output_path / \"uncalibrated_models_4.yaml\") as f:\n", " print(f.read())" @@ -612,7 +380,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "02df7ed9-422d-4f28-9b01-8670be873933", "metadata": {}, "outputs": [], @@ -629,19 +397,10 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "57e483fd-5ffa-48a4-8c2a-359f6ebd1422", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "terminate: true\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(\"output_cli/metadata.yaml\") as f:\n", " print(f.read())" @@ -658,7 +417,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "id": "d5b5087d", "metadata": {}, "outputs": [], @@ -679,63 +438,10 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "30721bfa", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "- criteria: {}\n", - " estimated_parameters: {}\n", - " model_hash: M1_3-000\n", - " model_id: M1_3-000\n", - " model_subspace_id: M1_3\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: estimate\n", - " k2: 0.1\n", - " k3: 0\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: null\n", - "- criteria: {}\n", - " estimated_parameters: {}\n", - " model_hash: M1_5-000\n", - " model_id: M1_5-000\n", - " model_subspace_id: M1_5\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: estimate\n", - " k2: 0.1\n", - " k3: estimate\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: null\n", - "- criteria: {}\n", - " estimated_parameters: {}\n", - " model_hash: M1_6-000\n", - " model_id: M1_6-000\n", - " model_subspace_id: M1_6\n", - " model_subspace_indices:\n", - " - 0\n", - " - 0\n", - " - 0\n", - " parameters:\n", - " k1: estimate\n", - " k2: estimate\n", - " k3: 0\n", - " petab_yaml: ../model_selection/petab_problem.yaml\n", - " predecessor_model_hash: null\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(output_path / \"uncalibrated_models_5.yaml\") as f:\n", " print(f.read())" @@ -752,7 +458,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "73d54111", "metadata": {}, "outputs": [], @@ -772,36 +478,10 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "id": "c36564f1", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "criteria:\n", - " AIC: 15.0\n", - "estimated_parameters:\n", - " k2: 0.15\n", - " k3: 0.0\n", - "model_hash: M1_4-000\n", - "model_id: M1_4-000\n", - "model_subspace_id: M1_4\n", - "model_subspace_indices:\n", - "- 0\n", - "- 0\n", - "- 0\n", - "parameters:\n", - " k1: 0\n", - " k2: estimate\n", - " k3: estimate\n", - "petab_yaml: ../model_selection/petab_problem.yaml\n", - "predecessor_model_hash: null\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "with open(output_path / \"best_model.yaml\") as f:\n", " print(f.read())" @@ -817,18 +497,10 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "id": "d5d03cd6", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "petab_select/doc/examples/output_cli/best_model_petab/problem.yaml\n" - ] - } - ], + "outputs": [], "source": [ "%%bash -s \"$output_path_str\"\n", "output_path_str=$1\n", diff --git a/doc/examples/workflow_python.ipynb b/doc/examples/workflow_python.ipynb index cbc7afb8..ba5d7e72 100644 --- a/doc/examples/workflow_python.ipynb +++ b/doc/examples/workflow_python.ipynb @@ -27,23 +27,10 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "eab391ee", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Information about the model selection problem:\n", - "YAML: model_selection/petab_select_problem.yaml\n", - "Method: forward\n", - "Criterion: Criterion.AIC\n", - "Version: beta_1\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "import petab_select\n", "from petab_select import Model\n", @@ -83,6 +70,7 @@ "Model hash: {model.get_hash()}\n", "Model ID: {model.model_id}\n", "{select_problem.criterion}: {model.get_criterion(select_problem.criterion, compute=False)}\n", + "Model calibrated in iteration: {model.iteration}\n", "\"\"\"\n", " )\n", "\n", @@ -130,7 +118,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "f0f327ad", "metadata": {}, "outputs": [], @@ -160,24 +148,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "edefa697", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Model subspace ID: M1_0\n", - "PEtab YAML location: model_selection/petab_problem.yaml\n", - "Custom model parameters: {'k1': 0, 'k2': 0, 'k3': 0}\n", - "Model hash: M1_0-000\n", - "Model ID: M1_0-000\n", - "Criterion.AIC: None\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "for candidate_model in iteration[UNCALIBRATED_MODELS]:\n", " print_model(candidate_model)" @@ -193,7 +167,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "0f027ef2", "metadata": {}, "outputs": [], @@ -210,24 +184,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "1c51dd49", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Model subspace ID: M1_0\n", - "PEtab YAML location: model_selection/petab_problem.yaml\n", - "Custom model parameters: {'k1': 0, 'k2': 0, 'k3': 0}\n", - "Model hash: M1_0-000\n", - "Model ID: M1_0-000\n", - "Criterion.AIC: 200\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "local_best_model = petab_select.ui.get_best(\n", " problem=select_problem, models=iteration_results[MODELS]\n", @@ -254,7 +214,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "b15c30ea", "metadata": {}, "outputs": [], @@ -284,39 +244,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "5b6969ca", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Model subspace ID: M1_1\n", - "PEtab YAML location: model_selection/petab_problem.yaml\n", - "Custom model parameters: {'k1': 0.2, 'k2': 0.1, 'k3': 'estimate'}\n", - "Model hash: M1_1-000\n", - "Model ID: M1_1-000\n", - "Criterion.AIC: 150\n", - "\n", - "Model subspace ID: M1_2\n", - "PEtab YAML location: model_selection/petab_problem.yaml\n", - "Custom model parameters: {'k1': 0.2, 'k2': 'estimate', 'k3': 0}\n", - "Model hash: M1_2-000\n", - "Model ID: M1_2-000\n", - "Criterion.AIC: 140\n", - "\n", - "\u001b[1mBEST MODEL OF CURRENT ITERATION\u001b[0m\n", - "Model subspace ID: M1_3\n", - "PEtab YAML location: model_selection/petab_problem.yaml\n", - "Custom model parameters: {'k1': 'estimate', 'k2': 0.1, 'k3': 0}\n", - "Model hash: M1_3-000\n", - "Model ID: M1_3-000\n", - "Criterion.AIC: 130\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "iteration_results = dummy_calibration_tool(\n", " problem=select_problem, candidate_space=iteration_results[CANDIDATE_SPACE]\n", @@ -341,32 +272,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "6d3468d3", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Model subspace ID: M1_5\n", - "PEtab YAML location: model_selection/petab_problem.yaml\n", - "Custom model parameters: {'k1': 'estimate', 'k2': 0.1, 'k3': 'estimate'}\n", - "Model hash: M1_5-000\n", - "Model ID: M1_5-000\n", - "Criterion.AIC: -70\n", - "\n", - "\u001b[1mBEST MODEL OF CURRENT ITERATION\u001b[0m\n", - "Model subspace ID: M1_6\n", - "PEtab YAML location: model_selection/petab_problem.yaml\n", - "Custom model parameters: {'k1': 'estimate', 'k2': 'estimate', 'k3': 0}\n", - "Model hash: M1_6-000\n", - "Model ID: M1_6-000\n", - "Criterion.AIC: -110\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "iteration_results = dummy_calibration_tool(\n", " problem=select_problem, candidate_space=iteration_results[CANDIDATE_SPACE]\n", @@ -391,25 +300,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "9f9c438c", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[1mBEST MODEL OF CURRENT ITERATION\u001b[0m\n", - "Model subspace ID: M1_7\n", - "PEtab YAML location: model_selection/petab_problem.yaml\n", - "Custom model parameters: {'k1': 'estimate', 'k2': 'estimate', 'k3': 'estimate'}\n", - "Model hash: M1_7-000\n", - "Model ID: M1_7-000\n", - "Criterion.AIC: 50\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "iteration_results = dummy_calibration_tool(\n", " problem=select_problem, candidate_space=iteration_results[CANDIDATE_SPACE]\n", @@ -434,7 +328,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "30344b30", "metadata": {}, "outputs": [], @@ -454,18 +348,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "7843fcb6", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of candidate models: 0.\n" - ] - } - ], + "outputs": [], "source": [ "print(f\"Number of candidate models: {len(iteration_results[MODELS])}.\")" ] @@ -480,24 +366,10 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "219d27e4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Model subspace ID: M1_6\n", - "PEtab YAML location: model_selection/petab_problem.yaml\n", - "Custom model parameters: {'k1': 'estimate', 'k2': 'estimate', 'k3': 0}\n", - "Model hash: M1_6-000\n", - "Model ID: M1_6-000\n", - "Criterion.AIC: -110\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "best_model = petab_select.ui.get_best(\n", " problem=select_problem,\n", @@ -517,7 +389,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "cacda13d", "metadata": {}, "outputs": [], @@ -534,24 +406,10 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "7440cc69", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Model subspace ID: M1_4\n", - "PEtab YAML location: model_selection/petab_problem.yaml\n", - "Custom model parameters: {'k1': 0.2, 'k2': 'estimate', 'k3': 'estimate'}\n", - "Model hash: M1_4-000\n", - "Model ID: M1_4-000\n", - "Criterion.AIC: None\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "for candidate_model in candidate_space.models:\n", " print_model(candidate_model)" diff --git a/doc/index.rst b/doc/index.rst index 697bcba6..13a26268 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -59,6 +59,7 @@ interfaces, and can be installed from PyPI, with: problem_definition examples + analysis Test Suite api diff --git a/petab_select/__init__.py b/petab_select/__init__.py index 665c4102..033e6c62 100644 --- a/petab_select/__init__.py +++ b/petab_select/__init__.py @@ -2,6 +2,7 @@ import sys +from .analyze import * from .candidate_space import * from .constants import * from .criteria import * diff --git a/petab_select/analyze.py b/petab_select/analyze.py new file mode 100644 index 00000000..9dad49b5 --- /dev/null +++ b/petab_select/analyze.py @@ -0,0 +1,161 @@ +"""Methods to analyze results of model selection.""" + +import warnings +from collections.abc import Callable + +from .constants import Criterion +from .model import Model, ModelHash, default_compare +from .models import Models + +__all__ = [ + # "get_predecessor_models", + "group_by_predecessor_model", + "group_by_iteration", + "get_best_by_iteration", +] + + +# def get_predecessor_models(models: Models) -> Models: +# """Get all models that were predecessors to other models. +# +# Args: +# models: +# The models +# +# Returns: +# The predecessor models. +# """ +# predecessor_models = Models([ +# models.get( +# model.predecessor_model_hash, +# # Handle virtual initial model. +# model.predecessor_model_hash, +# ) for model in models +# ]) +# return predecessor_models + + +def group_by_predecessor_model(models: Models) -> dict[ModelHash, Models]: + """Group models by their predecessor model. + + Args: + models: + The models. + + Returns: + Key is predecessor model hash, value is models. + """ + result = {} + for model in models: + if model.predecessor_model_hash not in result: + result[model.predecessor_model_hash] = Models() + result[model.predecessor_model_hash].append(model) + return result + + +def group_by_iteration( + models: Models, sort: bool = True +) -> dict[int | None, Models]: + """Group models by their iteration. + + Args: + models: + The models. + sort: + Whether to sort the iterations. + + Returns: + Key is iteration, value is models. + """ + result = {} + for model in models: + if model.iteration not in result: + result[model.iteration] = Models() + result[model.iteration].append(model) + if sort: + result = {iteration: result[iteration] for iteration in sorted(result)} + return result + + +def get_best( + models: Models, + criterion: Criterion, + compare: Callable[[Model, Model], bool] | None = None, + compute_criterion: bool = False, +) -> Model: + """Get the best model. + + Args: + models: + The models. + criterion. + The criterion. + compare: + The method used to compare two models. + Defaults to :func:``petab_select.model.default_compare``. + compute_criterion: + Whether to try computing criterion values, if sufficient + information is available (e.g., likelihood and number of + parameters, to compute AIC). + + Returns: + The best model. + """ + if compare is None: + compare = default_compare + + best_model = None + for model in models: + if compute_criterion and not model.has_criterion(criterion): + model.get_criterion(criterion) + if not model.has_criterion(criterion): + warnings.warn( + f"The model `{model.hash}` has no value set for criterion " + f"`{criterion}`. Consider using `compute_criterion=True` " + "if there is sufficient information already stored in the " + "model (e.g. the likelihood).", + RuntimeWarning, + stacklevel=2, + ) + continue + if best_model is None: + best_model = model + continue + if compare(best_model, model, criterion=criterion): + best_model = model + if best_model is None: + raise KeyError( + "None of the supplied models have a value set for the criterion " + f"`{criterion}`." + ) + return best_model + + +def get_best_by_iteration( + models: Models, + *args, + **kwargs, +) -> dict[int, Models]: + """Get the best model of each iteration. + + See :func:``get_best`` for additional required arguments. + + Args: + models: + The models. + *args, **kwargs: + Forwarded to :func:``get_best``. + + Returns: + The strictly improving models. Keys are iteration, values are models. + """ + iterations_models = group_by_iteration(models=models) + best_by_iteration = { + iteration: get_best( + *args, + models=iteration_models, + **kwargs, + ) + for iteration, iteration_models in iterations_models.items() + } + return best_by_iteration diff --git a/petab_select/candidate_space.py b/petab_select/candidate_space.py index 8af42c14..03dd2f78 100644 --- a/petab_select/candidate_space.py +++ b/petab_select/candidate_space.py @@ -63,6 +63,8 @@ class CandidateSpace(abc.ABC): An example of a difference is in the bidirectional method, where ``governing_method`` stores the bidirectional method, whereas `method` may also store the forward or backward methods. + iteration: + The iteration of model selection. limit: A handler to limit the number of accepted models. models: @@ -104,6 +106,7 @@ def __init__( summary_tsv: TYPE_PATH = None, previous_predecessor_model: Model | None = None, calibrated_models: Models | None = None, + iteration: int = 0, ): """See class attributes for arguments.""" self.method = method @@ -130,6 +133,7 @@ def __init__( self.criterion = criterion self.calibrated_models = calibrated_models or Models() self.latest_iteration_calibrated_models = Models() + self.iteration = iteration def set_iteration_user_calibrated_models( self, user_calibrated_models: Models | None @@ -187,9 +191,11 @@ def set_iteration_user_calibrated_models( self.models = iteration_uncalibrated_models def get_iteration_calibrated_models( - self, calibrated_models: dict[str, Model], reset: bool = False - ) -> dict[str, Model]: - """Get the full list of calibrated models for the current iteration. + self, + calibrated_models: Models, + reset: bool = False, + ) -> Models: + """Get all calibrated models for the current iteration. The full list of models identified for calibration in an iteration of model selection may include models for which calibration results are @@ -206,9 +212,12 @@ def get_iteration_calibrated_models( Whether to remove the previously calibrated models from the candidate space, after they are used to produce the full list of calibrated models. + iteration: + If provided, the iteration attribute of each model will be set + to this. Returns: - The full list of calibrated models. + All calibrated models for the current iteration. """ combined_calibrated_models = ( self.iteration_user_calibrated_models + calibrated_models @@ -217,6 +226,9 @@ def get_iteration_calibrated_models( self.set_iteration_user_calibrated_models( user_calibrated_models=Models() ) + for model in combined_calibrated_models: + model.iteration = self.iteration + return combined_calibrated_models def write_summary_tsv(self, row: list[Any]): diff --git a/petab_select/constants.py b/petab_select/constants.py index 9afc1cb6..2946aeb5 100644 --- a/petab_select/constants.py +++ b/petab_select/constants.py @@ -49,6 +49,7 @@ # PEtab Select model selection problem (but may be subsequently stored in the # PEtab Select model report format. PREDECESSOR_MODEL_HASH = "predecessor_model_hash" +ITERATION = "iteration" PETAB_PROBLEM = "petab_problem" PETAB_YAML = "petab_yaml" HASH = "hash" diff --git a/petab_select/model.py b/petab_select/model.py index fbb040d2..81d73145 100644 --- a/petab_select/model.py +++ b/petab_select/model.py @@ -15,6 +15,7 @@ from .constants import ( CRITERIA, ESTIMATED_PARAMETERS, + ITERATION, MODEL_HASH, MODEL_HASH_DELIMITER, MODEL_ID, @@ -46,6 +47,7 @@ "Model", "default_compare", "ModelHash", + "VIRTUAL_INITIAL_MODEL_HASH", ] @@ -63,6 +65,9 @@ class Model(PetabMixin): Functions to convert attributes from :class:`Model` to YAML. criteria: The criteria values of the calibrated model (e.g. AIC). + iteration: + The iteration of the model selection algorithm where this model was + identified. model_id: The model ID. petab_yaml: @@ -90,6 +95,7 @@ class Model(PetabMixin): PARAMETERS, ESTIMATED_PARAMETERS, CRITERIA, + ITERATION, ) converters_load = { MODEL_ID: lambda x: x, @@ -105,6 +111,7 @@ class Model(PetabMixin): Criterion(criterion_id_value): float(criterion_value) for criterion_id_value, criterion_value in x.items() }, + ITERATION: lambda x: int(x) if x is not None else x, } converters_save = { MODEL_ID: lambda x: str(x), @@ -126,6 +133,7 @@ class Model(PetabMixin): criterion_id.value: float(criterion_value) for criterion_id, criterion_value in x.items() }, + ITERATION: lambda x: int(x) if x is not None else None, } def __init__( @@ -138,6 +146,7 @@ def __init__( parameters: dict[str, int | float] = None, estimated_parameters: dict[str, int | float] = None, criteria: dict[str, float] = None, + iteration: int = None, # Optionally provided to reduce repeated parsing of `petab_yaml`. petab_problem: petab.Problem | None = None, model_hash: Any | None = None, @@ -149,6 +158,7 @@ def __init__( self.parameters = parameters self.estimated_parameters = estimated_parameters self.criteria = criteria + self.iteration = iteration self.predecessor_model_hash = predecessor_model_hash if self.predecessor_model_hash is not None: @@ -536,6 +546,10 @@ def __str__(self): # data = f'{self.model_id}\t{self.petab_yaml}\t{parameter_values}' return f"{header}\n{data}" + def __repr__(self) -> str: + """The model hash.""" + return f'' + def get_mle(self) -> dict[str, float]: """Get the maximum likelihood estimate of the model.""" """ @@ -952,11 +966,7 @@ def get_model(self, petab_select_problem: Problem) -> Model: return petab_select_problem.model_space.model_subspaces[ self.model_subspace_id - ].indices_to_model( - self.unhash_model_subspace_indices( - self.model_subspace_indices_hash - ) - ) + ].indices_to_model(self.unhash_model_subspace_indices()) def __hash__(self) -> str: """The PEtab hash. @@ -981,3 +991,6 @@ def __eq__(self, other_hash: str | ModelHash) -> bool: # petab_hash = ModelHash.from_hash(other_hash).petab_hash # return self.petab_hash == petab_hash return str(self) == str(other_hash) + + +VIRTUAL_INITIAL_MODEL_HASH = ModelHash.from_hash(VIRTUAL_INITIAL_MODEL) diff --git a/petab_select/models.py b/petab_select/models.py index fa0cf579..03996adb 100644 --- a/petab_select/models.py +++ b/petab_select/models.py @@ -4,11 +4,22 @@ from collections import Counter from collections.abc import Iterable, MutableSequence from pathlib import Path -from typing import TYPE_CHECKING, TypeAlias +from typing import TYPE_CHECKING, Any, TypeAlias +import numpy as np +import pandas as pd import yaml -from .constants import TYPE_PATH +from .constants import ( + CRITERIA, + ESTIMATED_PARAMETERS, + ITERATION, + MODEL_HASH, + MODEL_ID, + PREDECESSOR_MODEL_HASH, + TYPE_PATH, + Criterion, +) from .model import ( Model, ModelHash, @@ -112,7 +123,6 @@ def __getitem__( case ModelHash() | str(): return self._models[self._hashes.index(key)] case slice(): - print(key) return self.__class__(self._models[key]) case Iterable(): # TODO sensible to yield here? @@ -306,6 +316,15 @@ def get( except KeyError: return default + def values(self) -> Models: + """Get the models. DEPRECATED.""" + warnings.warn( + "`models.values()` is deprecated. Use `models` instead.", + DeprecationWarning, + stacklevel=2, + ) + return self + class Models(ListDict): """A collection of models. @@ -408,6 +427,113 @@ def to_yaml( with open(output_yaml, "w") as f: yaml.safe_dump(model_dicts, f) + def get_criterion( + self, + criterion: Criterion, + as_dict: bool = False, + relative: bool = False, + ) -> list[float] | dict[ModelHash, float]: + """Get the criterion value for all models. + + Args: + criterion: + The criterion. + as_dict: + Whether to return a dictionary, with model hashes for keys. + relative: + Whether to compute criterion values relative to the + smallest criterion value. + + Returns: + The criterion values. + """ + result = [model.get_criterion(criterion=criterion) for model in self] + if relative: + result = list(np.array(result) - min(result)) + if as_dict: + result = dict(zip(self._hashes, result, strict=False)) + return result + + def _getattr( + self, + attr: str, + key: Any = None, + use_default: bool = False, + default: Any = None, + ) -> list[Any]: + """Get an attribute of each model. + + Args: + attr: + The name of the attribute (e.g. ``MODEL_ID``). + key: + The key of the attribute, if you want to further subset. + For example, if ``attr=ESTIMATED_PARAMETERS``, this could + be a specific parameter ID. + use_default: + Whether to use a default value for models that are missing + ``attr`` or ``key``. + default: + Value to use for models that do not have ``attr`` or ``key``, + if ``use_default==True``. + + Returns: + The list of attribute values. + """ + # FIXME remove when model is `dataclass` + values = [] + for model in self: + try: + value = getattr(model, attr) + except: + if not use_default: + raise + value = default + + if key is not None: + try: + value = value[key] + except: + if not use_default: + raise + value = default + + values.append(value) + return values + + @property + def df(self) -> pd.DataFrame: + """Get a dataframe of model attributes.""" + return pd.DataFrame( + { + MODEL_ID: self._getattr(MODEL_ID), + MODEL_HASH: self._getattr(MODEL_HASH), + Criterion.NLLH: self._getattr( + CRITERIA, Criterion.NLLH, use_default=True + ), + Criterion.AIC: self._getattr( + CRITERIA, Criterion.AIC, use_default=True + ), + Criterion.AICC: self._getattr( + CRITERIA, Criterion.AICC, use_default=True + ), + Criterion.BIC: self._getattr( + CRITERIA, Criterion.BIC, use_default=True + ), + ITERATION: self._getattr(ITERATION, use_default=True), + PREDECESSOR_MODEL_HASH: self._getattr( + PREDECESSOR_MODEL_HASH, use_default=True + ), + ESTIMATED_PARAMETERS: self._getattr( + ESTIMATED_PARAMETERS, use_default=True + ), + } + ) + + @property + def hashes(self) -> list[ModelHash]: + return self._hashes + def models_from_yaml_list( model_list_yaml: TYPE_PATH, diff --git a/petab_select/plot.py b/petab_select/plot.py index 08137c82..859c6a33 100644 --- a/petab_select/plot.py +++ b/petab_select/plot.py @@ -12,10 +12,14 @@ import numpy as np import upsetplot from more_itertools import one -from toposort import toposort -from .constants import VIRTUAL_INITIAL_MODEL, Criterion -from .model import Model, ModelHash +from .analyze import ( + get_best_by_iteration, + group_by_iteration, +) +from .constants import Criterion +from .model import VIRTUAL_INITIAL_MODEL_HASH, Model, ModelHash +from .models import Models RELATIVE_LABEL_FONTSIZE = -2 NORMAL_NODE_COLOR = "darkgrey" @@ -25,80 +29,14 @@ "bar_criterion_vs_models", "graph_history", "graph_iteration_layers", - "line_selected", + "line_best_by_iteration", "scatter_criterion_vs_n_estimated", "upset", ] -def get_model_hashes(models: list[Model]) -> dict[str, Model]: - """Get the model hash to model mapping. - - Args: - models: - The models. - - Returns: - The mapping. - """ - model_hashes = {model.get_hash(): model for model in models} - return model_hashes - - -def get_selected_models( - models: list[Model], - criterion: Criterion, -) -> list[Model]: - """Get the models that strictly improved on their predecessors. - - Args: - models: - The models. - criterion: - The criterion - - Returns: - The strictly improving models. - """ - criterion_value0 = np.inf - model0 = None - model_hashes = get_model_hashes(models) - for model in models: - criterion_value = model.get_criterion(criterion) - if criterion_value < criterion_value0: - criterion_value0 = criterion_value - model0 = model - - selected_models = [model0] - while True: - model0 = selected_models[-1] - model1 = model_hashes.get(model0.predecessor_model_hash, None) - if model1 is None: - break - selected_models.append(model1) - - return selected_models[::-1] - - -def get_relative_criterion_values( - criterion_values: dict[str, float] | list[float], -) -> dict[str, float] | list[float]: - values = criterion_values - if isinstance(criterion_values, dict): - values = criterion_values.values() - - value0 = np.inf - for value in values: - if value < value0: - value0 = value - - if isinstance(criterion_values, dict): - return {k: v - value0 for k, v in criterion_values.items()} - return [v - value0 for v in criterion_values] - - def upset( - models: list[Model], criterion: Criterion + models: Models, criterion: Criterion ) -> dict[str, matplotlib.axes.Axes | None]: """Plot an UpSet plot of estimated parameters and criterion. @@ -112,16 +50,15 @@ def upset( The plot axes (see documentation from the `upsetplot `__ package). """ # Get delta criterion values - values = np.array( - get_relative_criterion_values( - [model.get_criterion(criterion) for model in models] - ) - ) + values = np.array(models.get_criterion(criterion=criterion, relative=True)) # Sort by criterion value index = np.argsort(values) values = values[index] - labels = [models[i].get_estimated_parameter_ids_all() for i in index] + labels = [ + model.get_estimated_parameter_ids_all() + for model in np.array(models)[index] + ] with warnings.catch_warnings(): # TODO remove warnings context manager when fixed in upsetplot package @@ -137,8 +74,8 @@ def upset( return axes -def line_selected( - models: list[Model], +def line_best_by_iteration( + models: Models, criterion: Criterion, relative: bool = True, fz: int = 14, @@ -168,7 +105,9 @@ def line_selected( Returns: The plot axes. """ - models = get_selected_models(models=models, criterion=criterion) + best_by_iteration = get_best_by_iteration( + models=models, criterion=criterion + ) if labels is None: labels = {} @@ -178,20 +117,22 @@ def line_selected( _, ax = plt.subplots(figsize=(5, 4)) linewidth = 3 - models = [model for model in models if model != VIRTUAL_INITIAL_MODEL] + iterations = sorted(best_by_iteration) + best_models = Models( + [best_by_iteration[iteration] for iteration in iterations] + ) + iteration_labels = [ + str(iteration) + f"\n({labels.get(model.get_hash(), model.model_id)})" + for iteration, model in zip(iterations, best_models, strict=True) + ] - criterion_values = { - labels.get(model.get_hash(), model.model_id): model.get_criterion( - criterion - ) - for model in models - } - if relative: - criterion_values = get_relative_criterion_values(criterion_values) + criterion_values = best_models.get_criterion( + criterion=criterion, relative=relative + ) ax.plot( - criterion_values.keys(), - criterion_values.values(), + iteration_labels, + criterion_values, linewidth=linewidth, color=NORMAL_NODE_COLOR, marker="x", @@ -206,11 +147,12 @@ def line_selected( ax.set_ylabel((r"$\Delta$" if relative else "") + criterion, fontsize=fz) # could change to compared_model_ids, if all models are plotted ax.set_xticklabels( - criterion_values.keys(), + ax.get_xticklabels(), fontsize=fz + RELATIVE_LABEL_FONTSIZE, ) - for tick in ax.yaxis.get_major_ticks(): - tick.label1.set_fontsize(fz + RELATIVE_LABEL_FONTSIZE) + ax.yaxis.set_tick_params( + which="major", labelsize=fz + RELATIVE_LABEL_FONTSIZE + ) ytl = ax.get_yticks() ax.set_ylim([min(ytl), max(ytl)]) # removing top and right borders @@ -220,7 +162,7 @@ def line_selected( def graph_history( - models: list[Model], + models: Models, criterion: Criterion = None, labels: dict[str, str] = None, colors: dict[str, str] = None, @@ -259,27 +201,23 @@ def graph_history( default_spring_layout_kwargs = {"k": 1, "iterations": 20} if spring_layout_kwargs is None: spring_layout_kwargs = default_spring_layout_kwargs - model_hashes = get_model_hashes(models) - criterion_values = { - model_hash: model.get_criterion(criterion) - for model_hash, model in model_hashes.items() - } - if relative: - criterion_values = get_relative_criterion_values(criterion_values) + criterion_values = models.get_criterion( + criterion=criterion, relative=relative, as_dict=True + ) if labels is None: labels = { - model_hash: model.model_id + model.get_hash(): model.model_id + ( - f"\n{criterion_values[model_hash]:.2f}" + f"\n{criterion_values[model.get_hash()]:.2f}" if criterion is not None else "" ) - for model_hash, model in model_hashes.items() + for model in models } labels = labels.copy() - labels[VIRTUAL_INITIAL_MODEL] = "Virtual\nInitial\nModel" + labels[VIRTUAL_INITIAL_MODEL_HASH] = "Virtual\nInitial\nModel" G = nx.DiGraph() edges = [] @@ -289,8 +227,8 @@ def graph_history( from_ = labels.get(predecessor_model_hash, predecessor_model_hash) # may only not be the case for # COMPARED_MODEL_ID == INITIAL_VIRTUAL_MODEL - if predecessor_model_hash in model_hashes: - predecessor_model = model_hashes[predecessor_model_hash] + if predecessor_model_hash in models: + predecessor_model = models[predecessor_model_hash] from_ = labels.get( predecessor_model.get_hash(), predecessor_model.model_id, @@ -370,29 +308,24 @@ def bar_criterion_vs_models( Returns: The plot axes. """ - model_hashes = get_model_hashes(models) - if bar_kwargs is None: bar_kwargs = {} if labels is None: - labels = { - model_hash: model.model_id - for model_hash, model in model_hashes.items() - } + labels = {model.get_hash(): model.model_id for model in models} if ax is None: _, ax = plt.subplots() - criterion_values = { - labels.get(model.get_hash(), model.model_id): model.get_criterion( - criterion - ) - for model in models - } + bar_model_labels = [ + labels.get(model.get_hash(), model.model_id) for model in models + ] + criterion_values = models.get_criterion( + criterion=criterion, relative=relative + ) if colors is not None: - if label_diff := set(colors).difference(criterion_values): + if label_diff := set(colors).difference(bar_model_labels): raise ValueError( "Colors were provided for the following model labels, but " f"these are not in the graph: {label_diff}" @@ -403,10 +336,8 @@ def bar_criterion_vs_models( for model_label in criterion_values ] - if relative: - criterion_values = get_relative_criterion_values(criterion_values) - ax.bar(criterion_values.keys(), criterion_values.values(), **bar_kwargs) - ax.set_xlabel("Model labels") + ax.bar(bar_model_labels, criterion_values, **bar_kwargs) + ax.set_xlabel("Model") ax.set_ylabel( (r"$\Delta$" if relative else "") + criterion, ) @@ -453,11 +384,9 @@ def scatter_criterion_vs_n_estimated( Returns: The plot axes. """ - model_hashes = get_model_hashes(models) - labels = { - model_hash: labels.get(model.model_id, model.model_id) - for model_hash, model in model_hashes.items() + model.get_hash(): labels.get(model.model_id, model.model_id) + for model in models } if scatter_kwargs is None: @@ -475,12 +404,12 @@ def scatter_criterion_vs_n_estimated( ] n_estimated = [] - criterion_values = [] for model in models: n_estimated.append(len(model.get_estimated_parameter_ids_all())) - criterion_values.append(model.get_criterion(criterion)) - if relative: - criterion_values = get_relative_criterion_values(criterion_values) + + criterion_values = models.get_criterion( + criterion=criterion, relative=relative + ) if max_jitter: n_estimated = np.array(n_estimated, dtype=float) @@ -553,12 +482,9 @@ def graph_iteration_layers( Returns: The plot axes. """ - if ax is None: _, ax = plt.subplots(figsize=(20, 10)) - model_hashes = {model.get_hash(): model for model in models} - default_draw_networkx_kwargs = { #'node_color': NORMAL_NODE_COLOR, "arrowstyle": "-|>", @@ -573,20 +499,20 @@ def graph_iteration_layers( model.get_hash(): model.predecessor_model_hash for model in models } ancestry_as_set = {k: {v} for k, v in ancestry.items()} - ordering = [list(hashes) for hashes in toposort(ancestry_as_set)] + + ordering = [ + [model.get_hash() for model in iteration_models] + for iteration_models in group_by_iteration(models).values() + ] + if VIRTUAL_INITIAL_MODEL_HASH in ancestry.values(): + ordering.insert(0, [VIRTUAL_INITIAL_MODEL_HASH]) model_estimated_parameters = { model.get_hash(): set(model.estimated_parameters) for model in models } - model_criterion_values = None - model_criterion_values = { - model.get_hash(): model.get_criterion(criterion) for model in models - } - - min_criterion_value = min(model_criterion_values.values()) - model_criterion_values = { - k: v - min_criterion_value for k, v in model_criterion_values.items() - } + model_criterion_values = models.get_criterion( + criterion=criterion, relative=relative, as_dict=True + ) model_parameter_diffs = { model.get_hash(): ( @@ -658,7 +584,7 @@ def __getitem__(self, key): labels = { model_hash: ( label0 - if model_hash == ModelHash.from_hash(VIRTUAL_INITIAL_MODEL) + if model_hash == VIRTUAL_INITIAL_MODEL_HASH else "\n".join( [ label0, @@ -731,7 +657,7 @@ def __getitem__(self, key): # Add `n=...` labels N = [len(y) for y in Y] - for x, n in zip(X, N, strict=False): + for x, n in zip(X, N, strict=True): ax.annotate( f"n={n}", xy=(x, 1.1), diff --git a/petab_select/problem.py b/petab_select/problem.py index b0a763bd..5260f9e2 100644 --- a/petab_select/problem.py +++ b/petab_select/problem.py @@ -8,6 +8,7 @@ import yaml +from .analyze import get_best from .candidate_space import CandidateSpace, method_to_candidate_space_class from .constants import ( CANDIDATE_SPACE_ARGUMENTS, @@ -242,25 +243,20 @@ def get_best( Returns: The best model. """ + warnings.warn( + "Use ``petab_select.analyze.get_best`` instead.", + DeprecationWarning, + stacklevel=2, + ) if criterion is None: criterion = self.criterion - best_model = None - for model in models: - if compute_criterion and not model.has_criterion(criterion): - model.get_criterion(criterion) - if best_model is None: - if model.has_criterion(criterion): - best_model = model - # TODO warn if criterion is not available? - continue - if self.compare(best_model, model, criterion=criterion): - best_model = model - if best_model is None: - raise KeyError( - f"None of the supplied models have a value set for the criterion {criterion}." - ) - return best_model + return get_best( + models=models, + criterion=criterion, + compare=self.compare, + compute_criterion=compute_criterion, + ) def model_hash_to_model(self, model_hash: str | ModelHash) -> Model: """Get the model that matches a model hash. diff --git a/petab_select/ui.py b/petab_select/ui.py index 301dd3df..720a319c 100644 --- a/petab_select/ui.py +++ b/petab_select/ui.py @@ -6,6 +6,7 @@ import numpy as np import petab.v1 as petab +from . import analyze from .candidate_space import CandidateSpace, FamosCandidateSpace from .constants import ( CANDIDATE_SPACE, @@ -32,7 +33,23 @@ ] -def get_iteration(candidate_space: CandidateSpace) -> dict[str, Any]: +def start_iteration_result(candidate_space: CandidateSpace) -> dict[str, Any]: + """Get the state after starting the iteration. + + Args: + candidate_space: + The candidate space. + + Returns: + The candidate space, the uncalibrated models, and the predecessor + model. + """ + # Set model iteration for the models that the calibration tool + # will see. All models (user-supplied and newly-calibrated) will + # have their iteration set (again) in `end_iteration`, via + # `CandidateSpace.get_iteration_calibrated_models` + for model in candidate_space.models: + model.iteration = candidate_space.iteration return { CANDIDATE_SPACE: candidate_space, UNCALIBRATED_MODELS: candidate_space.models, @@ -121,6 +138,9 @@ def start_iteration( raise ValueError("Please provide a criterion.") candidate_space.criterion = criterion + # Start a new iteration + candidate_space.iteration += 1 + # Set the predecessor model to the previous predecessor model. predecessor_model = candidate_space.previous_predecessor_model @@ -143,7 +163,7 @@ def start_iteration( candidate_space.set_iteration_user_calibrated_models( user_calibrated_models=user_calibrated_models ) - return get_iteration(candidate_space=candidate_space) + return start_iteration_result(candidate_space=candidate_space) # Exclude the calibrated predecessor model. if not candidate_space.excluded(predecessor_model): @@ -156,9 +176,10 @@ def start_iteration( # by calling ui.best to find the best model to jump to if # this is not the first step of the search. if candidate_space.latest_iteration_calibrated_models: - predecessor_model = problem.get_best( - candidate_space.latest_iteration_calibrated_models, + predecessor_model = analyze.get_best( + models=candidate_space.latest_iteration_calibrated_models, criterion=criterion, + compare=problem.compare, ) # If the new predecessor model isn't better than the previous one, # keep the previous one. @@ -179,7 +200,7 @@ def start_iteration( isinstance(candidate_space, FamosCandidateSpace) and candidate_space.jumped_to_most_distant ): - return get_iteration(candidate_space=candidate_space) + return start_iteration_result(candidate_space=candidate_space) if predecessor_model is not None: candidate_space.reset(predecessor_model) @@ -221,7 +242,7 @@ def start_iteration( candidate_space.set_iteration_user_calibrated_models( user_calibrated_models=user_calibrated_models ) - return get_iteration(candidate_space=candidate_space) + return start_iteration_result(candidate_space=candidate_space) def end_iteration( @@ -332,7 +353,7 @@ def models_to_petab( def get_best( problem: Problem, models: list[Model], - criterion: str | None | None = None, + criterion: str | Criterion | None = None, ) -> Model: """Get the best model from a list of models. @@ -349,7 +370,10 @@ def get_best( The best model. """ # TODO return list, when multiple models are equally "best" - return problem.get_best(models=models, criterion=criterion) + criterion = criterion or problem.criterion + return analyze.get_best( + models=models, criterion=criterion, compare=problem.compare + ) def write_summary_tsv( diff --git a/pyproject.toml b/pyproject.toml index 780b7e2b..77e1d38c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,8 @@ test = [ "pytest-cov >= 2.10.0", "amici >= 0.11.25", "fides >= 0.7.5", - "pypesto > 0.2.13", + # "pypesto > 0.2.13", + "pypesto @ git+https://github.com/ICB-DCM/pyPESTO.git@select_class_models#egg=pypesto", "tox >= 3.12.4", ] doc = [ diff --git a/test/analyze/input/models.yaml b/test/analyze/input/models.yaml new file mode 100644 index 00000000..264e1154 --- /dev/null +++ b/test/analyze/input/models.yaml @@ -0,0 +1,66 @@ +- criteria: + AIC: 5 + model_id: model_1 + model_subspace_id: M + model_subspace_indices: + - 0 + - 1 + - 1 + iteration: 1 + parameters: + k1: 0.2 + k2: estimate + k3: estimate + estimated_parameters: + k2: 0.15 + k3: 0.0 + petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml + predecessor_model_hash: dummy_p0-0 +- criteria: + AIC: 4 + model_id: model_2 + model_subspace_id: M + model_subspace_indices: + - 1 + - 1 + - 0 + iteration: 5 + parameters: + k1: estimate + k2: estimate + k3: 0 + petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml + predecessor_model_hash: virtual_initial_model- +- criteria: + AIC: 3 + model_id: model_3 + model_subspace_id: M2 + model_subspace_indices: + - 0 + - 1 + - 1 + iteration: 1 + parameters: + k1: 0.2 + k2: estimate + k3: estimate + estimated_parameters: + k2: 0.15 + k3: 0.0 + petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml + predecessor_model_hash: virtual_initial_model- +- criteria: + AIC: 2 + model_id: model_4 + model_subspace_id: M2 + model_subspace_indices: + - 1 + - 1 + - 0 + iteration: 2 + parameters: + k1: estimate + k2: estimate + k3: 0 + petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml + predecessor_model_hash: virtual_initial_model- diff --git a/test/analyze/test_analyze.py b/test/analyze/test_analyze.py new file mode 100644 index 00000000..32169a85 --- /dev/null +++ b/test/analyze/test_analyze.py @@ -0,0 +1,81 @@ +from pathlib import Path + +import pytest + +from petab_select import ( + VIRTUAL_INITIAL_MODEL, + Criterion, + ModelHash, + Models, + analyze, +) + +base_dir = Path(__file__).parent + +DUMMY_HASH = "dummy_p0-0" +VIRTUAL_HASH = ModelHash.from_hash(VIRTUAL_INITIAL_MODEL) + + +@pytest.fixture +def models() -> Models: + return Models.from_yaml(base_dir / "input" / "models.yaml") + + +def test_group_by_predecessor_model(models: Models) -> None: + """Test ``analyze.group_by_predecessor_model``.""" + groups = analyze.group_by_predecessor_model(models) + # Expected groups + assert len(groups) == 2 + assert VIRTUAL_HASH in groups + assert DUMMY_HASH in groups + # Expected group members + assert len(groups[DUMMY_HASH]) == 1 + assert "M-011" in groups[DUMMY_HASH] + assert len(groups[VIRTUAL_HASH]) == 3 + assert "M-110" in groups[VIRTUAL_HASH] + assert "M2-011" in groups[VIRTUAL_HASH] + assert "M2-110" in groups[VIRTUAL_HASH] + + +def test_group_by_iteration(models: Models) -> None: + """Test ``analyze.group_by_iteration``.""" + groups = analyze.group_by_iteration(models) + # Expected groups + assert len(groups) == 3 + assert 1 in groups + assert 2 in groups + assert 5 in groups + # Expected group members + assert len(groups[1]) == 2 + assert "M-011" in groups[1] + assert "M2-011" in groups[1] + assert len(groups[2]) == 1 + assert "M2-110" in groups[2] + assert len(groups[5]) == 1 + assert "M-110" in groups[5] + + +def test_get_best_by_iteration(models: Models) -> None: + """Test ``analyze.get_best_by_iteration``.""" + groups = analyze.get_best_by_iteration(models, criterion=Criterion.AIC) + # Expected groups + assert len(groups) == 3 + assert 1 in groups + assert 2 in groups + assert 5 in groups + # Expected best models + assert groups[1].get_hash() == "M2-011" + assert groups[2].get_hash() == "M2-110" + assert groups[5].get_hash() == "M-110" + + +def test_relative_criterion_values(models: Models) -> None: + """Test ``analyze.get_relative_criterion_values``.""" + # TODO move to test_models.py? + criterion_values = models.get_criterion(criterion=Criterion.AIC) + test_value = models.get_criterion(criterion=Criterion.AIC, relative=True) + expected_value = [ + criterion_value - min(criterion_values) + for criterion_value in criterion_values + ] + assert test_value == expected_value diff --git a/test/candidate_space/test_famos.py b/test/candidate_space/test_famos.py index e7cf12e7..f4ad33e1 100644 --- a/test/candidate_space/test_famos.py +++ b/test/candidate_space/test_famos.py @@ -5,7 +5,7 @@ from more_itertools import one import petab_select -from petab_select import Method +from petab_select import Method, Models from petab_select.constants import ( CANDIDATE_SPACE, MODEL_HASH, @@ -126,8 +126,7 @@ def parse_summary_to_progress_list(summary_tsv: str) -> tuple[Method, set]: return progress_list progress_list = [] - all_calibrated_models = {} - calibrated_models = {} + all_calibrated_models = Models() candidate_space = petab_select_problem.new_candidate_space() candidate_space.summary_tsv.unlink(missing_ok=True) @@ -145,7 +144,7 @@ def parse_summary_to_progress_list(summary_tsv: str) -> tuple[Method, set]: ) # Calibrate candidate models - calibrated_models = {} + calibrated_models = Models() for candidate_model in iteration[UNCALIBRATED_MODELS]: calibrate(candidate_model) calibrated_models[candidate_model.get_hash()] = candidate_model @@ -155,7 +154,7 @@ def parse_summary_to_progress_list(summary_tsv: str) -> tuple[Method, set]: candidate_space=iteration[CANDIDATE_SPACE], calibrated_models=calibrated_models, ) - all_calibrated_models.update(iteration_results[MODELS]) + all_calibrated_models += iteration_results[MODELS] candidate_space = iteration_results[CANDIDATE_SPACE] # Stop iteration if there are no candidate models diff --git a/test/cli/input/models.yaml b/test/cli/input/models.yaml index d7523afa..06aa3933 100644 --- a/test/cli/input/models.yaml +++ b/test/cli/input/models.yaml @@ -1,5 +1,10 @@ - criteria: {} model_id: model_1 + model_subspace_id: M + model_subspace_indices: + - 0 + - 1 + - 1 parameters: k1: 0.2 k2: estimate @@ -10,6 +15,11 @@ petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml - criteria: {} model_id: model_2 + model_subspace_id: M + model_subspace_indices: + - 1 + - 1 + - 0 parameters: k1: estimate k2: estimate diff --git a/test/pypesto/generate_expected_models.py b/test/pypesto/generate_expected_models.py index 7d68bd7d..912748ff 100644 --- a/test/pypesto/generate_expected_models.py +++ b/test/pypesto/generate_expected_models.py @@ -19,7 +19,7 @@ # Do not use computationally-expensive test cases in CI skip_test_cases = [ - "0009", + # "0009", ] test_cases_path = Path(__file__).resolve().parent.parent.parent / "test_cases" @@ -41,50 +41,45 @@ def objective_customizer(obj): obj.amici_solver.setRelativeTolerance(1e-12) -# Indentation to match `test_pypesto.py`, to make it easier to keep files similar. -if True: - for test_case_path in test_cases_path.glob("*"): - if test_cases and test_case_path.stem not in test_cases: - continue - - if test_case_path.stem in skip_test_cases: - continue - - expected_model_yaml = test_case_path / "expected.yaml" - - if ( - SKIP_TEST_CASES_WITH_PREEXISTING_EXPECTED_MODEL - and expected_model_yaml.is_file() - ): - # Skip test cases that already have an expected model. - continue - print(f"Running test case {test_case_path.stem}") - - # Setup the pyPESTO model selector instance. - petab_select_problem = petab_select.Problem.from_yaml( - test_case_path / "petab_select_problem.yaml", - ) - pypesto_select_problem = pypesto.select.Problem( - petab_select_problem=petab_select_problem - ) - - # Run the selection process until "exhausted". - pypesto_select_problem.select_to_completion( - minimize_options=minimize_options, - objective_customizer=objective_customizer, - ) - - # Get the best model - best_model = petab_select_problem.get_best( - models=pypesto_select_problem.calibrated_models.values(), - ) - - # Generate the expected model. - best_model.to_yaml( - expected_model_yaml, paths_relative_to=test_case_path - ) - - petab_select.model.models_to_yaml_list( - models=pypesto_select_problem.calibrated_models.values(), - output_yaml="all_models.yaml", - ) +for test_case_path in test_cases_path.glob("*"): + if test_cases and test_case_path.stem not in test_cases: + continue + + if test_case_path.stem in skip_test_cases: + continue + + expected_model_yaml = test_case_path / "expected.yaml" + + if ( + SKIP_TEST_CASES_WITH_PREEXISTING_EXPECTED_MODEL + and expected_model_yaml.is_file() + ): + # Skip test cases that already have an expected model. + continue + print(f"Running test case {test_case_path.stem}") + + # Setup the pyPESTO model selector instance. + petab_select_problem = petab_select.Problem.from_yaml( + test_case_path / "petab_select_problem.yaml", + ) + pypesto_select_problem = pypesto.select.Problem( + petab_select_problem=petab_select_problem + ) + + # Run the selection process until "exhausted". + pypesto_select_problem.select_to_completion( + minimize_options=minimize_options, + objective_customizer=objective_customizer, + ) + + # Get the best model + best_model = petab_select_problem.get_best( + models=pypesto_select_problem.calibrated_models, + ) + + # Generate the expected model. + best_model.to_yaml(expected_model_yaml, paths_relative_to=test_case_path) + + pypesto_select_problem.calibrated_models.to_yaml( + f"all_models_{test_case_path.stem}.yaml" + ) diff --git a/test_cases/0001/expected.yaml b/test_cases/0001/expected.yaml index 7149938b..25c97f14 100644 --- a/test_cases/0001/expected.yaml +++ b/test_cases/0001/expected.yaml @@ -1,8 +1,9 @@ criteria: - AIC: -6.175405094206667 - NLLH: -4.0877025471033335 + AIC: -6.1754055040468785 + NLLH: -4.087702752023439 estimated_parameters: - sigma_x2: 0.1224643186838838 + sigma_x2: 0.12242920616053495 +iteration: 1 model_hash: M1_1-000 model_id: M1_1-000 model_subspace_id: M1_1 diff --git a/test_cases/0002/expected.yaml b/test_cases/0002/expected.yaml index c82acb72..57811a85 100644 --- a/test_cases/0002/expected.yaml +++ b/test_cases/0002/expected.yaml @@ -1,9 +1,10 @@ criteria: - AIC: -4.705325358569107 - NLLH: -4.3526626792845535 + AIC: -4.705325991177407 + NLLH: -4.3526629955887035 estimated_parameters: - k1: 0.20160877227137408 - sigma_x2: 0.11715051179648493 + k1: 0.20160877932991236 + sigma_x2: 0.11714038666761385 +iteration: 2 model_hash: M1_3-000 model_id: M1_3-000 model_subspace_id: M1_3 @@ -16,4 +17,4 @@ parameters: k2: 0.1 k3: 0 petab_yaml: ../0001/petab/petab_problem.yaml -predecessor_model_hash: null +predecessor_model_hash: M1_0-000 diff --git a/test_cases/0003/expected.yaml b/test_cases/0003/expected.yaml index 48a87a33..a0366cfb 100644 --- a/test_cases/0003/expected.yaml +++ b/test_cases/0003/expected.yaml @@ -1,8 +1,9 @@ criteria: - BIC: -6.383646149270872 - NLLH: -4.087702809249463 + BIC: -6.383646034818824 + NLLH: -4.087702752023439 estimated_parameters: - sigma_x2: 0.12245324237132274 + sigma_x2: 0.12242920723808924 +iteration: 1 model_hash: M1-110 model_id: M1-110 model_subspace_id: M1 diff --git a/test_cases/0004/expected.yaml b/test_cases/0004/expected.yaml index 811edc18..24f8ae41 100644 --- a/test_cases/0004/expected.yaml +++ b/test_cases/0004/expected.yaml @@ -1,9 +1,10 @@ criteria: - AICc: -0.7053253858878037 - NLLH: -4.352662692943902 + AICc: -0.7053259911583094 + NLLH: -4.352662995579155 estimated_parameters: - k1: 0.20160877435934813 - sigma_x2: 0.11714883276066365 + k1: 0.2016087783781175 + sigma_x2: 0.11714035262205941 +iteration: 3 model_hash: M1_3-000 model_id: M1_3-000 model_subspace_id: M1_3 @@ -16,4 +17,4 @@ parameters: k2: 0.1 k3: 0 petab_yaml: ../0001/petab/petab_problem.yaml -predecessor_model_hash: null +predecessor_model_hash: M1_6-000 diff --git a/test_cases/0005/expected.yaml b/test_cases/0005/expected.yaml index 897a2432..c30365a8 100644 --- a/test_cases/0005/expected.yaml +++ b/test_cases/0005/expected.yaml @@ -1,9 +1,10 @@ criteria: - AIC: -4.705325086169246 - NLLH: -4.352662543084623 + AIC: -4.705325991200599 + NLLH: -4.3526629956003 estimated_parameters: - k1: 0.20160877910494426 - sigma_x2: 0.11716072823171682 + k1: 0.2016087798698859 + sigma_x2: 0.11714036476432785 +iteration: 2 model_hash: M1_3-000 model_id: M1_3-000 model_subspace_id: M1_3 @@ -16,4 +17,4 @@ parameters: k2: 0.1 k3: 0 petab_yaml: ../0001/petab/petab_problem.yaml -predecessor_model_hash: null +predecessor_model_hash: M1_0-000 diff --git a/test_cases/0006/expected.yaml b/test_cases/0006/expected.yaml index efd80860..c8e92c9c 100644 --- a/test_cases/0006/expected.yaml +++ b/test_cases/0006/expected.yaml @@ -1,8 +1,9 @@ criteria: - AIC: -6.175403277446156 - NLLH: -4.087701638723078 + AIC: -6.1754055040468785 + NLLH: -4.087702752023439 estimated_parameters: - sigma_x2: 0.12248840167611977 + sigma_x2: 0.12242920606535417 +iteration: 1 model_hash: M1_0-000 model_id: M1_0-000 model_subspace_id: M1_0 @@ -15,4 +16,4 @@ parameters: k2: 0.1 k3: 0 petab_yaml: ../0001/petab/petab_problem.yaml -predecessor_model_hash: null +predecessor_model_hash: virtual_initial_model diff --git a/test_cases/0007/expected.yaml b/test_cases/0007/expected.yaml index b843cd92..4efd158a 100644 --- a/test_cases/0007/expected.yaml +++ b/test_cases/0007/expected.yaml @@ -1,7 +1,8 @@ criteria: - AIC: 11.117195852885663 - NLLH: 5.558597926442832 + AIC: 11.117195861535194 + NLLH: 5.558597930767597 estimated_parameters: {} +iteration: 1 model_hash: M1_0-000 model_id: M1_0-000 model_subspace_id: M1_0 @@ -14,4 +15,4 @@ parameters: k2: 0.1 k3: 0 petab_yaml: petab/petab_problem.yaml -predecessor_model_hash: null +predecessor_model_hash: virtual_initial_model diff --git a/test_cases/0008/expected.yaml b/test_cases/0008/expected.yaml index 0fb56440..6162ff4c 100644 --- a/test_cases/0008/expected.yaml +++ b/test_cases/0008/expected.yaml @@ -1,7 +1,8 @@ criteria: - AICc: 11.117195852885663 - NLLH: 5.558597926442832 + AICc: 11.117195861535194 + NLLH: 5.558597930767597 estimated_parameters: {} +iteration: 4 model_hash: M1_0-000 model_id: M1_0-000 model_subspace_id: M1_0 @@ -14,4 +15,4 @@ parameters: k2: 0.1 k3: 0 petab_yaml: ../0007/petab/petab_problem.yaml -predecessor_model_hash: null +predecessor_model_hash: M1_3-000 diff --git a/test_cases/0009/README.md b/test_cases/0009/README.md new file mode 100644 index 00000000..37243b6e --- /dev/null +++ b/test_cases/0009/README.md @@ -0,0 +1,5 @@ +N.B. This original Blasi et al. problem is difficult to solve with a stepwise method. Many forward/backward/forward+backward variants failed. This test case was found by: +1. performing 100 FAMoS starts, initialized at random models. Usually <5% of the starts ended at the best model. +2. assessing reproducibility. Most of the starts that end at the best model are not reproducible. Instead, the path through model space can differ a lot despite "good" calibration, because many pairs of models differ in AICc by less than numerical noise. + +1 start was found that reproducibly ends at the best model. The initial model of that start is the predecessor model in this test case. However, the path through model space is not reproducible -- there are at least two possibilities, perhaps more, depending on simulation tolerances. Hence, you should expect to produce a similar `expected_summary.tsv`, but perhaps with a few rows swapped. If you see a different summary.tsv, please report (or retry a few times). diff --git a/test_cases/0009/expected.yaml b/test_cases/0009/expected.yaml index 1c0260c3..6abbaa99 100644 --- a/test_cases/0009/expected.yaml +++ b/test_cases/0009/expected.yaml @@ -1,15 +1,16 @@ criteria: - AICc: -1708.110992459583 - NLLH: -862.3517925260878 + AICc: -1708.1109924658595 + NLLH: -862.351792529226 estimated_parameters: - a_0ac_k08: 0.4085198712518596 - a_b: 0.06675755142350405 - a_k05_k05k12: 30.888893099662752 - a_k05k12_k05k08k12: 4.872831719884531 - a_k08k12k16_4ac: 53.80209580336034 - a_k12_k05k12: 8.26789880667234 - a_k12k16_k08k12k16: 33.038691003614964 - a_k16_k12k16: 10.424836834041892 + a_0ac_k08: 0.4085141271467614 + a_b: 0.06675812072340812 + a_k05_k05k12: 30.88819982704895 + a_k05k12_k05k08k12: 4.872706275493909 + a_k08k12k16_4ac: 53.80184925213997 + a_k12_k05k12: 8.267871339049703 + a_k12k16_k08k12k16: 33.03793450182137 + a_k16_k12k16: 10.42455614921354 +iteration: 11 model_hash: M-01000100001000010010000000010001 model_id: M-01000100001000010010000000010001 model_subspace_id: M @@ -80,4 +81,4 @@ parameters: a_k16_k08k16: 1 a_k16_k12k16: estimate petab_yaml: petab/petab_problem.yaml -predecessor_model_hash: null +predecessor_model_hash: M-01000100001010010010000000010001 diff --git a/tox.ini b/tox.ini index 139c8b0d..2bf5551e 100644 --- a/tox.ini +++ b/tox.ini @@ -18,8 +18,6 @@ description = [testenv:base] extras = test -deps = - git+https://github.com/ICB-DCM/pyPESTO.git@develop\#egg=pypesto commands = pytest --cov=petab_select --cov-report=xml --cov-append test -s coverage report From f8d94d7034a5ffe49375d0ba1bbd7830879c3d27 Mon Sep 17 00:00:00 2001 From: Dilan Pathirana <59329744+dilpath@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:29:14 +0100 Subject: [PATCH 2/2] Create standard/schema for a `Model` (#130) * refactor `Model` with mkstd * remove constraint column (constraints aren't implemented yet) * update test cases 1-8 expected models * refactor to remove `PetabMixin` * predecessor_model now always set to virtual or real model; update candidate_space.py * model subspace: require explicit parameter definitions; implement `can_fix_all` * fix 0009 expected yaml * add schema; add to RTD --------- Co-authored-by: Daniel Weindl --- doc/conf.py | 2 +- doc/problem_definition.rst | 53 +- doc/standard/make_schemas.py | 3 + doc/standard/model.yaml | 67 + doc/test_suite.rst | 12 +- petab_select/candidate_space.py | 128 +- petab_select/cli.py | 2 +- petab_select/constants.py | 209 +-- petab_select/model.py | 1222 +++++++---------- petab_select/model_subspace.py | 133 +- petab_select/models.py | 86 +- petab_select/petab.py | 105 +- petab_select/plot.py | 42 +- petab_select/ui.py | 28 +- pyproject.toml | 3 +- test/analyze/input/models.yaml | 8 +- test/analyze/test_analyze.py | 18 +- .../test_files/predecessor_model.yaml | 47 +- test/candidate_space/test_famos.py | 23 +- test/cli/input/model.yaml | 16 +- test/cli/input/models.yaml | 26 +- test/cli/test_cli.py | 1 - test/model/input/model.yaml | 25 +- test/pypesto/generate_expected_models.py | 27 +- test_cases/0001/expected.yaml | 20 +- test_cases/0002/expected.yaml | 20 +- test_cases/0003/expected.yaml | 20 +- test_cases/0004/expected.yaml | 20 +- test_cases/0005/expected.yaml | 20 +- test_cases/0006/expected.yaml | 20 +- test_cases/0007/expected.yaml | 18 +- test_cases/0008/expected.yaml | 16 +- test_cases/0009/expected.yaml | 58 +- test_cases/0009/predecessor_model.yaml | 4 +- 34 files changed, 1179 insertions(+), 1323 deletions(-) create mode 100644 doc/standard/make_schemas.py create mode 100644 doc/standard/model.yaml diff --git a/doc/conf.py b/doc/conf.py index 93865e8e..e21a5944 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -58,7 +58,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinx_rtd_theme" -# html_static_path = ['_static'] +html_static_path = ["standard"] html_logo = "logo/logo-wide.svg" diff --git a/doc/problem_definition.rst b/doc/problem_definition.rst index eb05699d..9545e300 100644 --- a/doc/problem_definition.rst +++ b/doc/problem_definition.rst @@ -7,7 +7,9 @@ Model selection problems for PEtab Select are defined by the following files: #. a specification of the model space, and #. (optionally) a specification of the initial candidate model. -The different file formats are described below. +The different file formats are described below. Each file format is a YAML file +and comes with a YAML-formatted JSON schema, such that these files can be +easily worked with independently of the PEtab Select library. 1. Selection problem -------------------- @@ -116,28 +118,41 @@ can be specified like selected model. Here, the format for a single model is shown. Multiple models can be specified -as a YAML list of the same format. The only required key is the ``petab_yaml``, -as a model requires a PEtab problem. Other keys are required in different +as a YAML list of the same format. Some optional keys are required in different contexts (for example, model comparison will require ``criteria``). +Brief format description +^^^^^^^^^^^^^^^^^^^^^^^^ + + .. code-block:: yaml - criteria: # dict[string, float] (optional). Criterion ID => criterion value. - estimated_parameters: # dict[string, float] (optional). Parameter ID => parameter value. - model_hash: # string (optional). - model_id: # string (optional). - model_subspace_id: # string (optional). - model_subspace_indices: # string (optional). - parameters: # dict[string, float] (optional). Parameter ID => parameter value or "estimate". - petab_yaml: # string. - predecessor_model_hash: # string (optional). + model_subspace_id: # str (required). + model_subspace_indices: # list[int] (required). + criteria: # dict[str, float] (optional). Criterion ID => criterion value. + model_hash: # str (optional). + model_subspace_petab_yaml: # str (required). + estimated_parameters: # dict[str, float] (optional). Parameter ID => parameter value. + iteration: # int (optional). + model_id: # str (optional). + parameters: # dict[str, float | int | "estimate"] (required). Parameter ID => parameter value or "estimate". + predecessor_model_hash: # str (optional). -- ``criteria``: The value of the criterion by which model selection was performed, at least. Optionally, other criterion values too. -- ``estimated_parameters``: Parameter estimates, not only of parameters specified to be estimated in a model space file, but also parameters specified to be estimated in the original PEtab problem of the model. -- ``model_hash``: The model hash, generated by the PEtab Select library. -- ``model_id``: The model ID. - ``model_subspace_id``: Same as in the model space files. - ``model_subspace_indices``: The indices that locate this model in its model subspace. -- ``parameters``: The parameters from the problem (either values or ``'estimate'``) (a specific combination from a model space file, but uncalibrated). -- ``petab_yaml``: Same as in model space files. -- ``predecessor_model_hash``: The hash of the model that preceded this model during the model selection process. +- ``criteria``: The value of the criterion by which model selection was performed, at least. Optionally, other criterion values too. +- ``model_hash``: The model hash, generated by the PEtab Select library. The format is ``[MODEL_SUBSPACE_ID]-[MODEL_SUBSPACE_INDICES_HASH]``. If all parameters are in the model are defined like ``0;estimate``, then the hash is a string of ``1`` and ``0``, for parameters that are estimated or not, respectively. +- ``model_subspace_petab_yaml``: Same as in model space files (see ``petab_yaml``). +- ``estimated_parameters``: Parameter estimates, including all estimated parameters that are not in the model selection problem; i.e., parameters that are set to be estimated in the model subspace PEtab problem but don't appear in the column header of the model space file. +- ``iteration``: The iteration of model selection in which this model appeared. +- ``model_id``: The model ID. +- ``parameters``: The parameter combination from the model space file that defines this model (either values or ``"estimate"``). Not the calibrated values, which are in ``estimated_parameters``! +- ``predecessor_model_hash``: The hash of the model that preceded this model during the model selection process. Will be ``virtual_initial_model-`` if the model had no predecessor model. + +Schema +^^^^^^ + +The format is provided as `YAML-formatted JSON schema <_static/model.yaml>`_, which enables easy validation with various third-party tools. + +.. literalinclude:: standard/model.yaml + :language: yaml diff --git a/doc/standard/make_schemas.py b/doc/standard/make_schemas.py new file mode 100644 index 00000000..8e371a11 --- /dev/null +++ b/doc/standard/make_schemas.py @@ -0,0 +1,3 @@ +from petab_select.model import ModelStandard + +ModelStandard.save_schema("model.yaml") diff --git a/doc/standard/model.yaml b/doc/standard/model.yaml new file mode 100644 index 00000000..a49a042d --- /dev/null +++ b/doc/standard/model.yaml @@ -0,0 +1,67 @@ +$defs: + ModelHash: + type: string +description: "A model.\n\nSee :class:`ModelBase` for the standardized attributes.\ + \ Additional\nattributes are available in ``Model`` to improve usability.\n\nAttributes:\n\ + \ _model_subspace_petab_problem:\n The PEtab problem of the model subspace\ + \ of this model.\n If not provided, this is reconstructed from\n :attr:`model_subspace_petab_yaml`." +properties: + model_subspace_id: + title: Model Subspace Id + type: string + model_subspace_indices: + items: + type: integer + title: Model Subspace Indices + type: array + criteria: + additionalProperties: + type: number + title: Criteria + type: object + model_hash: + $ref: '#/$defs/ModelHash' + default: null + model_subspace_petab_yaml: + anyOf: + - format: path + type: string + - type: 'null' + title: Model Subspace Petab Yaml + estimated_parameters: + anyOf: + - additionalProperties: + type: number + type: object + - type: 'null' + default: null + title: Estimated Parameters + iteration: + anyOf: + - type: integer + - type: 'null' + default: null + title: Iteration + model_id: + default: null + title: Model Id + type: string + parameters: + additionalProperties: + anyOf: + - type: number + - type: integer + - const: estimate + type: string + title: Parameters + type: object + predecessor_model_hash: + $ref: '#/$defs/ModelHash' + default: null +required: +- model_subspace_id +- model_subspace_indices +- model_subspace_petab_yaml +- parameters +title: Model +type: object diff --git a/doc/test_suite.rst b/doc/test_suite.rst index 9b9aa443..4684963a 100644 --- a/doc/test_suite.rst +++ b/doc/test_suite.rst @@ -15,7 +15,6 @@ the model format. - Method - Model space files - Compressed format - - Constraints files - Predecessor (initial) models files * - 0001 - (all) @@ -23,34 +22,29 @@ the model format. - 1 - - - - * - 0002 [#f1]_ - AIC - forward - 1 - - - - * - 0003 - BIC - - all + - brute force - 1 - Yes - - - * - 0004 - AICc - backward - 1 - - - 1 - * - 0005 - AIC - forward - 1 - - - - 1 * - 0006 - AIC @@ -58,27 +52,23 @@ the model format. - 1 - - - - * - 0007 [#f2]_ - AIC - forward - 1 - - - - * - 0008 [#f2]_ - AICc - backward - 1 - - - - * - 0009 [#f3]_ - AICc - FAMoS - 1 - Yes - - - Yes .. [#f1] Model ``M1_0`` differs from ``M1_1`` in three parameters, but only 1 additional estimated parameter. The effect of this on model selection criteria needs to be clarified. Test case 0006 is a duplicate of 0002 that doesn't have this issue. diff --git a/petab_select/candidate_space.py b/petab_select/candidate_space.py index 03dd2f78..a9a3be39 100644 --- a/petab_select/candidate_space.py +++ b/petab_select/candidate_space.py @@ -20,14 +20,20 @@ PREDECESSOR_MODEL, PREVIOUS_METHODS, TYPE_PATH, - VIRTUAL_INITIAL_MODEL, VIRTUAL_INITIAL_MODEL_METHODS, Criterion, Method, ) from .handlers import TYPE_LIMIT, LimitHandler -from .model import Model, ModelHash, default_compare +from .model import ( + VIRTUAL_INITIAL_MODEL, + VIRTUAL_INITIAL_MODEL_HASH, + Model, + ModelHash, + default_compare, +) from .models import Models +from .petab import get_petab_parameters __all__ = [ "BackwardCandidateSpace", @@ -159,11 +165,7 @@ def set_iteration_user_calibrated_models( iteration_user_calibrated_models = Models() for model in self.models: if ( - ( - user_model := user_calibrated_models.get( - model.get_hash(), None - ) - ) + (user_model := user_calibrated_models.get(model.hash, None)) is not None ) and ( user_model.get_criterion( @@ -171,18 +173,14 @@ def set_iteration_user_calibrated_models( ) is not None ): - logging.info( - f"Using user-supplied result for: {model.get_hash()}" - ) + logging.info(f"Using user-supplied result for: {model.hash}") user_model_copy = copy.deepcopy(user_model) user_model_copy.predecessor_model_hash = ( - self.predecessor_model.get_hash() - if isinstance(self.predecessor_model, Model) - else self.predecessor_model + self.predecessor_model.hash + ) + iteration_user_calibrated_models[user_model_copy.hash] = ( + user_model_copy ) - iteration_user_calibrated_models[ - user_model_copy.get_hash() - ] = user_model_copy else: iteration_uncalibrated_models.append(model) self.iteration_user_calibrated_models = ( @@ -345,11 +343,7 @@ def accept( distance: The distance of the model from the predecessor model. """ - model.predecessor_model_hash = ( - self.predecessor_model.get_hash() - if isinstance(self.predecessor_model, Model) - else self.predecessor_model - ) + model.predecessor_model_hash = self.predecessor_model.hash self.models.append(model) self.distances.append(distance) self.set_excluded_hashes(model, extend=True) @@ -376,7 +370,7 @@ def excluded( ``True`` if the ``model`` is excluded, otherwise ``False``. """ if isinstance(model_hash, Model): - model_hash = model_hash.get_hash() + model_hash = model_hash.hash return model_hash in self.get_excluded_hashes() @abc.abstractmethod @@ -417,7 +411,7 @@ def consider(self, model: Model | None) -> bool: return False if self.excluded(model): warnings.warn( - f"Model `{model.get_hash()}` has been previously excluded " + f"Model `{model.hash}` has been previously excluded " "from the candidate space so is skipped here.", RuntimeWarning, stacklevel=2, @@ -435,19 +429,14 @@ def reset_accepted(self) -> None: self.models = Models() self.distances = [] - def set_predecessor_model(self, predecessor_model: Model | str | None): + def set_predecessor_model(self, predecessor_model: Model | None): """Set the predecessor model. See class attributes for arguments. """ + if predecessor_model is None: + predecessor_model = VIRTUAL_INITIAL_MODEL self.predecessor_model = predecessor_model - if ( - self.predecessor_model == VIRTUAL_INITIAL_MODEL - and self.method not in VIRTUAL_INITIAL_MODEL_METHODS - ): - raise ValueError( - f"A virtual initial model was requested for a method ({self.method}) that does not support them." - ) def get_predecessor_model(self) -> str | Model: """Get the predecessor model.""" @@ -472,7 +461,7 @@ def set_excluded_hashes( excluded_hashes = set() for potential_hash in hashes: if isinstance(potential_hash, Model): - potential_hash = potential_hash.get_hash() + potential_hash = potential_hash.hash excluded_hashes.add(potential_hash) if extend: @@ -531,7 +520,7 @@ def wrapper(): def reset( self, - predecessor_model: Model | str | None | None = None, + predecessor_model: Model | None = None, # FIXME change `Any` to some `TYPE_MODEL_HASH` (e.g. union of str/int/float) excluded_hashes: list[ModelHash] | None = None, limit: TYPE_LIMIT = None, @@ -592,18 +581,24 @@ def distances_in_estimated_parameters( model0 = self.predecessor_model model1 = model - if model0 != VIRTUAL_INITIAL_MODEL and not model1.petab_yaml.samefile( - model0.petab_yaml + if ( + model0.hash != VIRTUAL_INITIAL_MODEL_HASH + and not model1.model_subspace_petab_yaml.samefile( + model0.model_subspace_petab_yaml + ) ): + # FIXME raise NotImplementedError( - "Computation of distances between different PEtab problems is " - "currently not supported. This error is also raised if the same " - "PEtab problem is read from YAML files in different locations." + "Computing distances between models that have different " + "model subspace PEtab problems is currently not supported. " + "This check is based on the PEtab YAML file location." ) # All parameters from the PEtab problem are used in the computation. - if model0 == VIRTUAL_INITIAL_MODEL: - parameter_ids = list(model1.petab_parameters) + if model0.hash == VIRTUAL_INITIAL_MODEL_HASH: + parameter_ids = list( + get_petab_parameters(model1._model_subspace_petab_problem) + ) if self.method == Method.FORWARD: parameters0 = np.array([0 for _ in parameter_ids]) elif self.method == Method.BACKWARD: @@ -615,21 +610,12 @@ def distances_in_estimated_parameters( "developers." ) else: - parameter_ids = list(model0.petab_parameters) + parameter_ids = list( + get_petab_parameters(model0._model_subspace_petab_problem) + ) parameters0 = np.array( model0.get_parameter_values(parameter_ids=parameter_ids) ) - # FIXME need to take superset of all parameters amongst all PEtab problems - # in all model subspaces to get an accurate comparable distance. Currently - # only reasonable when working with a single PEtab problem for all models - # in all subspaces. - if model0.petab_yaml.resolve() != model1.petab_yaml.resolve(): - raise ValueError( - "Computing the distance between different models that " - 'have different "base" PEtab problems is not yet ' - f"supported. First base PEtab problem: {model0.petab_yaml}." - f" Second base PEtab problem: {model1.petab_yaml}." - ) parameters1 = np.array( model1.get_parameter_values(parameter_ids=parameter_ids) ) @@ -722,7 +708,7 @@ def is_plausible(self, model: Model) -> bool: # A model is plausible if the number of estimated parameters strictly # increases (or decreases, if `self.direction == -1`), and no # previously estimated parameters become fixed. - if self.predecessor_model == VIRTUAL_INITIAL_MODEL or ( + if self.predecessor_model.hash == VIRTUAL_INITIAL_MODEL.hash or ( n_steps > 0 and distances["l1"] == n_steps ): return True @@ -882,7 +868,7 @@ class FamosCandidateSpace(CandidateSpace): def __init__( self, *args, - predecessor_model: Model | str | None | None = None, + predecessor_model: Model | None = None, critical_parameter_sets: list = [], swap_parameter_sets: list = [], method_scheme: dict[tuple, str] = None, @@ -914,10 +900,10 @@ def __init__( predecessor_model = VIRTUAL_INITIAL_MODEL if ( - predecessor_model == VIRTUAL_INITIAL_MODEL + predecessor_model.hash == VIRTUAL_INITIAL_MODEL.hash and critical_parameter_sets ) or ( - predecessor_model != VIRTUAL_INITIAL_MODEL + predecessor_model.hash != VIRTUAL_INITIAL_MODEL.hash and not self.check_critical(predecessor_model) ): raise ValueError( @@ -925,7 +911,7 @@ def __init__( ) if ( - predecessor_model == VIRTUAL_INITIAL_MODEL + predecessor_model.hash == VIRTUAL_INITIAL_MODEL.hash and self.initial_method not in VIRTUAL_INITIAL_MODEL_METHODS ): raise ValueError( @@ -976,11 +962,7 @@ def __init__( ), Method.LATERAL: LateralCandidateSpace( *args, - predecessor_model=( - predecessor_model - if predecessor_model != VIRTUAL_INITIAL_MODEL - else None - ), + predecessor_model=predecessor_model, max_steps=1, **kwargs, ), @@ -1097,7 +1079,8 @@ def update_from_iteration_calibrated_models( go_into_switch_method = True for model in iteration_calibrated_models: if ( - self.best_model_of_current_run == VIRTUAL_INITIAL_MODEL + self.best_model_of_current_run.hash + == VIRTUAL_INITIAL_MODEL_HASH or default_compare( model0=self.best_model_of_current_run, model1=model, @@ -1183,9 +1166,9 @@ def check_swap(self, model: Model) -> bool: return True predecessor_estimated_parameters_ids = set( - self.predecessor_model.get_estimated_parameter_ids_all() + self.predecessor_model.get_estimated_parameter_ids() ) - estimated_parameters_ids = set(model.get_estimated_parameter_ids_all()) + estimated_parameters_ids = set(model.get_estimated_parameter_ids()) swapped_parameters_ids = estimated_parameters_ids.symmetric_difference( predecessor_estimated_parameters_ids @@ -1198,7 +1181,7 @@ def check_swap(self, model: Model) -> bool: def check_critical(self, model: Model) -> bool: """Check if the model contains all necessary critical parameters""" - estimated_parameters_ids = set(model.get_estimated_parameter_ids_all()) + estimated_parameters_ids = set(model.get_estimated_parameter_ids()) for critical_set in self.critical_parameter_sets: if not estimated_parameters_ids.intersection(set(critical_set)): return False @@ -1303,6 +1286,9 @@ def jump_to_most_distant( # critical parameter from each critical parameter set if not self.check_critical(predecessor_model): for critical_set in self.critical_parameter_sets: + # FIXME is this a good idea? probably better to request + # the model from the model subspace, rather than editing + # the parameters... predecessor_model.parameters[critical_set[0]] = ESTIMATE # self.update_method(self.initial_method) @@ -1341,7 +1327,11 @@ def get_most_distant( most_distant_indices = [] # FIXME for multiple PEtab problems? - parameter_ids = self.best_models[0].petab_parameters + parameter_ids = list( + get_petab_parameters( + self.best_models[0]._model_subspace_petab_problem + ) + ) for model in self.best_models: model_estimated_parameters = np.array( @@ -1392,7 +1382,7 @@ def get_most_distant( ) most_distant_model = Model( - petab_yaml=model.petab_yaml, + model_subspace_petab_yaml=model.model_subspace_petab_yaml, model_subspace_id=model.model_subspace_id, model_subspace_indices=most_distant_indices, parameters=most_distant_parameters, @@ -1413,7 +1403,6 @@ class LateralCandidateSpace(CandidateSpace): def __init__( self, *args, - predecessor_model: Model | None, max_steps: int = None, **kwargs, ): @@ -1425,7 +1414,6 @@ def __init__( super().__init__( *args, method=Method.LATERAL, - predecessor_model=predecessor_model, **kwargs, ) self.max_steps = max_steps diff --git a/petab_select/cli.py b/petab_select/cli.py index 37f83551..d0def393 100644 --- a/petab_select/cli.py +++ b/petab_select/cli.py @@ -177,7 +177,7 @@ def start_iteration( excluded_model_hashes += f.read().split("\n") excluded_hashes = [ - excluded_model.get_hash() for excluded_model in excluded_models + excluded_model.hash for excluded_model in excluded_models ] excluded_hashes += [ ModelHash.from_hash(hash_str) for hash_str in excluded_model_hashes diff --git a/petab_select/constants.py b/petab_select/constants.py index 2946aeb5..c25f6cfa 100644 --- a/petab_select/constants.py +++ b/petab_select/constants.py @@ -8,46 +8,56 @@ from pathlib import Path from typing import Literal -# Zero-indexed column/row indices -MODEL_ID_COLUMN = 0 -PETAB_YAML_COLUMN = 1 -# It is assumed that all columns after PARAMETER_DEFINITIONS_START contain -# parameter IDs. -PARAMETER_DEFINITIONS_START = 2 -HEADER_ROW = 0 +# Checked -PARAMETER_VALUE_DELIMITER = ";" -CODE_DELIMITER = "-" -ESTIMATE = "estimate" -PETAB_ESTIMATE_FALSE = 0 -PETAB_ESTIMATE_TRUE = 1 +# Criteria +CRITERIA = "criteria" +CRITERION = "criterion" -# TYPING_PATH = Union[str, Path] -TYPE_PATH = str | Path -# Model space file columns -# TODO ensure none of these occur twice in the column header (this would -# suggest that a parameter has a conflicting name) -# MODEL_ID = 'modelId' # TODO already defined, reorganize constants -# YAML = 'YAML' # FIXME +class Criterion(str, Enum): + """String literals for model selection criteria.""" + + #: The Akaike information criterion. + AIC = "AIC" + #: The corrected Akaike information criterion. + AICC = "AICc" + #: The Bayesian information criterion. + BIC = "BIC" + #: The likelihood. + LH = "LH" + #: The log-likelihood. + LLH = "LLH" + #: The negative log-likelihood. + NLLH = "NLLH" + #: The sum of squared residuals. + SSR = "SSR" + + +# Model +ESTIMATED_PARAMETERS = "estimated_parameters" +ITERATION = "iteration" MODEL_ID = "model_id" MODEL_SUBSPACE_ID = "model_subspace_id" MODEL_SUBSPACE_INDICES = "model_subspace_indices" -MODEL_CODE = "model_code" +PARAMETERS = "parameters" +MODEL_SUBSPACE_PETAB_YAML = "model_subspace_petab_yaml" +MODEL_SUBSPACE_PETAB_PROBLEM = "_model_subspace_petab_problem" +PETAB_YAML = "petab_yaml" +ROOT_PATH = "root_path" +ESTIMATE = "estimate" + +PETAB_PROBLEM = "petab_problem" + +# Model hash MODEL_HASH = "model_hash" -MODEL_HASHES = "model_hashes" MODEL_HASH_DELIMITER = "-" +MODEL_SUBSPACE_INDICES_HASH = "model_subspace_indices_hash" MODEL_SUBSPACE_INDICES_HASH_DELIMITER = "." MODEL_SUBSPACE_INDICES_HASH_MAP = ( # [0-9]+[A-Z]+[a-z] string.digits + string.ascii_uppercase + string.ascii_lowercase ) -PETAB_HASH_DIGEST_SIZE = None -# If `predecessor_model_hash` is defined for a model, it is the ID of the model that the -# current model was/is to be compared to. This is part of the result and is -# only (optionally) set by the PEtab calibration tool. It is not defined by the -# PEtab Select model selection problem (but may be subsequently stored in the -# PEtab Select model report format. PREDECESSOR_MODEL_HASH = "predecessor_model_hash" ITERATION = "iteration" PETAB_PROBLEM = "petab_problem" @@ -57,61 +67,24 @@ # MODEL_SPACE_FILE_NON_PARAMETER_COLUMNS = [MODEL_ID, PETAB_YAML] MODEL_SPACE_FILE_NON_PARAMETER_COLUMNS = [MODEL_SUBSPACE_ID, PETAB_YAML] -# COMPARED_MODEL_ID = 'compared_'+MODEL_ID -YAML_FILENAME = "yaml" - -# DISTANCES = { -# FORWARD: { -# 'l1': 1, -# 'size': 1, -# }, -# BACKWARD: { -# 'l1': 1, -# 'size': -1, -# }, -# LATERAL: { -# 'l1': 2, -# 'size': 0, -# }, -# } - -CRITERIA = "criteria" - -PARAMETERS = "parameters" -# PARAMETER_ESTIMATE = 'parameter_estimate' -ESTIMATED_PARAMETERS = "estimated_parameters" +# PEtab +PETAB_ESTIMATE_TRUE = 1 -# Problem keys -CRITERION = "criterion" -METHOD = "method" -VERSION = "version" +# Problem MODEL_SPACE_FILES = "model_space_files" -PROBLEM_ID = "problem_id" PROBLEM = "problem" +PROBLEM_ID = "problem_id" +VERSION = "version" +# Candidate space CANDIDATE_SPACE = "candidate_space" CANDIDATE_SPACE_ARGUMENTS = "candidate_space_arguments" +METHOD = "method" METHOD_SCHEME = "method_scheme" -PREVIOUS_METHODS = "previous_methods" NEXT_METHOD = "next_method" +PREVIOUS_METHODS = "previous_methods" PREDECESSOR_MODEL = "predecessor_model" -MODEL = "model" -MODELS = "models" -UNCALIBRATED_MODELS = "uncalibrated_models" -TERMINATE = "terminate" - -# Parameters can be fixed to a value, or estimated if indicated with the string -# `ESTIMATE`. -TYPE_PARAMETER = float | int | Literal[ESTIMATE] -TYPE_PARAMETER_OPTIONS = list[TYPE_PARAMETER] -# Parameter ID -> parameter value mapping. -TYPE_PARAMETER_DICT = dict[str, TYPE_PARAMETER] -# Parameter ID -> multiple possible parameter values. -TYPE_PARAMETER_OPTIONS_DICT = dict[str, TYPE_PARAMETER_OPTIONS] - -TYPE_CRITERION = float - class Method(str, Enum): """String literals for model selection methods.""" @@ -130,24 +103,13 @@ class Method(str, Enum): MOST_DISTANT = "most_distant" -class Criterion(str, Enum): - """String literals for model selection criteria.""" - - #: The Akaike information criterion. - AIC = "AIC" - #: The corrected Akaike information criterion. - AICC = "AICc" - #: The Bayesian information criterion. - BIC = "BIC" - #: The likelihood. - LH = "LH" - #: The log-likelihood. - LLH = "LLH" - #: The negative log-likelihood. - NLLH = "NLLH" - #: The sum of squared residuals. - SSR = "SSR" +# Typing +TYPE_PATH = str | Path +# UI +MODELS = "models" +UNCALIBRATED_MODELS = "uncalibrated_models" +TERMINATE = "terminate" #: Methods that move through model space by taking steps away from some model. STEPWISE_METHODS = [ @@ -163,7 +125,8 @@ class Criterion(str, Enum): ] #: Virtual initial models can be used to initialize some initial model methods. -VIRTUAL_INITIAL_MODEL = "virtual_initial_model" +# FIXME replace by real "dummy" model object +# VIRTUAL_INITIAL_MODEL = "virtual_initial_model" #: Methods that are compatible with a virtual initial model. VIRTUAL_INITIAL_MODEL_METHODS = [ Method.BACKWARD, @@ -177,3 +140,69 @@ class Criterion(str, Enum): if not x.startswith("_") and x not in ("sys", "Enum", "Path", "Dict", "List", "Literal", "Union") ] + + +# Unchecked +MODEL = "model" + +# Zero-indexed column/row indices +MODEL_ID_COLUMN = 0 +PETAB_YAML_COLUMN = 1 +# It is assumed that all columns after PARAMETER_DEFINITIONS_START contain +# parameter IDs. +PARAMETER_DEFINITIONS_START = 2 +HEADER_ROW = 0 + +PARAMETER_VALUE_DELIMITER = ";" +CODE_DELIMITER = "-" +PETAB_ESTIMATE_FALSE = 0 + +# TYPING_PATH = Union[str, Path] + +# Model space file columns +# TODO ensure none of these occur twice in the column header (this would +# suggest that a parameter has a conflicting name) +# MODEL_ID = 'modelId' # TODO already defined, reorganize constants +# YAML = 'YAML' # FIXME +MODEL_CODE = "model_code" +MODEL_HASHES = "model_hashes" +PETAB_HASH_DIGEST_SIZE = None +# If `predecessor_model_hash` is defined for a model, it is the ID of the model that the +# current model was/is to be compared to. This is part of the result and is +# only (optionally) set by the PEtab calibration tool. It is not defined by the +# PEtab Select model selection problem (but may be subsequently stored in the +# PEtab Select model report format. +HASH = "hash" + +# MODEL_SPACE_FILE_NON_PARAMETER_COLUMNS = [MODEL_ID, PETAB_YAML] +MODEL_SPACE_FILE_NON_PARAMETER_COLUMNS = [MODEL_SUBSPACE_ID, PETAB_YAML] + +# COMPARED_MODEL_ID = 'compared_'+MODEL_ID +YAML_FILENAME = "yaml" + +# DISTANCES = { +# FORWARD: { +# 'l1': 1, +# 'size': 1, +# }, +# BACKWARD: { +# 'l1': 1, +# 'size': -1, +# }, +# LATERAL: { +# 'l1': 2, +# 'size': 0, +# }, +# } + + +# Parameters can be fixed to a value, or estimated if indicated with the string +# `ESTIMATE`. +TYPE_PARAMETER = float | int | Literal[ESTIMATE] +TYPE_PARAMETER_OPTIONS = list[TYPE_PARAMETER] +# Parameter ID -> parameter value mapping. +TYPE_PARAMETER_DICT = dict[str, TYPE_PARAMETER] +# Parameter ID -> multiple possible parameter values. +TYPE_PARAMETER_OPTIONS_DICT = dict[str, TYPE_PARAMETER_OPTIONS] + +TYPE_CRITERION = float diff --git a/petab_select/model.py b/petab_select/model.py index 81d73145..ae92df8a 100644 --- a/petab_select/model.py +++ b/petab_select/model.py @@ -2,17 +2,18 @@ from __future__ import annotations +import copy import warnings from os.path import relpath from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, ClassVar, Literal +import mkstd import petab.v1 as petab -import yaml -from more_itertools import one -from petab.v1.C import ESTIMATE, NOMINAL_VALUE +from petab.v1.C import NOMINAL_VALUE from .constants import ( + ESTIMATE, CRITERIA, ESTIMATED_PARAMETERS, ITERATION, @@ -21,470 +22,559 @@ MODEL_ID, MODEL_SUBSPACE_ID, MODEL_SUBSPACE_INDICES, + MODEL_SUBSPACE_INDICES_HASH, MODEL_SUBSPACE_INDICES_HASH_DELIMITER, MODEL_SUBSPACE_INDICES_HASH_MAP, - PARAMETERS, - PETAB_ESTIMATE_TRUE, + MODEL_SUBSPACE_PETAB_YAML, PETAB_PROBLEM, PETAB_YAML, - PREDECESSOR_MODEL_HASH, - TYPE_CRITERION, + ROOT_PATH, TYPE_PARAMETER, - TYPE_PATH, - VIRTUAL_INITIAL_MODEL, Criterion, ) from .criteria import CriterionComputer from .misc import ( parameter_string_to_value, ) -from .petab import PetabMixin +from .petab import get_petab_parameters if TYPE_CHECKING: from .problem import Problem + +from pydantic import ( + BaseModel, + PrivateAttr, + ValidationInfo, + ValidatorFunctionWrapHandler, +) + __all__ = [ "Model", "default_compare", "ModelHash", - "VIRTUAL_INITIAL_MODEL_HASH", + "VIRTUAL_INITIAL_MODEL", ] +from pydantic import ( + Field, + field_serializer, + field_validator, + model_serializer, + model_validator, +) -class Model(PetabMixin): - """A (possibly uncalibrated) model. - NB: some of these attribute names correspond to constants defined in the - `constants.py` file, to facilitate loading models from/saving models to - disk (see the `Model.saved_attributes` class attribute). +class ModelHash(BaseModel): + """The model hash. + + The model hash is designed to be human-readable and able to be converted + back into the corresponding model. Currently, if two models from two + different model subspaces are actually the same PEtab problem, they will + still have different model hashes. Attributes: - converters_load: - Functions to convert attributes from YAML to :class:`Model`. - converters_save: - Functions to convert attributes from :class:`Model` to YAML. - criteria: - The criteria values of the calibrated model (e.g. AIC). - iteration: - The iteration of the model selection algorithm where this model was - identified. - model_id: - The model ID. - petab_yaml: - The path to the PEtab problem YAML file. - parameters: - Parameter values that will overwrite the PEtab problem definition, - or change parameters to be estimated. - estimated_parameters: - Parameter estimates from a model calibration tool, for parameters - that are specified as estimated in the PEtab problem or PEtab - Select model YAML. These are untransformed values (i.e., not on - log scale). - saved_attributes: - Attributes that will be saved to disk by the :meth:`Model.to_yaml` - method. + model_subspace_id: + The ID of the model subspace of the model. Unique up to a single + PEtab Select problem model space. + model_subspace_indices_hash: + A hash of the location of the model in its model + subspace. Unique up to a single model subspace. """ - saved_attributes = ( - MODEL_ID, - MODEL_SUBSPACE_ID, - MODEL_SUBSPACE_INDICES, - MODEL_HASH, - PREDECESSOR_MODEL_HASH, - PETAB_YAML, - PARAMETERS, - ESTIMATED_PARAMETERS, - CRITERIA, - ITERATION, - ) - converters_load = { - MODEL_ID: lambda x: x, - MODEL_SUBSPACE_ID: lambda x: x, - MODEL_SUBSPACE_INDICES: lambda x: [] if not x else x, - MODEL_HASH: lambda x: x, - PREDECESSOR_MODEL_HASH: lambda x: x, - PETAB_YAML: lambda x: x, - PARAMETERS: lambda x: x, - ESTIMATED_PARAMETERS: lambda x: x, - CRITERIA: lambda x: { - # `criterion_id_value` is the ID of the criterion in the enum `Criterion`. - Criterion(criterion_id_value): float(criterion_value) - for criterion_id_value, criterion_value in x.items() - }, - ITERATION: lambda x: int(x) if x is not None else x, - } - converters_save = { - MODEL_ID: lambda x: str(x), - MODEL_SUBSPACE_ID: lambda x: str(x), - MODEL_SUBSPACE_INDICES: lambda x: [int(xi) for xi in x], - MODEL_HASH: lambda x: str(x), - PREDECESSOR_MODEL_HASH: lambda x: str(x) if x is not None else x, - PETAB_YAML: lambda x: str(x), - PARAMETERS: lambda x: {str(k): v for k, v in x.items()}, - # FIXME handle with a `set_estimated_parameters` method instead? - # to avoid `float` cast here. Reason for cast is because e.g. pyPESTO - # can provide type `np.float64`, which causes issues when writing to - # YAML. - # ESTIMATED_PARAMETERS: lambda x: x, - ESTIMATED_PARAMETERS: lambda x: { - str(id): float(value) for id, value in x.items() - }, - CRITERIA: lambda x: { - criterion_id.value: float(criterion_value) - for criterion_id, criterion_value in x.items() - }, - ITERATION: lambda x: int(x) if x is not None else None, - } + model_subspace_id: str + model_subspace_indices_hash: str - def __init__( - self, - petab_yaml: TYPE_PATH, - model_subspace_id: str = None, - model_id: str = None, - model_subspace_indices: list[int] = None, - predecessor_model_hash: str = None, - parameters: dict[str, int | float] = None, - estimated_parameters: dict[str, int | float] = None, - criteria: dict[str, float] = None, - iteration: int = None, - # Optionally provided to reduce repeated parsing of `petab_yaml`. - petab_problem: petab.Problem | None = None, - model_hash: Any | None = None, - ): - self.model_id = model_id - self.model_subspace_id = model_subspace_id - self.model_subspace_indices = model_subspace_indices - # TODO clean parameters, ensure single float or str (`ESTIMATE`) type - self.parameters = parameters - self.estimated_parameters = estimated_parameters - self.criteria = criteria - self.iteration = iteration + @model_validator(mode="wrap") + def _check_kwargs( + kwargs: dict[str, str | list[int]] | ModelHash, + handler: ValidatorFunctionWrapHandler, + info: ValidationInfo, + ) -> ModelHash: + """Handle `ModelHash` creation from different sources. + + See documentation of Pydantic wrap validators. + """ + if isinstance(kwargs, ModelHash): + return kwargs - self.predecessor_model_hash = predecessor_model_hash - if self.predecessor_model_hash is not None: - self.predecessor_model_hash = ModelHash.from_hash( - self.predecessor_model_hash + if isinstance(kwargs, dict): + kwargs[MODEL_SUBSPACE_INDICES_HASH] = ( + ModelHash.hash_model_subspace_indices( + kwargs[MODEL_SUBSPACE_INDICES] + ) ) + del kwargs[MODEL_SUBSPACE_INDICES] + + if isinstance(kwargs, str): + kwargs = ModelHash.kwargs_from_str(hash_str=kwargs) + + expected_model_hash = None + if MODEL_HASH in kwargs: + expected_model_hash = kwargs[MODEL_HASH] + if isinstance(expected_model_hash, str): + expected_model_hash = ModelHash.from_str(expected_model_hash) + del kwargs[MODEL_HASH] + + model_hash = handler(kwargs) + + if expected_model_hash is not None: + if model_hash != expected_model_hash: + warnings.warn( + "The provided model hash is inconsistent with its model " + "subspace and model subspace indices. Old hash: " + f"`{expected_model_hash}`. New hash: `{model_hash}`.", + stacklevel=2, + ) - if self.parameters is None: - self.parameters = {} - if self.estimated_parameters is None: - self.estimated_parameters = {} - if self.criteria is None: - self.criteria = {} + return model_hash - super().__init__(petab_yaml=petab_yaml, petab_problem=petab_problem) + @model_serializer() + def _serialize(self) -> str: + return str(self) - self.model_hash = None - self.get_hash() - if model_hash is not None: - model_hash = ModelHash.from_hash(model_hash) - if self.model_hash != model_hash: - raise ValueError( - "The supplied model hash does not match the computed " - "model hash." - ) + @staticmethod + def kwargs_from_str(hash_str: str) -> dict[str, str]: + """Convert a model hash string into constructor kwargs.""" + return dict( + zip( + [MODEL_SUBSPACE_ID, MODEL_SUBSPACE_INDICES_HASH], + hash_str.split(MODEL_HASH_DELIMITER), + strict=True, + ) + ) + + @staticmethod + def hash_model_subspace_indices(model_subspace_indices: list[int]) -> str: + """Hash the location of a model in its subspace. + + Args: + model_subspace_indices: + The location (indices) of the model in its subspace. + + Returns: + The hash. + """ + if not model_subspace_indices: + return "" + if max(model_subspace_indices) < len(MODEL_SUBSPACE_INDICES_HASH_MAP): + return "".join( + MODEL_SUBSPACE_INDICES_HASH_MAP[index] + for index in model_subspace_indices + ) + return MODEL_SUBSPACE_INDICES_HASH_DELIMITER.join( + str(i) for i in model_subspace_indices + ) + + def unhash_model_subspace_indices(self) -> list[int]: + """Get the location of a model in its subspace. + + Returns: + The location, as indices of the subspace. + """ + if ( + MODEL_SUBSPACE_INDICES_HASH_DELIMITER + not in self.model_subspace_indices_hash + ): + return [ + MODEL_SUBSPACE_INDICES_HASH_MAP.index(s) + for s in self.model_subspace_indices_hash + ] + return [ + int(s) + for s in self.model_subspace_indices_hash.split( + MODEL_SUBSPACE_INDICES_HASH_DELIMITER + ) + ] + + def get_model(self, problem: Problem) -> Model: + """Get the model that a hash corresponds to. + + Args: + problem: + The :class:`Problem` that will be used to look up the model. + + Returns: + The model. + """ + return problem.model_space.model_subspaces[ + self.model_subspace_id + ].indices_to_model(self.unhash_model_subspace_indices()) + + def __hash__(self) -> str: + """Not the model hash! Use `Model.hash` instead.""" + return hash(str(self)) + + def __eq__(self, other_hash: str | ModelHash) -> bool: + """Check whether two model hashes are equivalent.""" + return str(self) == str(other_hash) + + def __str__(self) -> str: + """Convert the hash to a string.""" + return MODEL_HASH_DELIMITER.join( + [self.model_subspace_id, self.model_subspace_indices_hash] + ) + + def __repr__(self) -> str: + """Convert the hash to a string representation.""" + return str(self) + + +class VirtualModelBase(BaseModel): + """Sufficient information for the virtual initial model.""" + + model_subspace_id: str + """The ID of the subspace that this model belongs to.""" + model_subspace_indices: list[int] + """The location of this model in its subspace.""" + criteria: dict[Criterion, float] = Field(default_factory=dict) + """The criterion values of the calibrated model (e.g. AIC).""" + model_hash: ModelHash = Field(default=None) + """The model hash (treat as read-only after initialization).""" + + @model_validator(mode="after") + def _check_hash(self: ModelBase) -> ModelBase: + """Validate the model hash.""" + kwargs = { + MODEL_SUBSPACE_ID: self.model_subspace_id, + MODEL_SUBSPACE_INDICES: self.model_subspace_indices, + } + if self.model_hash is not None: + kwargs[MODEL_HASH] = self.model_hash + self.model_hash = ModelHash.model_validate(kwargs) + + return self + + @field_validator("criteria", mode="after") + @classmethod + def _fix_criteria_typing( + cls, criteria: dict[str | Criterion, float] + ) -> dict[Criterion, float]: + """Fix criteria typing.""" + criteria = { + ( + criterion + if isinstance(criterion, Criterion) + else Criterion[criterion] + ): value + for criterion, value in criteria.items() + } + return criteria + + @field_serializer("criteria") + def _serialize_criteria( + self, criteria: dict[Criterion, float] + ) -> dict[str, float]: + """Serialize criteria.""" + criteria = { + criterion.value: value for criterion, value in criteria.items() + } + return criteria + + @property + def hash(self) -> ModelHash: + """Get the model hash.""" + return self.model_hash + + def __hash__(self) -> None: + """Use ``Model.hash`` instead.""" + raise NotImplementedError("Use `Model.hash` instead.") + + # def __eq__(self, other_model: Model | _VirtualInitialModel) -> bool: + # """Check whether two model hashes are equivalent.""" + # return self.hash == other.hash + + +class ModelBase(VirtualModelBase): + """Definition of the standardized model. + + :class:`Model` is extended with additional helper methods -- use that + instead of ``ModelBase``. + """ + + # TODO would use `FilePath` here (and remove `None` as an option), + # but then need to handle the + # `VIRTUAL_INITIAL_MODEL` dummy path differently. + model_subspace_petab_yaml: Path | None + """The location of the base PEtab problem for the model subspace. + + N.B.: Not the PEtab problem for this model specifically! + Use :meth:`Model.to_petab` to get the model-specific PEtab + problem. + """ + estimated_parameters: dict[str, float] | None = Field(default=None) + """The parameter estimates of the calibrated model (always unscaled).""" + iteration: int | None = Field(default=None) + """The iteration of model selection that calibrated this model.""" + model_id: str = Field(default=None) + """The model ID.""" + parameters: dict[str, float | int | Literal[ESTIMATE]] + """PEtab problem parameters overrides for this model. + + For example, fixes parameters to certain values, or sets them to be + estimated. + """ + predecessor_model_hash: ModelHash = Field(default=None) + """The predecessor model hash.""" + + PATH_ATTRIBUTES: ClassVar[list[str]] = [ + MODEL_SUBSPACE_PETAB_YAML, + ] + + @model_validator(mode="wrap") + def _fix_relative_paths( + data: dict[str, Any] | ModelBase, + handler: ValidatorFunctionWrapHandler, + info: ValidationInfo, + ) -> ModelBase: + if isinstance(data, ModelBase): + return data + model = handler(data) + + root_path = None + if ROOT_PATH in data: + root_path = data.pop(ROOT_PATH) + if root_path is None: + return model + + model.resolve_paths(root_path=root_path) + return model + + @model_validator(mode="after") + def _fix_id(self: ModelBase) -> ModelBase: + """Fix a missing ID by setting it to the hash.""" if self.model_id is None: - self.model_id = self.get_hash() + self.model_id = str(self.hash) + return self - self.criterion_computer = CriterionComputer(self) + @model_validator(mode="after") + def _fix_predecessor_model_hash(self: ModelBase) -> ModelBase: + """Fix missing predecessor model hashes. - def set_criterion(self, criterion: Criterion, value: float) -> None: - """Set a criterion value for the model. + Sets them to ``VIRTUAL_INITIAL_MODEL.hash``. + """ + if self.predecessor_model_hash is None: + self.predecessor_model_hash = VIRTUAL_INITIAL_MODEL.hash + self.predecessor_model_hash = ModelHash.model_validate( + self.predecessor_model_hash + ) + return self + + def to_yaml( + self, + yaml_path: str | Path, + ) -> None: + """Save a model to a YAML file. + + All paths will be made relative to the ``yaml_path`` directory. Args: - criterion: - The criterion (e.g. ``petab_select.constants.Criterion.AIC``). - value: - The criterion value for the (presumably calibrated) model. + yaml_path: + The model YAML file location. """ - if criterion in self.criteria: - warnings.warn( - "Overwriting saved criterion value. " - f"Criterion: {criterion}. Value: {self.get_criterion(criterion)}.", - stacklevel=2, + root_path = Path(yaml_path).parent + + model = copy.deepcopy(self) + model.set_relative_paths(root_path=root_path) + ModelStandard.save_data(data=model, filename=yaml_path) + + def set_relative_paths(self, root_path: str | Path) -> None: + """Change all paths to be relative to ``root_path``.""" + root_path = Path(root_path).resolve() + for path_attribute in self.PATH_ATTRIBUTES: + setattr( + self, + path_attribute, + relpath( + getattr(self, path_attribute).resolve(), + start=root_path, + ), ) - # FIXME debug why value is overwritten during test case 0002. - if False: - print( - "Overwriting saved criterion value. " - f"Criterion: {criterion}. Value: {self.get_criterion(criterion)}." - ) - breakpoint() - self.criteria[criterion] = value + + def resolve_paths(self, root_path: str | Path) -> None: + """Resolve all paths to be relative to ``root_path``.""" + root_path = Path(root_path).resolve() + for path_attribute in self.PATH_ATTRIBUTES: + setattr( + self, + path_attribute, + (root_path / getattr(self, path_attribute)).resolve(), + ) + + +class Model(ModelBase): + """A model. + + See :class:`ModelBase` for the standardized attributes. Additional + attributes are available in ``Model`` to improve usability. + + Attributes: + _model_subspace_petab_problem: + The PEtab problem of the model subspace of this model. + If not provided, this is reconstructed from + :attr:`model_subspace_petab_yaml`. + """ + + _model_subspace_petab_problem: petab.Problem = PrivateAttr(default=None) + + @model_validator(mode="after") + def _fix_petab_problem(self: Model) -> Model: + """Fix a missing PEtab problem by loading it from disk.""" + if ( + self._model_subspace_petab_problem is None + and self.model_subspace_petab_yaml is not None + ): + self._model_subspace_petab_problem = petab.Problem.from_yaml( + self.model_subspace_petab_yaml + ) + return self + + def model_post_init(self, __context: Any) -> None: + """Add additional instance attributes.""" + self._criterion_computer = CriterionComputer(self) def has_criterion(self, criterion: Criterion) -> bool: - """Check whether the model provides a value for a criterion. + """Check whether a value for a criterion has been set.""" + return self.criteria.get(criterion) is not None - Args: - criterion: - The criterion (e.g. `petab_select.constants.Criterion.AIC`). - """ - # TODO also `and self.criteria[id] is not None`? - return criterion in self.criteria + def set_criterion(self, criterion: Criterion, value: float) -> None: + """Set a criterion value.""" + if self.has_criterion(criterion=criterion): + warnings.warn( + f"Overwriting saved criterion value. Criterion: {criterion}. " + f"Value: `{self.get_criterion(criterion)}`.", + stacklevel=2, + ) + self.criteria[criterion] = float(value) def get_criterion( self, criterion: Criterion, compute: bool = True, raise_on_failure: bool = True, - ) -> TYPE_CRITERION | None: + ) -> float | None: """Get a criterion value for the model. Args: criterion: - The ID of the criterion (e.g. ``petab_select.constants.Criterion.AIC``). + The criterion. compute: - Whether to try to compute the criterion value based on other model - attributes. For example, if the ``'AIC'`` criterion is requested, this - can be computed from a predetermined model likelihood and its - number of estimated parameters. + Whether to attempt computing the criterion value. For example, + the AIC can be computed if the likelihood is available. raise_on_failure: - Whether to raise a `ValueError` if the criterion could not be - computed. If `False`, `None` is returned. + Whether to raise a ``ValueError`` if the criterion could not be + computed. If ``False``, ``None`` is returned. Returns: - The criterion value, or `None` if it is not available. - TODO check for previous use of this method before `.get` was used + The criterion value, or ``None`` if it is not available. """ - if criterion not in self.criteria and compute: + if not self.has_criterion(criterion=criterion) and compute: self.compute_criterion( criterion=criterion, raise_on_failure=raise_on_failure, ) - # value = self.criterion_computer(criterion=id) - # self.set_criterion(id=id, value=value) - return self.criteria.get(criterion, None) def compute_criterion( self, criterion: Criterion, raise_on_failure: bool = True, - ) -> TYPE_CRITERION: + ) -> float: """Compute a criterion value for the model. - The value will also be stored, which will overwrite any previously stored value - for the criterion. + The value will also be stored, which will overwrite any previously + stored value for the criterion. Args: criterion: - The ID of the criterion - (e.g. :obj:`petab_select.constants.Criterion.AIC`). + The criterion. raise_on_failure: - Whether to raise a `ValueError` if the criterion could not be - computed. If `False`, `None` is returned. + Whether to raise a ``ValueError`` if the criterion could not be + computed. If ``False``, ``None`` is returned. Returns: The criterion value. """ + criterion_value = None try: - criterion_value = self.criterion_computer(criterion) + criterion_value = self._criterion_computer(criterion) self.set_criterion(criterion, criterion_value) - result = criterion_value except ValueError as err: if raise_on_failure: raise ValueError( - f"Insufficient information to compute criterion `{criterion}`." + "Insufficient information to compute criterion " + f"`{criterion}`." ) from err - result = None - return result + return criterion_value def set_estimated_parameters( self, estimated_parameters: dict[str, float], scaled: bool = False, ) -> None: - """Set the estimated parameters. + """Set parameter estimates. Args: estimated_parameters: The estimated parameters. scaled: - Whether the ``estimated_parameters`` values are on the scale - defined in the PEtab problem (``True``), or untransformed - (``False``). + Whether the parameter estimates are on the scale defined in the + PEtab problem (``True``), or unscaled (``False``). """ if scaled: - estimated_parameters = self.petab_problem.unscale_parameters( - estimated_parameters - ) - self.estimated_parameters = estimated_parameters - - @staticmethod - def from_dict( - model_dict: dict[str, Any], - base_path: TYPE_PATH = None, - petab_problem: petab.Problem = None, - ) -> Model: - """Generate a model from a dictionary of attributes. - - Args: - model_dict: - A dictionary of attributes. The keys are attribute - names, the values are the corresponding attribute values for - the model. Required attributes are the required arguments of - the :meth:`Model.__init__` method. - base_path: - The path that any relative paths in the model are relative to - (e.g. the path to the PEtab problem YAML file - :meth:`Model.petab_yaml` may be relative). - petab_problem: - Optionally provide the PEtab problem, to avoid loading it multiple - times. - NB: This may cause issues if multiple models write to the same PEtab - problem in memory. - - Returns: - A model instance, initialized with the provided attributes. - """ - unknown_attributes = set(model_dict).difference(Model.converters_load) - if unknown_attributes: - warnings.warn( - "Ignoring unknown attributes: " - + ", ".join(unknown_attributes), - stacklevel=2, - ) - - if base_path is not None: - model_dict[PETAB_YAML] = base_path / model_dict[PETAB_YAML] - - model_dict = { - attribute: Model.converters_load[attribute](value) - for attribute, value in model_dict.items() - if attribute in Model.converters_load - } - model_dict[PETAB_PROBLEM] = petab_problem - return Model(**model_dict) - - @staticmethod - def from_yaml(model_yaml: TYPE_PATH) -> Model: - """Generate a model from a PEtab Select model YAML file. - - Args: - model_yaml: - The path to the PEtab Select model YAML file. - - Returns: - A model instance, initialized with the provided attributes. - """ - with open(str(model_yaml)) as f: - model_dict = yaml.safe_load(f) - # TODO check that the hash is reproducible - if isinstance(model_dict, list): - try: - model_dict = one(model_dict) - except ValueError: - if len(model_dict) <= 1: - raise - raise ValueError( - "The provided YAML file contains a list with greater than " - "one element. Use the `Models.from_yaml` or provide a " - "YAML file with only one model specified." + estimated_parameters = ( + self._model_subspace_petab_problem.unscale_parameters( + estimated_parameters ) - - return Model.from_dict(model_dict, base_path=Path(model_yaml).parent) - - def to_dict( - self, - resolve_paths: bool = True, - paths_relative_to: str | Path = None, - ) -> dict[str, Any]: - """Generate a dictionary from the attributes of a :class:`Model` instance. - - Args: - resolve_paths: - Whether to resolve relative paths into absolute paths. - paths_relative_to: - If not ``None``, paths will be converted to be relative to this path. - Takes priority over ``resolve_paths``. - - Returns: - A dictionary of attributes. The keys are attribute - names, the values are the corresponding attribute values for - the model. Required attributes are the required arguments of - the :meth:`Model.__init__` method. - """ - model_dict = {} - for attribute in self.saved_attributes: - model_dict[attribute] = self.converters_save[attribute]( - getattr(self, attribute) ) - # TODO test - if resolve_paths: - if model_dict[PETAB_YAML]: - model_dict[PETAB_YAML] = str( - Path(model_dict[PETAB_YAML]).resolve() - ) - if paths_relative_to is not None: - if model_dict[PETAB_YAML]: - model_dict[PETAB_YAML] = relpath( - Path(model_dict[PETAB_YAML]).resolve(), - Path(paths_relative_to).resolve(), - ) - return model_dict - - def to_yaml(self, petab_yaml: TYPE_PATH, *args, **kwargs) -> None: - """Generate a PEtab Select model YAML file from a :class:`Model` instance. - - Parameters: - petab_yaml: - The location where the PEtab Select model YAML file will be - saved. - args, kwargs: - Additional arguments are passed to ``self.to_dict``. - """ - # FIXME change `getattr(self, PETAB_YAML)` to be relative to - # destination? - # kind of fixed, as the path will be resolved in `to_dict`. - with open(petab_yaml, "w") as f: - yaml.dump(self.to_dict(*args, **kwargs), f) - # yaml.dump(self.to_dict(), str(petab_yaml)) + self.estimated_parameters = estimated_parameters def to_petab( self, - output_path: TYPE_PATH = None, + output_path: str | Path = None, set_estimated_parameters: bool | None = None, - ) -> dict[str, petab.Problem | TYPE_PATH]: - """Generate a PEtab problem. + ) -> dict[str, petab.Problem | str | Path]: + """Generate the PEtab problem for this model. Args: output_path: - The directory where PEtab files will be written to disk. If not - specified, the PEtab files will not be written to disk. + If specified, the PEtab tables will be written to disk, inside + this directory. set_estimated_parameters: - Whether to set the nominal value of estimated parameters to their - estimates. If parameter estimates are available, this - will default to `True`. + Whether to implement ``Model.estimated_parameters`` as the + nominal values of the PEtab problem parameter table. + Defaults to ``True`` if ``Model.estimated_parameters`` is set. Returns: - A 2-tuple. The first value is a PEtab problem that can be used - with a PEtab-compatible tool for calibration of this model. If - ``output_path`` is not ``None``, the second value is the path to a - PEtab YAML file that can be used to load the PEtab problem (the - first value) into any PEtab-compatible tool. + The PEtab problem. Also returns the path of the PEtab problem YAML + file, if ``output_path`` is specified. """ - # TODO could use `copy.deepcopy(self.petab_problem)` from PetabMixin? - petab_problem = petab.Problem.from_yaml(str(self.petab_yaml)) + petab_problem = petab.Problem.from_yaml(self.model_subspace_petab_yaml) if set_estimated_parameters is None and self.estimated_parameters: set_estimated_parameters = True + if set_estimated_parameters: + required_estimates = { + parameter_id + for parameter_id, value in self.parameters.items() + if value == ESTIMATE + } + missing_estimates = required_estimates.difference( + self.estimated_parameters + ) + if missing_estimates: + raise ValueError( + "Try again with `set_estimated_parameters=False`, because " + "some parameter estimates are missing. Missing estimates for: " + f"`{missing_estimates}`." + ) + for parameter_id, parameter_value in self.parameters.items(): # If the parameter is to be estimated. if parameter_value == ESTIMATE: petab_problem.parameter_df.loc[parameter_id, ESTIMATE] = 1 - if set_estimated_parameters: - if parameter_id not in self.estimated_parameters: - raise ValueError( - "Not all estimated parameters are available " - "in `model.estimated_parameters`. Hence, the " - "estimated parameter vector cannot be set as " - "the nominal value in the PEtab problem. " - "Try calling this method with " - "`set_estimated_parameters=False`." - ) petab_problem.parameter_df.loc[ parameter_id, NOMINAL_VALUE ] = self.estimated_parameters[parameter_id] @@ -494,7 +584,6 @@ def to_petab( petab_problem.parameter_df.loc[parameter_id, NOMINAL_VALUE] = ( parameter_string_to_value(parameter_value) ) - # parameter_value petab_yaml = None if output_path is not None: @@ -509,94 +598,43 @@ def to_petab( PETAB_YAML: petab_yaml, } - def get_hash(self) -> str: - """Get the model hash. - - See the documentation for :class:`ModelHash` for more information. - - This is not implemented as ``__hash__`` because Python automatically - truncates values in a system-dependent manner, which reduces - interoperability - ( https://docs.python.org/3/reference/datamodel.html#object.__hash__ ). - - Returns: - The hash. - """ - if self.model_hash is None: - self.model_hash = ModelHash.from_model(model=self) - return self.model_hash - - def __hash__(self) -> None: - """Use `Model.get_hash` instead.""" - raise NotImplementedError("Use `Model.get_hash() instead.`") - - def __str__(self): - """Get a print-ready string representation of the model. - - Returns: - The print-ready string representation, in TSV format. - """ + def __str__(self) -> str: + """Printable model summary.""" parameter_ids = "\t".join(self.parameters.keys()) parameter_values = "\t".join(str(v) for v in self.parameters.values()) - header = "\t".join([MODEL_ID, PETAB_YAML, parameter_ids]) + header = "\t".join( + [MODEL_ID, MODEL_SUBSPACE_PETAB_YAML, parameter_ids] + ) data = "\t".join( - [self.model_id, str(self.petab_yaml), parameter_values] + [ + self.model_id, + str(self.model_subspace_petab_yaml), + parameter_values, + ] ) - # header = f'{MODEL_ID}\t{PETAB_YAML}\t{parameter_ids}' - # data = f'{self.model_id}\t{self.petab_yaml}\t{parameter_values}' return f"{header}\n{data}" def __repr__(self) -> str: - """The model hash.""" - return f'' + """The model hash. - def get_mle(self) -> dict[str, float]: - """Get the maximum likelihood estimate of the model.""" - """ - FIXME(dilpath) - # Check if original PEtab problem or PEtab Select model has estimated - # parameters. e.g. can use some of `self.to_petab` to get the parameter - # df and see if any are estimated. - if not self.has_estimated_parameters: - warn('The MLE for this model contains no estimated parameters.') - if not all([ - parameter_id in getattr(self, ESTIMATED_PARAMETERS) - for parameter_id in self.get_estimated_parameter_ids_all() - ]): - warn('Not all estimated parameters have estimates stored.') - petab_problem = petab.Problem.from_yaml(str(self.petab_yaml)) - return { - parameter_id: ( - getattr(self, ESTIMATED_PARAMETERS).get( - # Return estimated parameter from `petab_select.Model` - # if possible. - parameter_id, - # Else return nominal value from PEtab parameter table. - petab_problem.parameter_df.loc[ - parameter_id, NOMINAL_VALUE - ], - ) - ) - for parameter_id in petab_problem.parameter_df.index - } - # TODO rewrite to construct return dict in a for loop, for more - # informative error message as soon as a "should-be-estimated" - # parameter has not estimate available in `self.estimated_parameters`. + The hash can be used to reconstruct the model (see + :meth:``ModelHash.get_model``). """ - # TODO - pass + return f'' - def get_estimated_parameter_ids_all(self) -> list[str]: - estimated_parameter_ids = [] + def get_estimated_parameter_ids(self, full: bool = True) -> list[str]: + """Get estimated parameter IDs. - # Add all estimated parameters in the PEtab problem. - petab_problem = petab.Problem.from_yaml(str(self.petab_yaml)) - for parameter_id in petab_problem.parameter_df.index: - if ( - petab_problem.parameter_df.loc[parameter_id, ESTIMATE] - == PETAB_ESTIMATE_TRUE - ): - estimated_parameter_ids.append(parameter_id) + Args: + full: + Whether to provide all IDs, including additional parameters + that are not part of the model selection problem but estimated. + """ + estimated_parameter_ids = [] + if full: + estimated_parameter_ids = ( + self._model_subspace_petab_problem.x_free_ids + ) # Add additional estimated parameters, and collect fixed parameters, # in this model's parameterization. @@ -616,7 +654,6 @@ def get_estimated_parameter_ids_all(self) -> list[str]: for parameter_id in estimated_parameter_ids if parameter_id not in fixed_parameter_ids ] - return estimated_parameter_ids def get_parameter_values( @@ -627,28 +664,41 @@ def get_parameter_values( Includes ``ESTIMATE`` for parameters that should be estimated. - The ordering is by ``parameter_ids`` if supplied, else - ``self.petab_parameters``. - Args: parameter_ids: The IDs of parameters that values will be returned for. Order - is maintained. + is maintained. Defaults to the model subspace PEtab problem + parameters (including those not part of the model selection + problem). Returns: The values of parameters. """ + nominal_values = get_petab_parameters( + self._model_subspace_petab_problem + ) if parameter_ids is None: - parameter_ids = list(self.petab_parameters) + parameter_ids = list(nominal_values) return [ - self.parameters.get( - parameter_id, - # Default to PEtab problem. - self.petab_parameters[parameter_id], - ) + self.parameters.get(parameter_id, nominal_values[parameter_id]) for parameter_id in parameter_ids ] + @staticmethod + def from_yaml( + yaml_path: str | Path, + ) -> Model: + """Load a model from a YAML file. + + Args: + yaml_path: + The model YAML file location. + """ + model = ModelStandard.load_data( + filename=yaml_path, root_path=yaml_path.parent + ) + return model + def default_compare( model0: Model, @@ -669,12 +719,12 @@ def default_compare( criterion: The criterion. criterion_threshold: - The value by which the new model must improve on the original - model. Should be non-negative, regardless of the criterion. + The non-negative value by which the new model must improve on the + original model. Returns: - ``True` if ``model1`` has a better criterion value than ``model0``, else - ``False``. + ``True` if ``model1`` has a better criterion value than ``model0``, + else ``False``. """ if not model1.has_criterion(criterion): warnings.warn( @@ -683,7 +733,7 @@ def default_compare( stacklevel=2, ) return False - if model0 == VIRTUAL_INITIAL_MODEL or model0 is None: + if model0.hash == VIRTUAL_INITIAL_MODEL_HASH or model0 is None: return True if criterion_threshold < 0: warnings.warn( @@ -715,282 +765,14 @@ def default_compare( raise NotImplementedError(f"Unknown criterion: {criterion}.") -class ModelHash(str): - """A class to handle model hash functionality. - - The model hash is designed to be human-readable and able to be converted - back into the corresponding model. Currently, if two models from two - different model subspaces are actually the same PEtab problem, they will - still have different model hashes. - - Attributes: - model_subspace_id: - The ID of the model subspace of the model. Unique up to a single - PEtab Select problem model space. - model_subspace_indices_hash: - A hash of the location of the model in its model - subspace. Unique up to a single model subspace. - """ - - # FIXME petab problem--specific hashes that are cross-platform? - """ - The model hash is designed to be: human-readable; able to be converted - back into the corresponding model, and unique up to the same PEtab - problem and parameters. - - Consider two different models in different model subspaces, with - `ModelHash`s `model_hash0` and `model_hash1`, respectively. Assume that - these two models end up encoding the same PEtab problem (e.g. they set the - same parameters to be estimated). - The string representation will be different, - `str(model_hash0) != str(model_hash1)`, but their hashes will pass the - equality check: `model_hash0 == model_hash1` and - `hash(model_hash0) == hash(model_hash1)`. - - This means that different models in different model subspaces that end up - being the same PEtab problem will have different human-readable hashes, - but if these models arise during model selection, then only one of them - will be calibrated. - - The PEtab hash size is computed automatically as the smallest size that - ensures a collision probability of less than $2^{-64}$. - N.B.: this assumes only one model subspace, and only 2 options for each - parameter (e.g. `0` and `estimate`). You can manually set the size with - :const:`petab_select.constants.PETAB_HASH_DIGEST_SIZE`. - - petab_hash: - A hash that is unique up to the same PEtab problem, which is - determined by: the PEtab problem YAML file location, nominal - parameter values, and parameters set to be estimated. This means - that different models may have the same `unique_petab_hash`, - because they are the same estimation problem. - """ - - def __init__( - self, - model_subspace_id: str, - model_subspace_indices_hash: str, - # petab_hash: str, - ): - self.model_subspace_id = model_subspace_id - self.model_subspace_indices_hash = model_subspace_indices_hash - # self.petab_hash = petab_hash - - def __new__( - cls, - model_subspace_id: str, - model_subspace_indices_hash: str, - # petab_hash: str, - ): - hash_str = MODEL_HASH_DELIMITER.join( - [ - model_subspace_id, - model_subspace_indices_hash, - # petab_hash, - ] - ) - instance = super().__new__(cls, hash_str) - return instance - - def __getnewargs_ex__(self): - return ( - (), - { - "model_subspace_id": self.model_subspace_id, - "model_subspace_indices_hash": self.model_subspace_indices_hash, - # 'petab_hash': self.petab_hash, - }, - ) - - def __copy__(self): - return ModelHash( - model_subspace_id=self.model_subspace_id, - model_subspace_indices_hash=self.model_subspace_indices_hash, - # petab_hash=self.petab_hash, - ) - - def __deepcopy__(self, memo): - return self.__copy__() - - # @staticmethod - # def get_petab_hash(model: Model) -> str: - # """Get a hash that is unique up to the same estimation problem. - - # See :attr:`petab_hash` for more information. - - # Args: - # model: - # The model. - - # Returns: - # The unique PEtab hash. - # """ - # digest_size = PETAB_HASH_DIGEST_SIZE - # if digest_size is None: - # petab_info_bits = len(model.model_subspace_indices) - # # Ensure <2^{-64} probability of collision - # petab_info_bits += 64 - # # Convert to bytes, round up. - # digest_size = int(petab_info_bits / 8) + 1 - - # petab_yaml = str(model.petab_yaml.resolve()) - # model_parameter_df = model.to_petab(set_estimated_parameters=False)[ - # PETAB_PROBLEM - # ].parameter_df - # nominal_parameter_hash = hash_parameter_dict( - # model_parameter_df[NOMINAL_VALUE].to_dict() - # ) - # estimate_parameter_hash = hash_parameter_dict( - # model_parameter_df[ESTIMATE].to_dict() - # ) - # return hash_str( - # petab_yaml + estimate_parameter_hash + nominal_parameter_hash, - # digest_size=digest_size, - # ) - - @staticmethod - def from_hash(model_hash: str | ModelHash) -> ModelHash: - """Reconstruct a :class:`ModelHash` object. - - Args: - model_hash: - The model hash. - - Returns: - The :class:`ModelHash` object. - """ - if isinstance(model_hash, ModelHash): - return model_hash - - if model_hash == VIRTUAL_INITIAL_MODEL: - return ModelHash( - model_subspace_id=VIRTUAL_INITIAL_MODEL, - model_subspace_indices_hash="", - # petab_hash=VIRTUAL_INITIAL_MODEL, - ) - - ( - model_subspace_id, - model_subspace_indices_hash, - # petab_hash, - ) = model_hash.split(MODEL_HASH_DELIMITER) - return ModelHash( - model_subspace_id=model_subspace_id, - model_subspace_indices_hash=model_subspace_indices_hash, - # petab_hash=petab_hash, - ) - - @staticmethod - def from_model(model: Model) -> ModelHash: - """Create a hash for a model. - - Args: - model: - The model. - - Returns: - The model hash. - """ - model_subspace_id = "" - model_subspace_indices_hash = "" - if model.model_subspace_id is not None: - model_subspace_id = model.model_subspace_id - model_subspace_indices_hash = ( - ModelHash.hash_model_subspace_indices( - model.model_subspace_indices - ) - ) - - return ModelHash( - model_subspace_id=model_subspace_id, - model_subspace_indices_hash=model_subspace_indices_hash, - # petab_hash=ModelHash.get_petab_hash(model=model), - ) - - @staticmethod - def hash_model_subspace_indices(model_subspace_indices: list[int]) -> str: - """Hash the location of a model in its subspace. - - Args: - model_subspace_indices: - The location (indices) of the model in its subspace. - - Returns: - The hash. - """ - try: - return "".join( - MODEL_SUBSPACE_INDICES_HASH_MAP[index] - for index in model_subspace_indices - ) - except KeyError: - return MODEL_SUBSPACE_INDICES_HASH_DELIMITER.join( - str(i) for i in model_subspace_indices - ) - - def unhash_model_subspace_indices(self) -> list[int]: - """Get the location of a model in its subspace. - - Returns: - The location, as indices of the subspace. - """ - if ( - MODEL_SUBSPACE_INDICES_HASH_DELIMITER - in self.model_subspace_indices_hash - ): - return [ - int(s) - for s in self.model_subspace_indices_hash.split( - MODEL_SUBSPACE_INDICES_HASH_DELIMITER - ) - ] - else: - return [ - MODEL_SUBSPACE_INDICES_HASH_MAP.index(s) - for s in self.model_subspace_indices_hash - ] - - def get_model(self, petab_select_problem: Problem) -> Model: - """Get the model that a hash corresponds to. - - Args: - petab_select_problem: - The PEtab Select problem. The model will be found in its model - space. - - Returns: - The model. - """ - # if self.petab_hash == VIRTUAL_INITIAL_MODEL: - # return self.petab_hash - - return petab_select_problem.model_space.model_subspaces[ - self.model_subspace_id - ].indices_to_model(self.unhash_model_subspace_indices()) - - def __hash__(self) -> str: - """The PEtab hash. - - N.B.: this is not the model hash! As the equality between two models - is determined by their PEtab hash only, this method only returns the - PEtab hash. However, the model hash is the full string with the - human-readable elements as well. :func:`ModelHash.from_hash` does not - accept the PEtab hash as input, rather the full string. - """ - return hash(str(self)) - - def __eq__(self, other_hash: str | ModelHash) -> bool: - """Check whether two model hashes are equivalent. - - Returns: - Whether the two hashes correspond to equivalent PEtab problems. - """ - # petab_hash = other_hash - # # Check whether the PEtab hash needs to be extracted - # if MODEL_HASH_DELIMITER in other_hash: - # petab_hash = ModelHash.from_hash(other_hash).petab_hash - # return self.petab_hash == petab_hash - return str(self) == str(other_hash) +VIRTUAL_INITIAL_MODEL = VirtualModelBase.model_validate( + { + "model_subspace_id": "virtual_initial_model", + "model_subspace_indices": [], + } +) +# TODO deprecate, use `VIRTUAL_INITIAL_MODEL.hash` instead +VIRTUAL_INITIAL_MODEL_HASH = VIRTUAL_INITIAL_MODEL.hash -VIRTUAL_INITIAL_MODEL_HASH = ModelHash.from_hash(VIRTUAL_INITIAL_MODEL) +ModelStandard = mkstd.YamlStandard(model=Model) diff --git a/petab_select/model_subspace.py b/petab_select/model_subspace.py index 1f077996..1f62bd75 100644 --- a/petab_select/model_subspace.py +++ b/petab_select/model_subspace.py @@ -7,6 +7,7 @@ import numpy as np import pandas as pd +import petab.v1 as petab from more_itertools import powerset from .candidate_space import CandidateSpace @@ -20,19 +21,18 @@ TYPE_PARAMETER_OPTIONS, TYPE_PARAMETER_OPTIONS_DICT, TYPE_PATH, - VIRTUAL_INITIAL_MODEL, Method, ) from .misc import parameter_string_to_value -from .model import Model -from .petab import PetabMixin +from .model import VIRTUAL_INITIAL_MODEL, Model +from .petab import get_petab_parameters __all__ = [ "ModelSubspace", ] -class ModelSubspace(PetabMixin): +class ModelSubspace: """Efficient representation of exponentially large model subspaces. Attributes: @@ -42,37 +42,38 @@ class ModelSubspace(PetabMixin): The location of the PEtab problem YAML file. parameters: The key is the ID of the parameter. The value is a list of values - that the parameter can take (including `ESTIMATE`). + that the parameter can take (including ``ESTIMATE``). exclusions: Hashes of models that have been previously submitted to a candidate space for consideration (:meth:`CandidateSpace.consider`). """ - """ - FIXME(dilpath) - #history: - # A history of all models that have been accepted by the candidate - # space. Models are represented as indices (see e.g. - # `ModelSubspace.parameters_to_indices`). - """ - def __init__( self, model_subspace_id: str, - petab_yaml: str, + petab_yaml: str | Path, parameters: TYPE_PARAMETER_OPTIONS_DICT, exclusions: list[Any] | None | None = None, ): self.model_subspace_id = model_subspace_id + self.petab_yaml = Path(petab_yaml) self.parameters = parameters - # TODO switch from mixin to attribute - super().__init__(petab_yaml=petab_yaml, parameters_as_lists=True) - self.exclusions = set() if exclusions is not None: self.exclusions = set(exclusions) + self.petab_problem = petab.Problem.from_yaml(self.petab_yaml) + + for parameter_id, parameter_value in self.parameters.items(): + if not parameter_value: + raise ValueError( + f"The parameter `{parameter_id}` is in the definition " + "of this model subspace. However, its value is empty. " + f"Please specify either its fixed value or `'{ESTIMATE}'` " + "(e.g. in the model space table)." + ) + def check_compatibility_stepwise_method( self, candidate_space: CandidateSpace, @@ -91,9 +92,15 @@ def check_compatibility_stepwise_method( """ if candidate_space.method not in STEPWISE_METHODS: return True - if candidate_space.predecessor_model != VIRTUAL_INITIAL_MODEL and ( - str(candidate_space.predecessor_model.petab_yaml.resolve()) - != str(self.petab_yaml.resolve()) + if ( + candidate_space.predecessor_model.hash + != VIRTUAL_INITIAL_MODEL.hash + and ( + str( + candidate_space.predecessor_model.model_subspace_petab_yaml.resolve() + ) + != str(self.petab_yaml.resolve()) + ) ): warnings.warn( "The supplied candidate space is initialized with a model " @@ -101,10 +108,9 @@ def check_compatibility_stepwise_method( "This is currently not supported for stepwise methods " "(e.g. forward or backward). " f"This model subspace: `{self.model_subspace_id}`. " - "This model subspace PEtab YAML: " - f"`{self.petab_yaml}`. " + f"This model subspace PEtab YAML: `{self.petab_yaml}`. " "The candidate space PEtab YAML: " - f"`{candidate_space.predecessor_model.petab_yaml}`.", + f"`{candidate_space.predecessor_model.model_subspace_petab_yaml}`.", stacklevel=2, ) return False @@ -238,29 +244,37 @@ def continue_searching( # Compute parameter sets that are useful for finding minimal forward or backward # moves in the subspace. # Parameters that are currently estimated in the predecessor model. - if candidate_space.predecessor_model == VIRTUAL_INITIAL_MODEL: + if ( + candidate_space.predecessor_model.hash + == VIRTUAL_INITIAL_MODEL.hash + ): if candidate_space.method == Method.FORWARD: - old_estimated_all = set() - old_fixed_all = set(self.parameters) + old_estimated_all = self.must_estimate_all + old_fixed_all = self.can_fix_all elif candidate_space.method == Method.BACKWARD: - old_estimated_all = set(self.parameters) - old_fixed_all = set() + old_estimated_all = self.can_estimate_all + old_fixed_all = self.must_fix_all + elif candidate_space.method == Method.BRUTE_FORCE: + # doesn't matter what these are set to + old_estimated_all = self.must_estimate_all + old_fixed_all = self.must_fix_all else: # Should already be handled elsewhere (e.g. # `self.check_compatibility_stepwise_method`). raise NotImplementedError( - f"The default parameter set for a candidate space with the virtual initial model and method {candidate_space.method} is not implemented. Please report if this is desired." + "The virtual initial model and method " + f"{candidate_space.method} is not implemented. " + "Please report at https://github.com/PEtab-dev/petab_select/issues if this is desired." ) else: - old_estimated_all = set() - old_fixed_all = set() - if isinstance(candidate_space.predecessor_model, Model): - old_estimated_all = candidate_space.predecessor_model.get_estimated_parameter_ids_all() - old_fixed_all = [ - parameter_id - for parameter_id in self.parameters_all - if parameter_id not in old_estimated_all - ] + old_estimated_all = ( + candidate_space.predecessor_model.get_estimated_parameter_ids() + ) + old_fixed_all = [ + parameter_id + for parameter_id in self.parameters_all + if parameter_id not in old_estimated_all + ] # Parameters that are fixed in the candidate space # predecessor model but are necessarily estimated in this subspace. @@ -307,7 +321,8 @@ def continue_searching( # there are no valid "forward" moves. if ( not new_can_estimate_all - and candidate_space.predecessor_model != VIRTUAL_INITIAL_MODEL + and candidate_space.predecessor_model.hash + != VIRTUAL_INITIAL_MODEL.hash ): return # There are estimated parameters in the predecessor model that @@ -318,7 +333,8 @@ def continue_searching( # parameters. if ( new_must_estimate_all - or candidate_space.predecessor_model == VIRTUAL_INITIAL_MODEL + or candidate_space.predecessor_model.hash + == VIRTUAL_INITIAL_MODEL.hash ): # Consider minimal models that have all necessarily-estimated # parameters. @@ -397,7 +413,8 @@ def continue_searching( # are no valid "backward" moves. if ( not new_can_fix_all - and candidate_space.predecessor_model != VIRTUAL_INITIAL_MODEL + and candidate_space.predecessor_model.hash + != VIRTUAL_INITIAL_MODEL.hash ): return # There are fixed parameters in the predecessor model that must be estimated @@ -408,7 +425,8 @@ def continue_searching( # parameters. if ( new_must_fix_all - or candidate_space.predecessor_model == VIRTUAL_INITIAL_MODEL + or candidate_space.predecessor_model.hash + == VIRTUAL_INITIAL_MODEL.hash ): # Consider minimal models that have all necessarily-fixed # parameters. @@ -508,7 +526,8 @@ def continue_searching( if ( # `and` is redundant with the "equal number" check above. (new_must_estimate_all and new_must_fix_all) - or candidate_space.predecessor_model == VIRTUAL_INITIAL_MODEL + or candidate_space.predecessor_model.hash + == VIRTUAL_INITIAL_MODEL.hash ): # Consider all models that have the required estimated and # fixed parameters. @@ -654,7 +673,7 @@ def exclude_model(self, model: Model) -> None: model: The model that will be excluded. """ - self.exclude_model_hash(model_hash=model.get_hash()) + self.exclude_model_hash(model_hash=model.hash) def exclude_models(self, models: Iterable[Model]) -> None: """Exclude models from the model subspace. @@ -674,7 +693,7 @@ def excluded( model: Model, ) -> bool: """Whether a model is excluded.""" - return model.get_hash() in self.exclusions + return model.hash in self.exclusions def reset_exclusions( self, @@ -744,11 +763,11 @@ def indices_to_model(self, indices: list[int]) -> Model | None: ``None``, if the model is excluded from the subspace. """ model = Model( - petab_yaml=self.petab_yaml, model_subspace_id=self.model_subspace_id, model_subspace_indices=indices, + model_subspace_petab_yaml=self.petab_yaml, parameters=self.indices_to_parameters(indices), - petab_problem=self.petab_problem, + _model_subspace_petab_problem=self.petab_problem, ) if self.excluded(model): return None @@ -828,7 +847,10 @@ def parameters_all(self) -> TYPE_PARAMETER_DICT: Parameter values in the PEtab problem are overwritten by the model subspace values. """ - return {**self.petab_parameters, **self.parameters} + return { + **get_petab_parameters(self.petab_problem, as_lists=True), + **self.parameters, + } @property def can_fix(self) -> list[str]: @@ -840,10 +862,15 @@ def can_fix(self) -> list[str]: return [ parameter_id for parameter_id, parameter_values in self.parameters.items() - # If the possible parameter values are not only `ESTIMATE`, then - # it is assumed there is a fixed possible parameter value. - # TODO explicitly check for a lack of `ValueError` when cast to - # float? + if parameter_values != [ESTIMATE] + ] + + @property + def can_fix_all(self) -> list[str]: + """All arameters that can be fixed, according to the subspace.""" + return [ + parameter_id + for parameter_id, parameter_values in self.parameters_all.items() if parameter_values != [ESTIMATE] ] @@ -909,7 +936,7 @@ def must_estimate_all(self) -> list[str]: """All parameters that must be estimated in this subspace.""" must_estimate_petab = [ parameter_id - for parameter_id in self.petab_parameter_ids_estimated + for parameter_id in self.petab_problem.x_free_ids if parameter_id not in self.parameters ] return [*must_estimate_petab, *self.must_estimate] diff --git a/petab_select/models.py b/petab_select/models.py index 03996adb..6e770d35 100644 --- a/petab_select/models.py +++ b/petab_select/models.py @@ -16,13 +16,16 @@ ITERATION, MODEL_HASH, MODEL_ID, + MODEL_SUBSPACE_PETAB_PROBLEM, PREDECESSOR_MODEL_HASH, + ROOT_PATH, TYPE_PATH, Criterion, ) from .model import ( Model, ModelHash, + VirtualModelBase, ) if TYPE_CHECKING: @@ -107,6 +110,8 @@ def __contains__(self, item: ModelLike) -> bool: return item in self._models case ModelHash() | str(): return item in self._hashes + case VirtualModelBase(): + return False case _: raise TypeError(f"Unexpected type: `{type(item)}`.") @@ -176,7 +181,7 @@ def __setitem__(self, key: ModelIndex, item: ModelLike) -> None: if key < len(self): self._models[key] = item - self._hashes[key] = item.get_hash() + self._hashes[key] = item.hash else: # Key doesn't exist, e.g., instead of # models[1] = model1 @@ -199,17 +204,17 @@ def _update(self, index: int, item: ModelLike) -> None: A model or a model hash. """ model = self._model_like_to_model(item) - if model.get_hash() in self: + if model.hash in self: warnings.warn( ( - f"A model with hash `{model.get_hash()}` already exists " + f"A model with hash `{model.hash}` already exists " "in this collection of models. The previous model will be " "overwritten." ), RuntimeWarning, stacklevel=2, ) - self[model.get_hash()] = model + self[model.hash] = model else: self._models.insert(index, None) self._hashes.insert(index, None) @@ -285,14 +290,14 @@ def insert(self, index: int, item: ModelLike): # def remove(self, item: ModelLike): # # Re-use __delitem__ logic # if isinstance(item, Model): - # item = item.get_hash() + # item = item.hash # del self[item] # skipped clear, copy, count def index(self, item: ModelLike, *args) -> int: if isinstance(item, Model): - item = item.get_hash() + item = item.hash return self._hashes.index(item, *args) # skipped reverse, sort @@ -369,7 +374,9 @@ def from_yaml( models_yaml: The path to the PEtab Select list of model YAML file. petab_problem: - See :meth:`Model.from_dict`. + Provide a preloaded copy of the PEtab problem. Note: + all models should share the same PEtab problem if this is + provided. problem: The PEtab Select problem. @@ -381,25 +388,20 @@ def from_yaml( if not model_dict_list: # Empty file models = [] - elif not isinstance(model_dict_list, list): + elif isinstance(model_dict_list, dict): # File contains a single model - models = [ - Model.from_dict( - model_dict_list, - base_path=Path(models_yaml).parent, - petab_problem=petab_problem, - ) - ] - else: - # File contains a list of models - models = [ - Model.from_dict( - model_dict, - base_path=Path(models_yaml).parent, - petab_problem=petab_problem, - ) - for model_dict in model_dict_list - ] + model_dict_list = [model_dict_list] + + models = [ + Model.model_validate( + { + **model_dict, + ROOT_PATH: Path(models_yaml).parent, + MODEL_SUBSPACE_PETAB_PROBLEM: petab_problem, + } + ) + for model_dict in model_dict_list + ] return Models(models=models, problem=problem) @@ -541,25 +543,7 @@ def models_from_yaml_list( allow_single_model: bool = True, problem: Problem = None, ) -> Models: - """Generate a model from a PEtab Select list of model YAML file. - - Deprecated. Use `petab_select.Models.from_yaml` instead. - - Args: - model_list_yaml: - The path to the PEtab Select list of model YAML file. - petab_problem: - See :meth:`Model.from_dict`. - allow_single_model: - Given a YAML file that contains a single model directly (not in - a 1-element list), if ``True`` then the single model will be read in, - else a ``ValueError`` will be raised. - problem: - The PEtab Select problem. - - Returns: - The models. - """ + """Deprecated. Use `petab_select.Models.from_yaml` instead.""" warnings.warn( ( "Use `petab_select.Models.from_yaml` instead. " @@ -580,19 +564,7 @@ def models_to_yaml_list( output_yaml: TYPE_PATH, relative_paths: bool = True, ) -> None: - """Generate a YAML listing of models. - - Deprecated. Use `petab_select.Models.to_yaml` instead. - - Args: - models: - The models. - output_yaml: - The location where the YAML will be saved. - relative_paths: - Whether to rewrite the paths in each model (e.g. the path to the - model's PEtab problem) relative to the `output_yaml` location. - """ + """Deprecated. Use `petab_select.Models.to_yaml` instead.""" warnings.warn( "Use `petab_select.Models.to_yaml` instead.", DeprecationWarning, diff --git a/petab_select/petab.py b/petab_select/petab.py index 8d370c8e..792e6ddf 100644 --- a/petab_select/petab.py +++ b/petab_select/petab.py @@ -1,91 +1,32 @@ -from pathlib import Path +"""Helper methods for working with PEtab problems.""" -import petab.v1 as petab -from more_itertools import one -from petab.v1.C import ESTIMATE, NOMINAL_VALUE +from typing import Literal -from .constants import PETAB_ESTIMATE_FALSE, TYPE_PARAMETER_DICT, TYPE_PATH +import numpy as np +import petab.v1 as petab +from petab.v1.C import ESTIMATE +__all__ = ["get_petab_parameters"] -class PetabMixin: - """Useful things for classes that contain a PEtab problem. - All attributes/methods are prefixed with `petab_`. +def get_petab_parameters( + petab_problem: petab.Problem, as_lists: bool = False +) -> dict[str, float | Literal[ESTIMATE] | list[float | Literal[ESTIMATE]]]: + """Convert PEtab problem parameters to the format in model space files. - Attributes: - petab_yaml: - The location of the PEtab problem YAML file. + Args: petab_problem: The PEtab problem. - petab_parameters: - The parameters from the PEtab parameters table, where keys are - parameter IDs, and values are either :obj:`ESTIMATE` if the - parameter is set to be estimated, else the nominal value. - """ - - def __init__( - self, - petab_yaml: TYPE_PATH | None = None, - petab_problem: petab.Problem | None = None, - parameters_as_lists: bool = False, - ): - if petab_yaml is None and petab_problem is None: - raise ValueError( - "Please supply at least one of either the location of the " - "PEtab problem YAML file, or an instance of the PEtab problem." - ) - self.petab_yaml = petab_yaml - if self.petab_yaml is not None: - self.petab_yaml = Path(self.petab_yaml) - - self.petab_problem = petab_problem - if self.petab_problem is None: - self.petab_problem = petab.Problem.from_yaml(str(petab_yaml)) - - self.petab_parameters = { - parameter_id: ( - row[NOMINAL_VALUE] - if row[ESTIMATE] == PETAB_ESTIMATE_FALSE - else ESTIMATE - ) - for parameter_id, row in self.petab_problem.parameter_df.iterrows() - } - if parameters_as_lists: - self.petab_parameters = { - k: [v] for k, v in self.petab_parameters.items() - } + as_lists: + Each value will be provided inside a list object, similar to the + format for multiple values for a parameter in a model subspace. - @property - def petab_parameter_ids_estimated(self) -> list[str]: - """Get the IDs of all estimated parameters. - - Returns: - The parameter IDs. - """ - return [ - parameter_id - for parameter_id, parameter_value in self.petab_parameters.items() - if parameter_value == ESTIMATE - ] - - @property - def petab_parameter_ids_fixed(self) -> list[str]: - """Get the IDs of all fixed parameters. - - Returns: - The parameter IDs. - """ - estimated = self.petab_parameter_ids_estimated - return [ - parameter_id - for parameter_id in self.petab_parameters - if parameter_id not in estimated - ] - - @property - def petab_parameters_singular(self) -> TYPE_PARAMETER_DICT: - """TODO deprecate and remove?""" - return { - parameter_id: one(parameter_value) - for parameter_id, parameter_value in self.petab_parameters - } + Returns: + Keys are parameter IDs, values are the nominal values for fixed + parameters, or :const:`ESTIMATE` for estimated parameters. + """ + values = np.array(petab_problem.x_nominal, dtype=object) + values[petab_problem.x_free_indices] = ESTIMATE + if as_lists: + values = [[v] for v in values] + return dict(zip(petab_problem.x_ids, values, strict=True)) diff --git a/petab_select/plot.py b/petab_select/plot.py index 859c6a33..e485ba07 100644 --- a/petab_select/plot.py +++ b/petab_select/plot.py @@ -56,7 +56,7 @@ def upset( index = np.argsort(values) values = values[index] labels = [ - model.get_estimated_parameter_ids_all() + model.get_estimated_parameter_ids() for model in np.array(models)[index] ] @@ -122,7 +122,7 @@ def line_best_by_iteration( [best_by_iteration[iteration] for iteration in iterations] ) iteration_labels = [ - str(iteration) + f"\n({labels.get(model.get_hash(), model.model_id)})" + str(iteration) + f"\n({labels.get(model.hash, model.model_id)})" for iteration, model in zip(iterations, best_models, strict=True) ] @@ -208,9 +208,9 @@ def graph_history( if labels is None: labels = { - model.get_hash(): model.model_id + model.hash: model.model_id + ( - f"\n{criterion_values[model.get_hash()]:.2f}" + f"\n{criterion_values[model.hash]:.2f}" if criterion is not None else "" ) @@ -230,7 +230,7 @@ def graph_history( if predecessor_model_hash in models: predecessor_model = models[predecessor_model_hash] from_ = labels.get( - predecessor_model.get_hash(), + predecessor_model.hash, predecessor_model.model_id, ) else: @@ -239,7 +239,7 @@ def graph_history( "not yet implemented." ) from_ = "None" - to = labels.get(model.get_hash(), model.model_id) + to = labels.get(model.hash, model.model_id) edges.append((from_, to)) G.add_edges_from(edges) @@ -312,13 +312,13 @@ def bar_criterion_vs_models( bar_kwargs = {} if labels is None: - labels = {model.get_hash(): model.model_id for model in models} + labels = {model.hash: model.model_id for model in models} if ax is None: _, ax = plt.subplots() bar_model_labels = [ - labels.get(model.get_hash(), model.model_id) for model in models + labels.get(model.hash, model.model_id) for model in models ] criterion_values = models.get_criterion( criterion=criterion, relative=relative @@ -385,7 +385,7 @@ def scatter_criterion_vs_n_estimated( The plot axes. """ labels = { - model.get_hash(): labels.get(model.model_id, model.model_id) + model.hash: labels.get(model.model_id, model.model_id) for model in models } @@ -405,7 +405,7 @@ def scatter_criterion_vs_n_estimated( n_estimated = [] for model in models: - n_estimated.append(len(model.get_estimated_parameter_ids_all())) + n_estimated.append(len(model.get_estimated_parameter_ids())) criterion_values = models.get_criterion( criterion=criterion, relative=relative @@ -495,36 +495,34 @@ def graph_iteration_layers( if draw_networkx_kwargs is None: draw_networkx_kwargs = default_draw_networkx_kwargs - ancestry = { - model.get_hash(): model.predecessor_model_hash for model in models - } + ancestry = {model.hash: model.predecessor_model_hash for model in models} ancestry_as_set = {k: {v} for k, v in ancestry.items()} ordering = [ - [model.get_hash() for model in iteration_models] + [model.hash for model in iteration_models] for iteration_models in group_by_iteration(models).values() ] if VIRTUAL_INITIAL_MODEL_HASH in ancestry.values(): ordering.insert(0, [VIRTUAL_INITIAL_MODEL_HASH]) model_estimated_parameters = { - model.get_hash(): set(model.estimated_parameters) for model in models + model.hash: set(model.estimated_parameters) for model in models } model_criterion_values = models.get_criterion( criterion=criterion, relative=relative, as_dict=True ) model_parameter_diffs = { - model.get_hash(): ( + model.hash: ( (set(), set()) if model.predecessor_model_hash not in model_estimated_parameters else ( - model_estimated_parameters[model.get_hash()].difference( + model_estimated_parameters[model.hash].difference( model_estimated_parameters[model.predecessor_model_hash] ), model_estimated_parameters[ model.predecessor_model_hash - ].difference(model_estimated_parameters[model.get_hash()]), + ].difference(model_estimated_parameters[model.hash]), ) ) for model in models @@ -534,9 +532,9 @@ def graph_iteration_layers( labels = ( labels | { - model.get_hash(): model.model_id + model.hash: model.model_id for model in models - if model.get_hash() not in labels + if model.hash not in labels } | { ModelHash.from_hash( @@ -670,8 +668,8 @@ def __getitem__(self, key): # selected_hashes = set(ancestry.values()) # selected_models = {} # for model in models: - # if model.get_hash() in selected_hashes: - # selected_models[model.get_hash()] = model + # if model.hash in selected_hashes: + # selected_models[model.hash] = model # selected_parameters = { # model_hash: sorted(model.estimated_parameters) diff --git a/petab_select/ui.py b/petab_select/ui.py index 720a319c..34abc14a 100644 --- a/petab_select/ui.py +++ b/petab_select/ui.py @@ -15,11 +15,10 @@ TERMINATE, TYPE_PATH, UNCALIBRATED_MODELS, - VIRTUAL_INITIAL_MODEL, Criterion, Method, ) -from .model import Model, ModelHash, default_compare +from .model import VIRTUAL_INITIAL_MODEL, Model, ModelHash, default_compare from .models import Models from .problem import Problem @@ -145,10 +144,7 @@ def start_iteration( predecessor_model = candidate_space.previous_predecessor_model # If the predecessor model has not yet been calibrated, then calibrate it. - if ( - predecessor_model is not None - and predecessor_model != VIRTUAL_INITIAL_MODEL - ): + if predecessor_model.hash != VIRTUAL_INITIAL_MODEL.hash: if ( predecessor_model.get_criterion( criterion, @@ -202,8 +198,7 @@ def start_iteration( ): return start_iteration_result(candidate_space=candidate_space) - if predecessor_model is not None: - candidate_space.reset(predecessor_model) + candidate_space.reset(predecessor_model) # FIXME store exclusions in candidate space only problem.model_space.exclude_model_hashes(model_hashes=excluded_hashes) @@ -388,7 +383,7 @@ def write_summary_tsv( previous_predecessor_parameter_ids = set() if isinstance(previous_predecessor_model, Model): previous_predecessor_parameter_ids = set( - previous_predecessor_model.get_estimated_parameter_ids_all() + previous_predecessor_model.get_estimated_parameter_ids() ) if predecessor_model is None: @@ -397,7 +392,7 @@ def write_summary_tsv( predecessor_criterion = None if isinstance(predecessor_model, Model): predecessor_parameter_ids = set( - predecessor_model.get_estimated_parameter_ids_all() + predecessor_model.get_estimated_parameter_ids() ) predecessor_criterion = predecessor_model.get_criterion( problem.criterion @@ -412,7 +407,7 @@ def write_summary_tsv( diff_candidates_parameter_ids = [] for candidate_model in candidate_space.models: candidate_parameter_ids = set( - candidate_model.get_estimated_parameter_ids_all() + candidate_model.get_estimated_parameter_ids() ) diff_candidates_parameter_ids.append( list( @@ -423,14 +418,13 @@ def write_summary_tsv( ) # FIXME remove once MostDistantCandidateSpace exists... + # which might be difficult to implement because the most + # distant is a hypothetical model, which is then used to find a + # real model in its neighborhood of the model space method = candidate_space.method - if ( - isinstance(candidate_space, FamosCandidateSpace) - and isinstance(candidate_space.predecessor_model, Model) - and candidate_space.predecessor_model.predecessor_model_hash is None - ): + if isinstance(candidate_space, FamosCandidateSpace): with open(candidate_space.summary_tsv) as f: - if sum(1 for _ in f) > 1: + if f.readlines()[-1].startswith("Jumped"): method = Method.MOST_DISTANT candidate_space.write_summary_tsv( diff --git a/pyproject.toml b/pyproject.toml index 77e1d38c..12d1afaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "pyyaml>=6.0.2", "click>=8.1.7", "dill>=0.3.9", + "mkstd>=0.0.5", ] [project.optional-dependencies] plot = [ @@ -37,7 +38,7 @@ test = [ "amici >= 0.11.25", "fides >= 0.7.5", # "pypesto > 0.2.13", - "pypesto @ git+https://github.com/ICB-DCM/pyPESTO.git@select_class_models#egg=pypesto", + "pypesto @ git+https://github.com/ICB-DCM/pyPESTO.git@select_mkstd#egg=pypesto", "tox >= 3.12.4", ] doc = [ diff --git a/test/analyze/input/models.yaml b/test/analyze/input/models.yaml index 264e1154..3730b6fc 100644 --- a/test/analyze/input/models.yaml +++ b/test/analyze/input/models.yaml @@ -14,7 +14,7 @@ estimated_parameters: k2: 0.15 k3: 0.0 - petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml + model_subspace_petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml predecessor_model_hash: dummy_p0-0 - criteria: AIC: 4 @@ -29,7 +29,7 @@ k1: estimate k2: estimate k3: 0 - petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml + model_subspace_petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml predecessor_model_hash: virtual_initial_model- - criteria: AIC: 3 @@ -47,7 +47,7 @@ estimated_parameters: k2: 0.15 k3: 0.0 - petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml + model_subspace_petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml predecessor_model_hash: virtual_initial_model- - criteria: AIC: 2 @@ -62,5 +62,5 @@ k1: estimate k2: estimate k3: 0 - petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml + model_subspace_petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml predecessor_model_hash: virtual_initial_model- diff --git a/test/analyze/test_analyze.py b/test/analyze/test_analyze.py index 32169a85..f37e6013 100644 --- a/test/analyze/test_analyze.py +++ b/test/analyze/test_analyze.py @@ -5,7 +5,6 @@ from petab_select import ( VIRTUAL_INITIAL_MODEL, Criterion, - ModelHash, Models, analyze, ) @@ -13,7 +12,6 @@ base_dir = Path(__file__).parent DUMMY_HASH = "dummy_p0-0" -VIRTUAL_HASH = ModelHash.from_hash(VIRTUAL_INITIAL_MODEL) @pytest.fixture @@ -26,15 +24,15 @@ def test_group_by_predecessor_model(models: Models) -> None: groups = analyze.group_by_predecessor_model(models) # Expected groups assert len(groups) == 2 - assert VIRTUAL_HASH in groups + assert VIRTUAL_INITIAL_MODEL.hash in groups assert DUMMY_HASH in groups # Expected group members assert len(groups[DUMMY_HASH]) == 1 assert "M-011" in groups[DUMMY_HASH] - assert len(groups[VIRTUAL_HASH]) == 3 - assert "M-110" in groups[VIRTUAL_HASH] - assert "M2-011" in groups[VIRTUAL_HASH] - assert "M2-110" in groups[VIRTUAL_HASH] + assert len(groups[VIRTUAL_INITIAL_MODEL.hash]) == 3 + assert "M-110" in groups[VIRTUAL_INITIAL_MODEL.hash] + assert "M2-011" in groups[VIRTUAL_INITIAL_MODEL.hash] + assert "M2-110" in groups[VIRTUAL_INITIAL_MODEL.hash] def test_group_by_iteration(models: Models) -> None: @@ -64,9 +62,9 @@ def test_get_best_by_iteration(models: Models) -> None: assert 2 in groups assert 5 in groups # Expected best models - assert groups[1].get_hash() == "M2-011" - assert groups[2].get_hash() == "M2-110" - assert groups[5].get_hash() == "M-110" + assert groups[1].hash == "M2-011" + assert groups[2].hash == "M2-110" + assert groups[5].hash == "M-110" def test_relative_criterion_values(models: Models) -> None: diff --git a/test/candidate_space/input/famos_synthetic/test_files/predecessor_model.yaml b/test/candidate_space/input/famos_synthetic/test_files/predecessor_model.yaml index f0442820..3a3aad43 100644 --- a/test/candidate_space/input/famos_synthetic/test_files/predecessor_model.yaml +++ b/test/candidate_space/input/famos_synthetic/test_files/predecessor_model.yaml @@ -1,8 +1,28 @@ +model_subspace_id: model_subspace_1 +model_subspace_indices: +- 1 +- 1 +- 0 +- 0 +- 1 +- 1 +- 0 +- 1 +- 1 +- 1 +- 0 +- 0 +- 0 +- 1 +- 1 +- 1 criteria: AIC: 30330.782621349786 AICc: 30332.80096997364 BIC: 30358.657538777607 NLLH: 15155.391310674893 +model_hash: model_subspace_1-1100110111000111 +model_subspace_petab_yaml: ../petab/FAMoS_2019_problem.yaml estimated_parameters: mu_AB: 0.09706971737957297 mu_AD: -0.6055359156893474 @@ -14,26 +34,23 @@ estimated_parameters: mu_DC: -1.1619119214640863 ro_A: -1.6431508614147425 ro_B: 2.9912966824709097 -model_hash: null -model_id: M_1100110111000111 -model_subspace_id: model_subspace_1 -model_subspace_indices: null +iteration: null +model_id: model_subspace_1-1100110111000111 parameters: + ro_A: estimate + ro_B: estimate + ro_C: 0 + ro_D: 0 mu_AB: estimate + mu_BA: estimate mu_AC: 0 + mu_CA: estimate mu_AD: estimate - mu_BA: estimate + mu_DA: estimate mu_BC: 0 - mu_BD: 0 - mu_CA: estimate mu_CB: 0 - mu_CD: estimate - mu_DA: estimate + mu_BD: 0 mu_DB: estimate + mu_CD: estimate mu_DC: estimate - ro_A: estimate - ro_B: estimate - ro_C: 0 - ro_D: 0 -petab_yaml: ../petab/FAMoS_2019_problem.yaml -predecessor_model_hash: null +predecessor_model_hash: virtual_initial_model- diff --git a/test/candidate_space/test_famos.py b/test/candidate_space/test_famos.py index f4ad33e1..5036dc69 100644 --- a/test/candidate_space/test_famos.py +++ b/test/candidate_space/test_famos.py @@ -5,7 +5,7 @@ from more_itertools import one import petab_select -from petab_select import Method, Models +from petab_select import Method, ModelHash, Models from petab_select.constants import ( CANDIDATE_SPACE, MODEL_HASH, @@ -35,7 +35,7 @@ def expected_criterion_values(input_path): sep="\t", ).set_index(MODEL_HASH) return { - petab_select.model.ModelHash.from_hash(k): v + ModelHash.model_validate(k): v for k, v in calibration_results[Criterion.AICC].items() } @@ -93,7 +93,7 @@ def calibrate( ) -> None: model.set_criterion( criterion=petab_select_problem.criterion, - value=expected_criterion_values[model.get_hash()], + value=expected_criterion_values[model.hash], ) def parse_summary_to_progress_list(summary_tsv: str) -> tuple[Method, set]: @@ -129,6 +129,7 @@ def parse_summary_to_progress_list(summary_tsv: str) -> tuple[Method, set]: all_calibrated_models = Models() candidate_space = petab_select_problem.new_candidate_space() + expected_repeated_model_hash0 = candidate_space.predecessor_model.hash candidate_space.summary_tsv.unlink(missing_ok=True) candidate_space._setup_summary_tsv() @@ -147,7 +148,7 @@ def parse_summary_to_progress_list(summary_tsv: str) -> tuple[Method, set]: calibrated_models = Models() for candidate_model in iteration[UNCALIBRATED_MODELS]: calibrate(candidate_model) - calibrated_models[candidate_model.get_hash()] = candidate_model + calibrated_models[candidate_model.hash] = candidate_model # Finalize iteration iteration_results = petab_select.ui.end_iteration( @@ -162,14 +163,20 @@ def parse_summary_to_progress_list(summary_tsv: str) -> tuple[Method, set]: raise StopIteration("No valid models found.") # A model is encountered twice and therefore skipped. - expected_repeated_model_hash = petab_select_problem.get_model( + expected_repeated_model_hash1 = petab_select_problem.get_model( model_subspace_id=one( petab_select_problem.model_space.model_subspaces ), model_subspace_indices=[int(s) for s in "0001011010010010"], - ).get_hash() - assert len(warning_record) == 1 - assert expected_repeated_model_hash in warning_record[0].message.args[0] + ).hash + # The predecessor model is also re-encountered. + assert len(warning_record) == 2 + assert ( + str(expected_repeated_model_hash0) in warning_record[0].message.args[0] + ) + assert ( + str(expected_repeated_model_hash1) in warning_record[1].message.args[0] + ) progress_list = parse_summary_to_progress_list(candidate_space.summary_tsv) diff --git a/test/cli/input/model.yaml b/test/cli/input/model.yaml index dcaaa5a2..7cda4c4a 100644 --- a/test/cli/input/model.yaml +++ b/test/cli/input/model.yaml @@ -1,10 +1,16 @@ -- criteria: {} +- model_subspace_id: M + model_subspace_indices: + - 0 + - 1 + - 1 + criteria: {} + model_hash: M-011 + model_subspace_petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml + estimated_parameters: + k2: 0.15 + k3: 0.0 model_id: model parameters: k1: 0.2 k2: estimate k3: estimate - estimated_parameters: - k2: 0.15 - k3: 0.0 - petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml diff --git a/test/cli/input/models.yaml b/test/cli/input/models.yaml index 06aa3933..b9d12b8e 100644 --- a/test/cli/input/models.yaml +++ b/test/cli/input/models.yaml @@ -1,4 +1,14 @@ -- criteria: {} +- model_subspace_id: M + model_subspace_indices: + - 0 + - 1 + - 1 + criteria: {} + model_hash: M-011 + model_subspace_petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml + estimated_parameters: + k2: 0.15 + k3: 0.0 model_id: model_1 model_subspace_id: M model_subspace_indices: @@ -9,11 +19,14 @@ k1: 0.2 k2: estimate k3: estimate - estimated_parameters: - k2: 0.15 - k3: 0.0 - petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml -- criteria: {} +- model_subspace_id: M + model_subspace_indices: + - 1 + - 1 + - 0 + criteria: {} + model_hash: M-110 + model_subspace_petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml model_id: model_2 model_subspace_id: M model_subspace_indices: @@ -24,4 +37,3 @@ k1: estimate k2: estimate k3: 0 - petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml diff --git a/test/cli/test_cli.py b/test/cli/test_cli.py index 0a4dc34d..ccf015ea 100644 --- a/test/cli/test_cli.py +++ b/test/cli/test_cli.py @@ -55,7 +55,6 @@ def test_model_to_petab( ], ) - print(result.stdout) # The new PEtab problem YAML file is output to stdout correctly. assert ( result.stdout == f'{base_dir / "output" / "model" / "problem.yaml"}\n' diff --git a/test/model/input/model.yaml b/test/model/input/model.yaml index dcaaa5a2..233861de 100644 --- a/test/model/input/model.yaml +++ b/test/model/input/model.yaml @@ -1,10 +1,15 @@ -- criteria: {} - model_id: model - parameters: - k1: 0.2 - k2: estimate - k3: estimate - estimated_parameters: - k2: 0.15 - k3: 0.0 - petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml +model_subspace_id: M +model_subspace_indices: +- 0 +- 1 +- 1 +criteria: {} +model_subspace_petab_yaml: ../../../doc/examples/model_selection/petab_problem.yaml +model_id: model +parameters: + k1: 0.2 + k2: estimate + k3: estimate +estimated_parameters: + k2: 0.15 + k3: 0.0 diff --git a/test/pypesto/generate_expected_models.py b/test/pypesto/generate_expected_models.py index 912748ff..ca6e8b09 100644 --- a/test/pypesto/generate_expected_models.py +++ b/test/pypesto/generate_expected_models.py @@ -13,8 +13,9 @@ # Set to `[]` to test all test_cases = [ - #'0004', - #'0008', + #'0001', + # "0003", + "0009", ] # Do not use computationally-expensive test cases in CI @@ -41,6 +42,12 @@ def objective_customizer(obj): obj.amici_solver.setRelativeTolerance(1e-12) +model_problem_options = { + "minimize_options": minimize_options, + "objective_customizer": objective_customizer, +} + + for test_case_path in test_cases_path.glob("*"): if test_cases and test_case_path.stem not in test_cases: continue @@ -67,19 +74,17 @@ def objective_customizer(obj): ) # Run the selection process until "exhausted". - pypesto_select_problem.select_to_completion( - minimize_options=minimize_options, - objective_customizer=objective_customizer, - ) + pypesto_select_problem.select_to_completion(**model_problem_options) # Get the best model - best_model = petab_select_problem.get_best( + best_model = petab_select.analyze.get_best( models=pypesto_select_problem.calibrated_models, + criterion=petab_select_problem.criterion, ) # Generate the expected model. - best_model.to_yaml(expected_model_yaml, paths_relative_to=test_case_path) + best_model.to_yaml(expected_model_yaml) - pypesto_select_problem.calibrated_models.to_yaml( - f"all_models_{test_case_path.stem}.yaml" - ) + # pypesto_select_problem.calibrated_models.to_yaml( + # output_yaml="all_models.yaml", + # ) diff --git a/test_cases/0001/expected.yaml b/test_cases/0001/expected.yaml index 25c97f14..a230aa28 100644 --- a/test_cases/0001/expected.yaml +++ b/test_cases/0001/expected.yaml @@ -1,19 +1,19 @@ -criteria: - AIC: -6.1754055040468785 - NLLH: -4.087702752023439 -estimated_parameters: - sigma_x2: 0.12242920616053495 -iteration: 1 -model_hash: M1_1-000 -model_id: M1_1-000 model_subspace_id: M1_1 model_subspace_indices: - 0 - 0 - 0 +criteria: + NLLH: -4.087702752023436 + AIC: -6.175405504046871 +model_hash: M1_1-000 +model_subspace_petab_yaml: petab/petab_problem.yaml +estimated_parameters: + sigma_x2: 0.12242920313036142 +iteration: 1 +model_id: M1_1-000 parameters: k1: 0.2 k2: 0.1 k3: 0 -petab_yaml: petab/petab_problem.yaml -predecessor_model_hash: null +predecessor_model_hash: virtual_initial_model- diff --git a/test_cases/0002/expected.yaml b/test_cases/0002/expected.yaml index 57811a85..510c60ce 100644 --- a/test_cases/0002/expected.yaml +++ b/test_cases/0002/expected.yaml @@ -1,20 +1,20 @@ -criteria: - AIC: -4.705325991177407 - NLLH: -4.3526629955887035 -estimated_parameters: - k1: 0.20160877932991236 - sigma_x2: 0.11714038666761385 -iteration: 2 -model_hash: M1_3-000 -model_id: M1_3-000 model_subspace_id: M1_3 model_subspace_indices: - 0 - 0 - 0 +criteria: + NLLH: -4.352662995581719 + AIC: -4.705325991163438 +model_hash: M1_3-000 +model_subspace_petab_yaml: ../0001/petab/petab_problem.yaml +estimated_parameters: + k1: 0.2016087813530968 + sigma_x2: 0.11714041764571122 +iteration: 2 +model_id: M1_3-000 parameters: k1: estimate k2: 0.1 k3: 0 -petab_yaml: ../0001/petab/petab_problem.yaml predecessor_model_hash: M1_0-000 diff --git a/test_cases/0003/expected.yaml b/test_cases/0003/expected.yaml index a0366cfb..218cba26 100644 --- a/test_cases/0003/expected.yaml +++ b/test_cases/0003/expected.yaml @@ -1,19 +1,19 @@ -criteria: - BIC: -6.383646034818824 - NLLH: -4.087702752023439 -estimated_parameters: - sigma_x2: 0.12242920723808924 -iteration: 1 -model_hash: M1-110 -model_id: M1-110 model_subspace_id: M1 model_subspace_indices: - 1 - 1 - 0 +criteria: + NLLH: -4.0877027520227704 + BIC: -6.383646034817486 +model_hash: M1-110 +model_subspace_petab_yaml: ../0001/petab/petab_problem.yaml +estimated_parameters: + sigma_x2: 0.12242924701706556 +iteration: 1 +model_id: M1-110 parameters: k1: 0.2 k2: 0.1 k3: 0 -petab_yaml: ../0001/petab/petab_problem.yaml -predecessor_model_hash: null +predecessor_model_hash: virtual_initial_model- diff --git a/test_cases/0004/expected.yaml b/test_cases/0004/expected.yaml index 24f8ae41..8f220f09 100644 --- a/test_cases/0004/expected.yaml +++ b/test_cases/0004/expected.yaml @@ -1,20 +1,20 @@ -criteria: - AICc: -0.7053259911583094 - NLLH: -4.352662995579155 -estimated_parameters: - k1: 0.2016087783781175 - sigma_x2: 0.11714035262205941 -iteration: 3 -model_hash: M1_3-000 -model_id: M1_3-000 model_subspace_id: M1_3 model_subspace_indices: - 0 - 0 - 0 +criteria: + NLLH: -4.352662995594862 + AICc: -0.7053259911897243 +model_hash: M1_3-000 +model_subspace_petab_yaml: ../0001/petab/petab_problem.yaml +estimated_parameters: + k1: 0.20160877986376358 + sigma_x2: 0.11714041204425464 +iteration: 3 +model_id: M1_3-000 parameters: k1: estimate k2: 0.1 k3: 0 -petab_yaml: ../0001/petab/petab_problem.yaml predecessor_model_hash: M1_6-000 diff --git a/test_cases/0005/expected.yaml b/test_cases/0005/expected.yaml index c30365a8..35949e30 100644 --- a/test_cases/0005/expected.yaml +++ b/test_cases/0005/expected.yaml @@ -1,20 +1,20 @@ -criteria: - AIC: -4.705325991200599 - NLLH: -4.3526629956003 -estimated_parameters: - k1: 0.2016087798698859 - sigma_x2: 0.11714036476432785 -iteration: 2 -model_hash: M1_3-000 -model_id: M1_3-000 model_subspace_id: M1_3 model_subspace_indices: - 0 - 0 - 0 +criteria: + NLLH: -4.352662995589992 + AIC: -4.7053259911799845 +model_hash: M1_3-000 +model_subspace_petab_yaml: ../0001/petab/petab_problem.yaml +estimated_parameters: + k1: 0.20160877971477925 + sigma_x2: 0.11714036509532029 +iteration: 2 +model_id: M1_3-000 parameters: k1: estimate k2: 0.1 k3: 0 -petab_yaml: ../0001/petab/petab_problem.yaml predecessor_model_hash: M1_0-000 diff --git a/test_cases/0006/expected.yaml b/test_cases/0006/expected.yaml index c8e92c9c..4a05253a 100644 --- a/test_cases/0006/expected.yaml +++ b/test_cases/0006/expected.yaml @@ -1,19 +1,19 @@ -criteria: - AIC: -6.1754055040468785 - NLLH: -4.087702752023439 -estimated_parameters: - sigma_x2: 0.12242920606535417 -iteration: 1 -model_hash: M1_0-000 -model_id: M1_0-000 model_subspace_id: M1_0 model_subspace_indices: - 0 - 0 - 0 +criteria: + NLLH: -4.087702752023439 + AIC: -6.1754055040468785 +model_hash: M1_0-000 +model_subspace_petab_yaml: ../0001/petab/petab_problem.yaml +estimated_parameters: + sigma_x2: 0.12242920634250658 +iteration: 1 +model_id: M1_0-000 parameters: k1: 0.2 k2: 0.1 k3: 0 -petab_yaml: ../0001/petab/petab_problem.yaml -predecessor_model_hash: virtual_initial_model +predecessor_model_hash: virtual_initial_model- diff --git a/test_cases/0007/expected.yaml b/test_cases/0007/expected.yaml index 4efd158a..f8d17428 100644 --- a/test_cases/0007/expected.yaml +++ b/test_cases/0007/expected.yaml @@ -1,18 +1,18 @@ -criteria: - AIC: 11.117195861535194 - NLLH: 5.558597930767597 -estimated_parameters: {} -iteration: 1 -model_hash: M1_0-000 -model_id: M1_0-000 model_subspace_id: M1_0 model_subspace_indices: - 0 - 0 - 0 +criteria: + NLLH: 5.558597930767597 + AIC: 11.117195861535194 +model_hash: M1_0-000 +model_subspace_petab_yaml: petab/petab_problem.yaml +estimated_parameters: {} +iteration: 1 +model_id: M1_0-000 parameters: k1: 0.2 k2: 0.1 k3: 0 -petab_yaml: petab/petab_problem.yaml -predecessor_model_hash: virtual_initial_model +predecessor_model_hash: virtual_initial_model- diff --git a/test_cases/0008/expected.yaml b/test_cases/0008/expected.yaml index 6162ff4c..715ec176 100644 --- a/test_cases/0008/expected.yaml +++ b/test_cases/0008/expected.yaml @@ -1,18 +1,18 @@ -criteria: - AICc: 11.117195861535194 - NLLH: 5.558597930767597 -estimated_parameters: {} -iteration: 4 -model_hash: M1_0-000 -model_id: M1_0-000 model_subspace_id: M1_0 model_subspace_indices: - 0 - 0 - 0 +criteria: + NLLH: 5.558597930767597 + AICc: 11.117195861535194 +model_hash: M1_0-000 +model_subspace_petab_yaml: ../0007/petab/petab_problem.yaml +estimated_parameters: {} +iteration: 4 +model_id: M1_0-000 parameters: k1: 0.2 k2: 0.1 k3: 0 -petab_yaml: ../0007/petab/petab_problem.yaml predecessor_model_hash: M1_3-000 diff --git a/test_cases/0009/expected.yaml b/test_cases/0009/expected.yaml index 6abbaa99..58bb09fa 100644 --- a/test_cases/0009/expected.yaml +++ b/test_cases/0009/expected.yaml @@ -1,18 +1,3 @@ -criteria: - AICc: -1708.1109924658595 - NLLH: -862.351792529226 -estimated_parameters: - a_0ac_k08: 0.4085141271467614 - a_b: 0.06675812072340812 - a_k05_k05k12: 30.88819982704895 - a_k05k12_k05k08k12: 4.872706275493909 - a_k08k12k16_4ac: 53.80184925213997 - a_k12_k05k12: 8.267871339049703 - a_k12k16_k08k12k16: 33.03793450182137 - a_k16_k12k16: 10.42455614921354 -iteration: 11 -model_hash: M-01000100001000010010000000010001 -model_id: M-01000100001000010010000000010001 model_subspace_id: M model_subspace_indices: - 0 @@ -47,6 +32,22 @@ model_subspace_indices: - 0 - 0 - 1 +criteria: + NLLH: -862.3517925313981 + AICc: -1708.1109924702037 +model_hash: M-01000100001000010010000000010001 +model_subspace_petab_yaml: petab/petab_problem.yaml +estimated_parameters: + a_0ac_k08: 0.40850355273291267 + a_k05_k05k12: 30.888150959586138 + a_k12_k05k12: 8.267845459216893 + a_k16_k12k16: 10.424629099941777 + a_k05k12_k05k08k12: 4.872747603868694 + a_k12k16_k08k12k16: 33.03769174387633 + a_k08k12k16_4ac: 53.80106471593421 + a_b: 0.06675819571287103 +iteration: 11 +model_id: M-01000100001000010010000000010001 parameters: a_0ac_k05: 1 a_0ac_k08: estimate @@ -55,30 +56,29 @@ parameters: a_k05_k05k08: 1 a_k05_k05k12: estimate a_k05_k05k16: 1 + a_k08_k05k08: 1 + a_k08_k08k12: 1 + a_k08_k08k16: 1 + a_k12_k05k12: estimate + a_k12_k08k12: 1 + a_k12_k12k16: 1 + a_k16_k05k16: 1 + a_k16_k08k16: 1 + a_k16_k12k16: estimate a_k05k08_k05k08k12: 1 a_k05k08_k05k08k16: 1 - a_k05k08k12_4ac: 1 - a_k05k08k16_4ac: 1 a_k05k12_k05k08k12: estimate a_k05k12_k05k12k16: 1 - a_k05k12k16_4ac: 1 a_k05k16_k05k08k16: 1 a_k05k16_k05k12k16: 1 - a_k08_k05k08: 1 - a_k08_k08k12: 1 - a_k08_k08k16: 1 a_k08k12_k05k08k12: 1 a_k08k12_k08k12k16: 1 - a_k08k12k16_4ac: estimate a_k08k16_k05k08k16: 1 a_k08k16_k08k12k16: 1 - a_k12_k05k12: estimate - a_k12_k08k12: 1 - a_k12_k12k16: 1 a_k12k16_k05k12k16: 1 a_k12k16_k08k12k16: estimate - a_k16_k05k16: 1 - a_k16_k08k16: 1 - a_k16_k12k16: estimate -petab_yaml: petab/petab_problem.yaml + a_k05k08k12_4ac: 1 + a_k05k08k16_4ac: 1 + a_k05k12k16_4ac: 1 + a_k08k12k16_4ac: estimate predecessor_model_hash: M-01000100001010010010000000010001 diff --git a/test_cases/0009/predecessor_model.yaml b/test_cases/0009/predecessor_model.yaml index 4471224b..581fa453 100644 --- a/test_cases/0009/predecessor_model.yaml +++ b/test_cases/0009/predecessor_model.yaml @@ -70,5 +70,5 @@ parameters: a_k16_k05k16: 1 a_k16_k08k16: 1 a_k16_k12k16: estimate -petab_yaml: petab/petab_problem.yaml -predecessor_model_hash: null +model_subspace_petab_yaml: petab/petab_problem.yaml +predecessor_model_hash: virtual_initial_model-