diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9f0b0036..bd7d9253 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,12 +3,14 @@ name: CI
# trigger
on:
push:
- branches:
- - main
pull_request:
+ branches:
+ - main
schedule:
# run Tuesday and Friday at 02:00 UTC
- cron: '00 2 * * TUE,FRI'
+ workflow_dispatch:
+ merge_group:
jobs:
base:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a3220358..10ee3412 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,12 +5,12 @@
repos:
- repo: https://github.com/psf/black
- rev: 22.6.0
+ rev: 23.10.0
hooks:
- id: black
description: The uncompromising code formatter
- repo: https://github.com/pycqa/isort
- rev: 5.10.1
+ rev: 5.12.0
hooks:
- id: isort
name: isort (python)
@@ -21,14 +21,14 @@ repos:
name: isort (pyi)
types: [pyi]
- repo: https://github.com/nbQA-dev/nbQA
- rev: 1.3.1
+ rev: 1.7.0
hooks:
- id: nbqa-black
- id: nbqa-pyupgrade
args: [--py36-plus]
- id: nbqa-isort
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.3.0
+ rev: v4.5.0
hooks:
- id: check-yaml
description: Check yaml files for parseable syntax
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..deef57f9
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,27 @@
+# .readthedocs.yml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ builder: html
+ configuration: doc/conf.py
+ fail_on_warning: True
+
+python:
+ install:
+ - method: pip
+ path: .
+ extra_requirements:
+ - doc
+
+build:
+ os: "ubuntu-22.04"
+ apt_packages:
+ - libatlas-base-dev
+ - swig
+ tools:
+ python: "3.11"
diff --git a/README.md b/README.md
index 984a2fb5..c0d87161 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,59 @@
# PEtab Select
-The repository for the development of the extension to PEtab for model selection, including the additional file formats and Python 3 package.
+
+The repository for the development of the extension to PEtab for model
+selection, including the additional file formats and Python 3 package.
## Install
-The Python 3 package provides both the Python 3 and command-line (CLI) interfaces, and can be installed from PyPI, with `pip3 install petab-select`.
+
+The Python 3 package provides both the Python 3 and command-line (CLI)
+interfaces, and can be installed from PyPI, with `pip3 install petab-select`.
+
+## Documentation
+
+Further documentation is available at
+[http://petab-select.readthedocs.io/](http://petab-select.readthedocs.io/).
## Examples
+
There are example Jupyter notebooks for usage of PEtab Select with
+
- the command-line interface, and
- the Python 3 interface,
in the `doc/examples` directory.
## Supported features
+
### Criterion
+
- `AIC`: https://en.wikipedia.org/wiki/Akaike_information_criterion#Definition
- `AICc`: https://en.wikipedia.org/wiki/Akaike_information_criterion#Modification_for_small_sample_size
- `BIC`: https://en.wikipedia.org/wiki/Bayesian_information_criterion#Definition
### Methods
+
- `forward`: https://en.wikipedia.org/wiki/Stepwise_regression#Main_approaches
- `backward`: https://en.wikipedia.org/wiki/Stepwise_regression#Main_approaches
-- `brute_force`: Optimize all possible model candidates, then return the model with the best criterion value.
+- `brute_force`: Optimize all possible model candidates, then return the model
+ with the best criterion value.
- `famos`: https://doi.org/10.1371/journal.pcbi.1007230
-Note that the directional methods (forward, backward) find models with the smallest step size (in terms of number of estimated parameters). For example, given the forward method and a predecessor model with 2 estimated parameters, if there are no models with 3 estimated parameters, but some models with 4 estimated parameters, then the search may return candidate models with 4 estimated parameters.
+Note that the directional methods (forward, backward) find models with the
+smallest step size (in terms of number of estimated parameters). For example,
+given the forward method and a predecessor model with 2 estimated parameters,
+if there are no models with 3 estimated parameters, but some models with 4
+estimated parameters, then the search may return candidate models with 4
+estimated parameters.
## File formats
-Column or key names that are surrounding by square brackets (e.g. `[constraint_files]`) are optional.
+
+Column or key names that are surrounding by square brackets
+(e.g. `[constraint_files]`) are optional.
+
### Selection problem
+
A YAML file with a description of the model selection problem.
+
```yaml
format_version: [string]
criterion: [string]
@@ -38,50 +63,74 @@ model_space_files: [List of filenames]
[predecessor_model_files]: [List of filenames]
```
-- `format_version`: The version of the model selection extension format (e.g. `'beta_1'`)
+- `format_version`: The version of the model selection extension format (
+ e.g. `'beta_1'`)
- `criterion`: The criterion by which models should be compared (e.g. `'AIC'`)
-- `method`: The method by which model candidates should be generated (e.g. `'forward'`)
+- `method`: The method by which model candidates should be generated
+ (e.g. `'forward'`)
- `model_space_files`: The filenames of model space files.
- `constraint_files`: The filenames of constraint files.
-- `predecessor_model_files`: The filenames of predecessor (initial) model files.
+- `predecessor_model_files`: The filenames of predecessor (initial) model
+ files.
### Model space
+
A TSV with candidate models, in compressed or uncompressed format.
-| `model_subspace_id` | `petab_yaml` | [`sbml`] | `parameter_id_1` | ... | `parameter_id_n` |
-|----------------------|------------------|------------|--------------------------------------------------------|-----|--------------------------------------------------------|
-| (Unique) [string] | [string] | [string] | [string/float] OR [; delimited list of string/float] | ... | [string/float] OR [; delimited list of string/float] |
+| `model_subspace_id` | `petab_yaml` | [`sbml`] | `parameter_id_1` | ... | `parameter_id_n` |
+|---------------------|--------------|----------|------------------------------------------------------|-----|------------------------------------------------------|
+| (Unique) [string] | [string] | [string] | [string/float] OR [; delimited list of string/float] | ... | [string/float] OR [; delimited list of string/float] |
- `model_subspace_id`: An ID for the model subspace.
- `petab_yaml`: The PEtab YAML filename that serves as the base for a model.
-- `sbml`: An SBML filename. If the PEtab YAML file specifies multiple SBML models, this can select a specific model by model filename.
-- `parameter_id_1`...`parameter_id_n` : Parameter IDs that are specified to take specific values or be estimated. Example valid values are:
- - uncompressed format:
- - `0.0`
- - `1.0`
- - `estimate`
- - compressed format
- - `0.0;1.1;estimate` (the parameter can take the values `0.0` or `1.1`, or be estimated according to the PEtab problem)
+- `sbml`: An SBML filename. If the PEtab YAML file specifies multiple SBML
+ models, this can select a specific model by model filename.
+- `parameter_id_1`...`parameter_id_n` : Parameter IDs that are specified to
+ take specific values or be estimated. Example valid values are:
+ - uncompressed format:
+ - `0.0`
+ - `1.0`
+ - `estimate`
+ - compressed format
+ - `0.0;1.1;estimate` (the parameter can take the values `0.0` or `1.1`,
+ or be estimated according to the PEtab problem)
### Constraints
+
A TSV file with constraints.
-| `petab_yaml` | [`if`] | `constraint` |
-|------------------|-------------------------------------------|--------------------------------|
-| [string] | [SBML L3 Formula expression] | [SBML L3 Formula expression] |
+| `petab_yaml` | [`if`] | `constraint` |
+|--------------|------------------------------|------------------------------|
+| [string] | [SBML L3 Formula expression] | [SBML L3 Formula expression] |
-- `petab_yaml`: The filename of the PEtab YAML file that this constraint applies to.
-- `if`: As a single YAML can relate to multiple models in the model space file, this ensures the constraint is only applies to the models that match this `if` statement
-- `constraint`: If a model violates this constraint, it is skipped during the model selection process and not optimized.
+- `petab_yaml`: The filename of the PEtab YAML file that this constraint
+ applies to.
+- `if`: As a single YAML can relate to multiple models in the model space file,
+ this ensures the constraint is only applied to the models that match
+ this `if` statement
+- `constraint`: If a model violates this constraint, it is skipped during the
+ model selection process and not optimized.
### Model(s) (Predecessor models / model interchange / report)
-- Predecessor models are used to initialize an appropriate model selection method. Model IDs should be unique here and compared to model IDs in any model space files.
-- Model interchange refers to the format used to transfer model information between PEtab Select and a PEtab-compatible calibration tool, during the model selection process.
-- Report refers to the final results of the model selection process, which may include calibration results from any calibrated models, or just the select 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. All other keys are may be required, for the different uses of the format (e.g. the report format should include `estimated_parameters`), or at different stages of the model selection process (the PEtab-compatible calibration tool should provide `criteria` for model comparison).
+- *Predecessor models* are used to initialize an appropriate model selection
+ method. Model IDs should be unique here and compared to model IDs in any
+ model space files.
+- *Model interchange* refers to the format used to transfer model information
+ between PEtab Select and a PEtab-compatible calibration tool, during the
+ model selection process.
+- *Report* refers to the final results of the model selection process, which may
+ include calibration results from any calibrated models, or just the select
+ 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.
+All other keys are maybe required, for the different uses of the format (e.g.,
+the report format should include `estimated_parameters`), or at different
+stages of the model selection process (the PEtab-compatible calibration tool
+should provide `criteria` for model comparison).
```yaml
[criteria]: [Dictionary of criterion names and values]
@@ -94,36 +143,55 @@ petab_yaml: [string]
[sbml]: [string]
```
-- `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.
+- `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).
+- `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.
+- `predecessor_model_hash`: The hash of the model that preceded this model
+ during the model selection process.
- `sbml`: Same as in model space files.
## Test cases
-Several test cases are provided, to test the compatibility of a PEtab-compatible calibration tool with different PEtab Select features.
-
-The test cases are available in the `test_cases` directory, and are provided in the model format.
-
-| Test ID | Criterion | Method | Model space files | Compressed format | Constraints files | Predecessor (initial) models files |
-|---------|-----------| -------------------|-------------------|-------------------|-------------------|----------------------|
-| 0001 | (all) | (only one model) | 1 | | | |
-| 0002[1](#test_case_0002) | AIC | forward | 1 | | | |
-| 0003 | BIC | all | 1 | Yes | | |
-| 0004 | AICc | backward | 1 | | 1 | |
-| 0005 | AIC | forward | 1 | | | 1 |
-| 0006 | AIC | forward | 1 | | | |
-| 0007[2](#test_case_0007_and_0008) | AIC | forward | 1 | | | |
-| 0008[2](#test_case_0007_and_0008) | AICc | backward | 1 | | | |
-| 0009[3](#test_case_0009) | AICc | FAMoS | 1 | Yes | | Yes |
-
-1. 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.
-
-2. Noise parameter is removed, noise is fixed to `1`.
-3. This is a computationally-expensive problem to solve. Developers can try a model selection initialized with the provided predecessor model, which is a model start that reproducibly finds the expected model. To solve the problem reproducibly ab initio, on the order of 100 random model starts are required. This test case reproduces the model selection problem presented in https://doi.org/10.1016/j.cels.2016.01.002 .
+Several test cases are provided, to test the compatibility of a
+PEtab-compatible calibration tool with different PEtab Select features.
+
+The test cases are available in the `test_cases` directory, and are provided in
+the model format.
+
+| Test ID | Criterion | Method | Model space files | Compressed format | Constraints files | Predecessor (initial) models files |
+|----------------------------------------------|-----------|------------------|-------------------|-------------------|-------------------|------------------------------------|
+| 0001 | (all) | (only one model) | 1 | | | |
+| 0002[1](#test_case_0002) | AIC | forward | 1 | | | |
+| 0003 | BIC | all | 1 | Yes | | |
+| 0004 | AICc | backward | 1 | | 1 | |
+| 0005 | AIC | forward | 1 | | | 1 |
+| 0006 | AIC | forward | 1 | | | |
+| 0007[2](#test_case_0007_and_0008) | AIC | forward | 1 | | | |
+| 0008[2](#test_case_0007_and_0008) | AICc | backward | 1 | | | |
+| 0009[3](#test_case_0009) | AICc | FAMoS | 1 | Yes | | Yes |
+
+1. 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.
+
+2. Noise parameter is removed, noise is
+fixed to `1`.
+
+3. This is a computationally expensive problem to
+solve. Developers can try a model selection initialized with the provided
+predecessor model, which is a model start that reproducibly finds the expected
+model. To solve the problem reproducibly ab initio, on the order of 100
+random model starts are required. This test case reproduces the model selection
+problem presented in https://doi.org/10.1016/j.cels.2016.01.002 .
diff --git a/changes.md b/changes.md
index 3f6ee711..63219bae 100644
--- a/changes.md
+++ b/changes.md
@@ -1,5 +1,15 @@
# Changes
+## 0.1.11
+- fixed bug in stepwise moves when working with multiple subspaces (#65)
+- fixed bug in FAMoS switching (#68)
+- removed `BidirectionalCandidateSpace` and `ForwardAndBackwardCandidateSpace` (#68)
+- set estimated parameters as the nominal values in exported PEtab problems (#77)
+- many CI, doc, and code quality improvements by @dweindl (#57, #58, #59, #60, #61, #63, #69, #70, #71, #72, #74)
+- fixed bug in model hash reproducibility (#78)
+- refactored `governing_method` out of `CandidateSpace` (#73)
+- fixed bug related to attempted calibration of virtual models (#75)
+
## 0.1.10
- added `Model.set_estimated_parameters`
- now expected that `Model.estimated_parameters` has untransformed values
diff --git a/doc/.gitignore b/doc/.gitignore
new file mode 100644
index 00000000..9719ae4c
--- /dev/null
+++ b/doc/.gitignore
@@ -0,0 +1,2 @@
+_build
+generated
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 00000000..d4bb2cbb
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/doc/api.rst b/doc/api.rst
new file mode 100644
index 00000000..5f239f5b
--- /dev/null
+++ b/doc/api.rst
@@ -0,0 +1,20 @@
+petab-select Python API
+=======================
+
+.. rubric:: Modules
+
+.. autosummary::
+ :toctree: generated
+
+ petab_select
+ petab_select.candidate_space
+ petab_select.constants
+ petab_select.criteria
+ petab_select.handlers
+ petab_select.misc
+ petab_select.model
+ petab_select.model_space
+ petab_select.model_subspace
+ petab_select.petab
+ petab_select.problem
+ petab_select.ui
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 00000000..32fcb951
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,76 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+import inspect
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+project = 'petab-select'
+copyright = '2023, The PEtab Select developers'
+author = 'The PEtab Select developers'
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = [
+ "readthedocs_ext.readthedocs",
+ "sphinx.ext.napoleon",
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.autosummary",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.mathjax",
+ "nbsphinx",
+ "IPython.sphinxext.ipython_console_highlighting",
+ "recommonmark",
+ "sphinx_autodoc_typehints",
+]
+
+templates_path = ['_templates']
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+
+intersphinx_mapping = {
+ "petab": (
+ "https://petab.readthedocs.io/projects/libpetab-python/en/latest/",
+ None,
+ ),
+ "pandas": ("https://pandas.pydata.org/docs/", None),
+ "numpy": ("https://numpy.org/devdocs/", None),
+ "python": ("https://docs.python.org/3", None),
+}
+
+autosummary_generate = True
+autodoc_default_options = {
+ "special-members": "__init__",
+ "inherited-members": True,
+}
+
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = 'sphinx_rtd_theme'
+# html_static_path = ['_static']
+
+
+def autodoc_skip_member(app, what, name, obj, skip, options):
+ """Exclude some objects from the documentation."""
+ if inspect.isbuiltin(obj):
+ return True
+
+ # Skip inherited members from builtins
+ # (skips, for example, all the int/str-derived methods of enums
+ if (
+ objclass := getattr(obj, "__objclass__", None)
+ ) and objclass.__module__ == "builtins":
+ return True
+
+ return None
+
+
+def setup(app: "sphinx.application.Sphinx"):
+ app.connect("autodoc-skip-member", autodoc_skip_member, priority=0)
diff --git a/doc/examples.rst b/doc/examples.rst
new file mode 100644
index 00000000..4147a47a
--- /dev/null
+++ b/doc/examples.rst
@@ -0,0 +1,11 @@
+Examples
+========
+
+Various example notebooks.
+
+.. toctree::
+ :maxdepth: 1
+
+ examples/example_cli_famos.ipynb
+ examples/workflow_cli.ipynb
+ examples/workflow_python.ipynb
diff --git a/doc/examples/example_cli_famos_helpers.py b/doc/examples/example_cli_famos_helpers.py
index cdbd2cdd..05f57686 100644
--- a/doc/examples/example_cli_famos_helpers.py
+++ b/doc/examples/example_cli_famos_helpers.py
@@ -1,5 +1,5 @@
from pathlib import Path
-from typing import Tuple
+from typing import List, Tuple
import pandas as pd
@@ -47,7 +47,9 @@ def calibrate(
)
-def parse_summary_to_progress_list(summary_tsv: str) -> Tuple[Method, set]:
+def parse_summary_to_progress_list(
+ summary_tsv: str,
+) -> List[Tuple[Method, set]]:
"""Get progress information from the summary file."""
df_raw = pd.read_csv(summary_tsv, sep='\t')
df = df_raw.loc[~pd.isnull(df_raw["predecessor change"])]
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 00000000..4e086749
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,22 @@
+.. petab-select documentation master file, created by
+ sphinx-quickstart on Mon Oct 23 09:01:31 2023.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to petab-select's documentation!
+========================================
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ examples
+ api
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/doc/make.bat b/doc/make.bat
new file mode 100644
index 00000000..32bb2452
--- /dev/null
+++ b/doc/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.https://www.sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/petab_select/__init__.py b/petab_select/__init__.py
index bd2994f5..b2455da3 100644
--- a/petab_select/__init__.py
+++ b/petab_select/__init__.py
@@ -1,5 +1,7 @@
"""Model selection extension for PEtab."""
+import sys
+
from .candidate_space import *
from .constants import *
from .criteria import *
@@ -9,3 +11,9 @@
from .model_subspace import *
from .problem import *
from .ui import *
+
+__all__ = [
+ x
+ for x in dir(sys.modules[__name__])
+ if not x.startswith('_') and x != 'sys'
+]
diff --git a/petab_select/candidate_space.py b/petab_select/candidate_space.py
index b54887df..ecd89eaf 100644
--- a/petab_select/candidate_space.py
+++ b/petab_select/candidate_space.py
@@ -4,10 +4,9 @@
import copy
import csv
import logging
-import os.path
import warnings
from pathlib import Path
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union
import numpy as np
from more_itertools import one
@@ -27,9 +26,17 @@
Method,
)
from .handlers import TYPE_LIMIT, LimitHandler
-from .misc import snake_case_to_camel_case
from .model import Model, default_compare
+__all__ = [
+ 'BackwardCandidateSpace',
+ 'BruteForceCandidateSpace',
+ 'CandidateSpace',
+ 'FamosCandidateSpace',
+ 'ForwardCandidateSpace',
+ 'LateralCandidateSpace',
+]
+
class CandidateSpace(abc.ABC):
"""A base class for collecting candidate models.
@@ -40,8 +47,8 @@ class CandidateSpace(abc.ABC):
Attributes:
distances:
The distances of all candidate models from the initial model.
- FIXME change list to int? Is storage of more than one value
- useful?
+
+ FIXME(dilpath) change list to int? Is storage of more than one value useful?
predecessor_model:
The model used for comparison, e.g. for stepwise methods.
previous_predecessor_model:
@@ -49,9 +56,9 @@ class CandidateSpace(abc.ABC):
models:
The current set of candidate models.
exclusions:
- A list of model hashes. Models that match a hash in `exclusions` will not
+ A list of model hashes. Models that match a hash in ``exclusions`` will not
be accepted into the candidate space. The hashes of models that are accepted
- are added to `exclusions`.
+ are added to ``exclusions``.
limit:
A handler to limit the number of accepted models.
method:
@@ -59,28 +66,23 @@ class CandidateSpace(abc.ABC):
governing_method:
Used to store the search method that governs the choice of method during
a search. In some cases, this is always the same as the method attribute.
- An example of a difference is in the bidirectional method, where `governing_method`
+ 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.
- retry_model_space_search_if_no_models:
- Whether a search with a candidate space should be repeated upon failure.
- Useful for the `BidirectionalCandidateSpace`, which switches directions
- upon failure.
summary_tsv:
- A string or `pathlib.Path`. A summary of the model selection progress
+ A string or :class:`pathlib.Path`. A summary of the model selection progress
will be written to this file.
- #limited:
- # A descriptor that handles the limit on the number of accepted models.
- #limit:
- # Models will fail `self.consider` if `len(self.models) >= limit`.
- """
- governing_method: Method = None
- method: Method = None
- retry_model_space_search_if_no_models: bool = False
+ FIXME(dilpath)
+ #limited:
+ # A descriptor that handles the limit on the number of accepted models.
+ #limit:
+ # Models will fail `self.consider` if `len(self.models) >= limit`.
+ """
def __init__(
self,
+ method: Method,
# TODO add MODEL_TYPE = Union[str, Model], str for VIRTUAL_INITIAL_MODEL
predecessor_model: Optional[Model] = None,
exclusions: Optional[List[Any]] = None,
@@ -88,13 +90,12 @@ def __init__(
summary_tsv: TYPE_PATH = None,
previous_predecessor_model: Optional[Model] = None,
):
+ self.method = method
+
self.limit = LimitHandler(
current=self.n_accepted,
limit=limit,
)
- # Each candidate class specifies this as a class attribute.
- if self.governing_method is None:
- self.governing_method = self.method
self.reset(predecessor_model=predecessor_model, exclusions=exclusions)
self.summary_tsv = summary_tsv
@@ -124,10 +125,7 @@ def write_summary_tsv(self, row):
def _setup_summary_tsv(self):
self.summary_tsv.resolve().parent.mkdir(parents=True, exist_ok=True)
- if self.summary_tsv.exists():
- with open(self.summary_tsv, "r", encoding="utf-8") as f:
- last_row = f.readlines()[-1]
- else:
+ if not self.summary_tsv.exists():
self.write_summary_tsv(
[
'method',
@@ -162,7 +160,7 @@ def is_plausible(self, model: Model) -> bool:
models in the model space.
For example, given a forward selection method that starts with an
- initial model `self.predecessor_model` that has no estimated
+ initial model ``self.predecessor_model`` that has no estimated
parameters, then only models with one or more estimated parameters are
plausible.
@@ -171,7 +169,7 @@ def is_plausible(self, model: Model) -> bool:
The candidate model.
Returns:
- `True` if `model` is plausible, else `False`.
+ ``True`` if ``model`` is plausible, else ``False``.
"""
return True
@@ -185,7 +183,7 @@ def distance(self, model: Model) -> Union[None, float, int]:
The initial model.
Returns:
- The distance from `predecessor_model` to `model`, or `None` if the
+ The distance from ``predecessor_model`` to ``model``, or ``None`` if the
distance should not be computed.
"""
return None
@@ -203,9 +201,11 @@ def accept(
The model that will be added.
distance:
The distance of the model from the predecessor model.
- #keep_others:
- # Whether to keep other models that were previously added to the
- # candidate space.
+
+ FIXME(dilpath)
+ #keep_others:
+ # Whether to keep other models that were previously added to the
+ # candidate space.
"""
model.predecessor_model_hash = (
self.predecessor_model.get_hash()
@@ -236,7 +236,7 @@ def exclude(
else:
self.exclusions.append(model.get_hash())
- def exclude_hashes(self, hashes: List[str]) -> None:
+ def exclude_hashes(self, hashes: Sequence[str]) -> None:
"""Exclude models from future consideration, by hash.
Args:
@@ -270,15 +270,17 @@ def consider(self, model: Union[Model, None]) -> bool:
Args:
model:
- The candidate model. This value may be `None` if the `ModelSubspace`
+ The candidate model. This value may be ``None`` if the :class:`ModelSubspace`
decided to exclude the model that would have been sent.
Returns:
Whether it is OK to send additional models to the candidate space. For
example, if the limit of the number of accepted models has been reached,
then no further models should be sent.
+
+ FIXME(dilpath)
TODO change to return whether the model was accepted, and instead add
- `self.continue` to determine whether additional models should be sent.
+ `self.continue` to determine whether additional models should be sent.
"""
# Model was excluded by the `ModelSubspace` that called this method, so can be
# skipped.
@@ -340,7 +342,7 @@ def wrap_search_subspaces(self, search_subspaces: Callable[[], None]):
"""Decorate the subspace searches of a model space.
Used by candidate spaces to perform changes that alter the search.
- See `BidirectionalCandidateSpace` for an example, where it's used to switch directions.
+ See :class:`BidirectionalCandidateSpace` for an example, where it's used to switch directions.
Args:
search_subspaces:
@@ -466,7 +468,7 @@ def update_after_calibration(
):
"""Do work in the candidate space after calibration.
- For example, this is used by the `FamosCandidateSpace` to switch
+ For example, this is used by the :class:`FamosCandidateSpace` to switch
methods.
Different candidate spaces require different arguments. All arguments
@@ -481,13 +483,12 @@ class ForwardCandidateSpace(CandidateSpace):
Attributes:
direction:
- `1` for the forward method, `-1` for the backward method.
+ ``1`` for the forward method, ``-1`` for the backward method.
max_steps:
Maximum number of steps forward in a single iteration of forward selection.
- Defaults to no maximum (`None`).
+ Defaults to no maximum (``None``).
"""
- method = Method.FORWARD
direction = 1
def __init__(
@@ -503,7 +504,12 @@ def __init__(
self.max_steps = max_steps
if predecessor_model is None:
predecessor_model = VIRTUAL_INITIAL_MODEL
- super().__init__(*args, predecessor_model=predecessor_model, **kwargs)
+ super().__init__(
+ method=Method.FORWARD if self.direction == 1 else Method.BACKWARD,
+ *args,
+ predecessor_model=predecessor_model,
+ **kwargs,
+ )
def is_plausible(self, model: Model) -> bool:
distances = self.distances_in_estimated_parameters(model)
@@ -530,7 +536,7 @@ def distance(self, model: Model) -> int:
return distances['l1']
def _consider_method(self, model) -> bool:
- """See `CandidateSpace._consider_method`."""
+ """See :meth:`CandidateSpace._consider_method`."""
distance = self.distance(model)
# Get the distance of the current "best" plausible model(s)
@@ -552,106 +558,9 @@ def _consider_method(self, model) -> bool:
class BackwardCandidateSpace(ForwardCandidateSpace):
"""The backward method class."""
- method = Method.BACKWARD
direction = -1
-class BidirectionalCandidateSpace(ForwardCandidateSpace):
- """The bidirectional method class.
-
- Attributes:
- method_history:
- The history of models that were found at each search.
- A list of dictionaries, where each dictionary contains keys for the `METHOD`
- and the list of `MODELS`.
- """
-
- # TODO refactor method to inherit from governing_method if not specified
- # by constructor argument -- remove from here.
- method = Method.BIDIRECTIONAL
- governing_method = Method.BIDIRECTIONAL
- retry_model_space_search_if_no_models = True
-
- def __init__(
- self,
- *args,
- initial_method: Method = Method.FORWARD,
- **kwargs,
- ):
- super().__init__(*args, **kwargs)
-
- # FIXME cannot access from CLI
- # FIXME probably fine to replace `self.initial_method`
- # with `self.method` here. i.e.:
- # 1. change `method` to `Method.FORWARD
- # 2. change signature to `initial_method: Method = None`
- # 3. change code here to `if initial_method is not None: self.method = initial_method`
- self.initial_method = initial_method
-
- self.method_history: List[Dict[str, Union[Method, List[Model]]]] = []
-
- def update_method(self, method: Method):
- if method == Method.FORWARD:
- self.direction = 1
- elif method == Method.BACKWARD:
- self.direction = -1
- else:
- raise NotImplementedError(
- f'Bidirectional direction must be either `Method.FORWARD` or `Method.BACKWARD`, not {method}.'
- )
-
- self.method = method
-
- def switch_method(self):
- if self.method == Method.FORWARD:
- method = Method.BACKWARD
- elif self.method == Method.BACKWARD:
- method = Method.FORWARD
-
- self.update_method(method=method)
-
- def setup_before_model_subspaces_search(self):
- # If previous search found no models, then switch method.
- previous_search = (
- None if not self.method_history else self.method_history[-1]
- )
- if previous_search is None:
- self.update_method(self.initial_method)
- return
-
- self.update_method(previous_search[METHOD])
- if not previous_search[MODELS]:
- self.switch_method()
- self.retry_model_space_search_if_no_models = False
-
- def setup_after_model_subspaces_search(self):
- current_search = {
- METHOD: self.method,
- MODELS: self.models,
- }
- self.method_history.append(current_search)
- self.method = self.governing_method
-
- def wrap_search_subspaces(self, search_subspaces):
- def wrapper():
- def iterate():
- self.setup_before_model_subspaces_search()
- search_subspaces()
- self.setup_after_model_subspaces_search()
-
- # Repeat until models are found or switching doesn't help.
- iterate()
- while (
- not self.models and self.retry_model_space_search_if_no_models
- ):
- iterate()
-
- # Reset flag for next time.
- self.retry_model_space_search_if_no_models = True
-
- return wrapper
-
-
class FamosCandidateSpace(CandidateSpace):
"""The FAMoS method class.
@@ -680,14 +589,13 @@ class FamosCandidateSpace(CandidateSpace):
n_reattempts:
Integer. The total number of times that a jump-to-most-distance action
will be performed, triggered whenever the model selection would
- normally terminate. Defaults to no reattempts (`0`).
+ normally terminate. Defaults to no reattempts (``0``).
consecutive_laterals:
- Boolean. If `True`, the method will continue performing lateral moves
+ Boolean. If ``True``, the method will continue performing lateral moves
while they produce better models. Otherwise, the method scheme will
be applied after one lateral move.
"""
- method = Method.FAMOS
default_method_scheme = {
(Method.BACKWARD, Method.FORWARD): Method.LATERAL,
(Method.FORWARD, Method.BACKWARD): Method.LATERAL,
@@ -805,9 +713,12 @@ def __init__(
self.initial_method
]
- super().__init__(*args, predecessor_model=predecessor_model, **kwargs)
-
- self.governing_method = Method.FAMOS
+ super().__init__(
+ method=self.method,
+ *args,
+ predecessor_model=predecessor_model,
+ **kwargs,
+ )
self.n_reattempts = n_reattempts
@@ -883,7 +794,7 @@ def update_after_calibration(
logging.info("Switching method")
self.switch_method(calibrated_models=calibrated_models)
self.switch_inner_candidate_space(
- calibrated_models=calibrated_models,
+ exclusions=list(calibrated_models),
)
logging.info(
"Method switched to ", self.inner_candidate_space.method
@@ -896,10 +807,9 @@ def update_from_newly_calibrated_models(
newly_calibrated_models: Dict[str, Model],
criterion: Criterion,
) -> bool:
- """Update the self.best_models with the latest
- `newly_calibrated_models`
+ """Update ``self.best_models`` with the latest ``newly_calibrated_models``
and determine if there was a new best model. If so, return
- True. False otherwise."""
+ ``False``. ``True`` otherwise."""
go_into_switch_method = True
for newly_calibrated_model in newly_calibrated_models.values():
@@ -929,7 +839,7 @@ def update_from_newly_calibrated_models(
self.best_models = self.best_models[: self.most_distant_max_number]
# When we switch to LATERAL method, we will do only one iteration with this
- # method. So if we do it succesfully (i.e. that we found a new best model), we
+ # method. So if we do it successfully (i.e. that we found a new best model), we
# want to switch method. This is why we put go_into_switch_method to True, so
# we go into the method switching pipeline
if self.method == Method.LATERAL and not self.consecutive_laterals:
@@ -949,9 +859,9 @@ def insert_model_into_best_models(
self.best_models.insert(insert_index, model_to_insert)
def consider(self, model: Union[Model, None]) -> bool:
- """Re-define consider of FAMoS to be the consider method
- of the inner_candidate_space. Update all the attributes
- changed in the condsider method."""
+ """Re-define ``consider`` of FAMoS to be the ``consider`` method
+ of the ``inner_candidate_space``. Update all the attributes
+ changed in the ``consider`` method."""
if self.limit.reached():
return False
@@ -975,12 +885,12 @@ def consider(self, model: Union[Model, None]) -> bool:
return True
def _consider_method(self, model) -> bool:
- """See `CandidateSpace._consider_method`."""
+ """See :meth:`CandidateSpace._consider_method`."""
return self.inner_candidate_space._consider_method(model)
def reset_accepted(self) -> None:
"""Changing the reset_accepted to reset the
- inner_candidate_space as well."""
+ ``inner_candidate_space`` as well."""
super().reset_accepted()
self.inner_candidate_space.reset_accepted()
@@ -988,7 +898,7 @@ def set_predecessor_model(
self, predecessor_model: Union[Model, str, None]
):
"""Setting the predecessor model for the
- inner_candidate_space as well."""
+ ``inner_candidate_space`` as well."""
super().set_predecessor_model(predecessor_model=predecessor_model)
self.inner_candidate_space.set_predecessor_model(
predecessor_model=predecessor_model
@@ -996,7 +906,7 @@ def set_predecessor_model(
def set_exclusions(self, exclusions: Union[List[str], None]):
"""Setting the exclusions for the
- inner_candidate_space as well."""
+ ``inner_candidate_space`` as well."""
self.exclusions = exclusions
self.inner_candidate_space.exclusions = exclusions
if self.exclusions is None:
@@ -1005,7 +915,7 @@ def set_exclusions(self, exclusions: Union[List[str], None]):
def set_limit(self, limit: TYPE_LIMIT = None):
"""Setting the limit for the
- inner_candidate_space as well."""
+ ``inner_candidate_space`` as well."""
if limit is not None:
self.limit.set_limit(limit)
self.inner_candidate_space.limit.set_limit(limit)
@@ -1049,7 +959,7 @@ def switch_method(
calibrated_models: Dict[str, Model],
) -> None:
"""Switch to the next method with respect to the history
- of methods used and the switching scheme in self.method_scheme"""
+ of methods used and the switching scheme in ``self.method_scheme``."""
previous_method = self.method
next_method = previous_method
@@ -1108,16 +1018,20 @@ def switch_method(
self.update_method(method=next_method)
def update_method(self, method: Method):
- """Update self.method to the method."""
+ """Update ``self.method`` to ``method``."""
self.method = method
def switch_inner_candidate_space(
self,
- calibrated_models: Dict[str, Model],
+ exclusions: List[str],
):
- """Switch self.inner_candidate_space to the candidate space of
- the current self.method."""
+ """Switch the inner candidate space to match the current method.
+
+ Args:
+ exclusions:
+ Hashes of excluded models.
+ """
# if self.method != Method.MOST_DISTANT:
self.inner_candidate_space = self.inner_candidate_spaces[self.method]
@@ -1125,7 +1039,7 @@ def switch_inner_candidate_space(
# calibrated models
self.inner_candidate_space.reset(
predecessor_model=self.predecessor_model,
- exclusions=calibrated_models,
+ exclusions=exclusions,
)
def jump_to_most_distant(
@@ -1169,11 +1083,12 @@ def get_most_distant(
) -> Model:
"""
Get most distant model to all the checked models. We take models from the
- sorted list of best models (self.best_models) and construct complements of
+ sorted list of best models (``self.best_models``) and construct complements of
these models. For all these complements we compute the distance in number of
different estimated parameters to all models from history. For each complement
we take the minimum of these distances as it's distance to history. Then we
choose the complement model with the maximal distance to history.
+
TODO:
Next we check if this model is contained in any subspace. If so we choose it.
If not we choose the model in a subspace that has least distance to this
@@ -1252,38 +1167,9 @@ def wrapper():
return wrapper
-# TODO rewrite so BidirectionalCandidateSpace inherits from ForwardAndBackwardCandidateSpace
-# instead
-class ForwardAndBackwardCandidateSpace(BidirectionalCandidateSpace):
- method = Method.FORWARD_AND_BACKWARD
- governing_method = Method.FORWARD_AND_BACKWARD
- retry_model_space_search_if_no_models = False
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs, initial_method=None)
-
- def wrap_search_subspaces(self, search_subspaces):
- def wrapper():
- for method in [Method.FORWARD, Method.BACKWARD]:
- self.update_method(method=method)
- search_subspaces()
- self.setup_after_model_subspaces_search()
-
- return wrapper
-
- # Disable unused interface
- setup_before_model_subspaces_search = None
- switch_method = None
-
- def setup_after_model_space_search(self):
- pass
-
-
class LateralCandidateSpace(CandidateSpace):
"""Find models with the same number of estimated parameters."""
- method = Method.LATERAL
-
def __init__(
self,
*args,
@@ -1297,6 +1183,7 @@ def __init__(
Maximal allowed number of swap moves. If 0 then there is no maximum.
"""
super().__init__(
+ method=Method.LATERAL,
*args,
predecessor_model=predecessor_model,
**kwargs,
@@ -1339,8 +1226,6 @@ def _consider_method(self, model):
class BruteForceCandidateSpace(CandidateSpace):
"""The brute-force method class."""
- method = Method.BRUTE_FORCE
-
def __init__(self, *args, **kwargs):
# if args or kwargs:
# # FIXME remove?
@@ -1349,40 +1234,43 @@ def __init__(self, *args, **kwargs):
# 'Arguments were provided but will be ignored, because of the '
# 'brute force candidate space.'
# )
- super().__init__(*args, **kwargs)
+ super().__init__(
+ method=Method.BRUTE_FORCE,
+ *args,
+ **kwargs,
+ )
def _consider_method(self, model):
return True
-candidate_space_classes = [
- ForwardCandidateSpace,
- BackwardCandidateSpace,
- BidirectionalCandidateSpace,
- LateralCandidateSpace,
- BruteForceCandidateSpace,
- ForwardAndBackwardCandidateSpace,
- FamosCandidateSpace,
-]
+candidate_space_classes = {
+ Method.FORWARD: ForwardCandidateSpace,
+ Method.BACKWARD: BackwardCandidateSpace,
+ Method.LATERAL: LateralCandidateSpace,
+ Method.BRUTE_FORCE: BruteForceCandidateSpace,
+ Method.FAMOS: FamosCandidateSpace,
+}
-def method_to_candidate_space_class(method: Method) -> str:
- """Instantiate a candidate space given its method name.
+def method_to_candidate_space_class(method: Method) -> Type[CandidateSpace]:
+ """Get a candidate space class, given its method name.
Args:
method:
- The name of the method corresponding to one of the implemented candidate
- spaces.
+ The name of the method corresponding to one of the implemented
+ candidate spaces.
Returns:
The candidate space.
"""
- for candidate_space_class in candidate_space_classes:
- if candidate_space_class.method == method:
- return candidate_space_class
- raise NotImplementedError(
- f'The provided method name {method} does not correspond to an implemented candidate space.'
- )
+ candidate_space_class = candidate_space_classes.get(method, None)
+ if candidate_space_class is None:
+ raise NotImplementedError(
+ f'The provided method `{method}` does not correspond to an '
+ 'implemented candidate space.'
+ )
+ return candidate_space_class
'''
diff --git a/petab_select/cli.py b/petab_select/cli.py
index 96b281a5..36f15615 100644
--- a/petab_select/cli.py
+++ b/petab_select/cli.py
@@ -1,5 +1,4 @@
"""The PEtab Select command-line interface."""
-import warnings
from pathlib import Path
from typing import Any, Dict, List
@@ -7,12 +6,11 @@
import dill
import numpy as np
import pandas as pd
-import yaml
from more_itertools import one
from . import ui
-from .candidate_space import CandidateSpace, method_to_candidate_space_class
-from .constants import INITIAL_MODEL_METHODS, PETAB_YAML
+from .candidate_space import CandidateSpace
+from .constants import PETAB_YAML
from .model import Model, models_from_yaml_list, models_to_yaml_list
from .problem import Problem
diff --git a/petab_select/constants.py b/petab_select/constants.py
index 25749482..ac0b2a73 100644
--- a/petab_select/constants.py
+++ b/petab_select/constants.py
@@ -1,7 +1,8 @@
"""Constants for the PEtab Select package."""
+import sys
from enum import Enum
from pathlib import Path
-from typing import Dict, List, Union
+from typing import Dict, List, Literal, Union
# Zero-indexed column/row indices
MODEL_ID_COLUMN = 0
@@ -48,12 +49,6 @@
# COMPARED_MODEL_ID = 'compared_'+MODEL_ID
YAML_FILENAME = 'yaml'
-# FORWARD = 'forward'
-# BACKWARD = 'backward'
-# BIDIRECTIONAL = 'bidirectional'
-# LATERAL = 'lateral'
-
-
# DISTANCES = {
# FORWARD: {
# 'l1': 1,
@@ -70,20 +65,6 @@
# }
CRITERIA = 'criteria'
-# FIXME remove, change all uses to Enum below
-# AIC = 'AIC'
-# AICC = 'AICc'
-# BIC = 'BIC'
-# AKAIKE_INFORMATION_CRITERION = AIC
-# CORRECTED_AKAIKE_INFORMATION_CRITERION = AICC
-# BAYESIAN_INFORMATION_CRITERION = BIC
-# LH = 'LH'
-# LLH = 'LLH'
-# NLLH = 'NLLH'
-# LIKELIHOOD = LH
-# LOG_LIKELIHOOD = LLH
-# NEGATIVE_LOG_LIKELIHOOD = NLLH
-
PARAMETERS = 'parameters'
# PARAMETER_ESTIMATE = 'parameter_estimate'
@@ -105,8 +86,7 @@
# Parameters can be fixed to a value, or estimated if indicated with the string
# `ESTIMATE`.
-# TODO change to `Literal[ESTIMATE]` (Python >= 3.8)
-TYPE_PARAMETER = Union[float, int, ESTIMATE]
+TYPE_PARAMETER = Union[float, int, Literal[ESTIMATE]]
TYPE_PARAMETER_OPTIONS = List[TYPE_PARAMETER]
# Parameter ID -> parameter value mapping.
TYPE_PARAMETER_DICT = Dict[str, TYPE_PARAMETER]
@@ -119,49 +99,62 @@
class Method(str, Enum):
"""String literals for model selection methods."""
+ #: The backward stepwise method.
BACKWARD = 'backward'
- BIDIRECTIONAL = 'bidirectional'
+ #: The brute-force method.
BRUTE_FORCE = 'brute_force'
+ #: The FAMoS method.
FAMOS = 'famos'
+ #: The forward stepwise method.
FORWARD = 'forward'
- FORWARD_AND_BACKWARD = 'forward_and_backward'
+ #: The lateral, or swap, method.
LATERAL = 'lateral'
+ #: The jump-to-most-distant-model method.
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'
-# Methods that move through model space by taking steps away from some model.
+#: Methods that move through model space by taking steps away from some model.
STEPWISE_METHODS = [
Method.BACKWARD,
- Method.BIDIRECTIONAL,
Method.FORWARD,
- Method.FORWARD_AND_BACKWARD,
Method.LATERAL,
]
-# Methods that require an initial model.
+#: Methods that require an initial model.
INITIAL_MODEL_METHODS = [
Method.BACKWARD,
- Method.BIDIRECTIONAL,
Method.FORWARD,
- Method.FORWARD_AND_BACKWARD,
Method.LATERAL,
]
-# Virtual initial models can be used to initialize some initial model methods.
+#: Virtual initial models can be used to initialize some initial model methods.
VIRTUAL_INITIAL_MODEL = 'virtual_initial_model'
+#: Methods that are compatible with a virtual initial model.
VIRTUAL_INITIAL_MODEL_METHODS = [
Method.BACKWARD,
- Method.BIDIRECTIONAL,
Method.FORWARD,
- Method.FORWARD_AND_BACKWARD,
+]
+
+
+__all__ = [
+ x
+ for x in dir(sys.modules[__name__])
+ if not x.startswith('_')
+ and x not in ('sys', "Enum", "Path", "Dict", "List", "Literal", "Union")
]
diff --git a/petab_select/criteria.py b/petab_select/criteria.py
index ffe6fa84..1f245a28 100644
--- a/petab_select/criteria.py
+++ b/petab_select/criteria.py
@@ -1,26 +1,28 @@
"""Implementations of model selection criteria."""
-import math
-
+import numpy as np
import petab
-from petab.C import OBJECTIVE, OBJECTIVE_PRIOR_PARAMETERS, OBJECTIVE_PRIOR_TYPE
+from petab.C import OBJECTIVE_PRIOR_PARAMETERS, OBJECTIVE_PRIOR_TYPE
+
+import petab_select
-from .constants import ( # LH,; LLH,; NLLH,
- PETAB_PROBLEM,
- TYPE_CRITERION,
- Criterion,
-)
+from .constants import PETAB_PROBLEM, Criterion # LH,; LLH,; NLLH,
-# from .model import Model
+__all__ = [
+ 'calculate_aic',
+ 'calculate_aicc',
+ 'calculate_bic',
+ 'CriterionComputer',
+]
# use as attribute e.g. `Model.criterion_computer`?
class CriterionComputer:
- """Compute various criterion."""
+ """Compute various criteria."""
def __init__(
self,
- model: 'petab_select.Model',
+ model: 'petab_select.model.Model',
):
self.model = model
self._petab_problem = None
@@ -29,7 +31,7 @@ def __init__(
def petab_problem(self) -> petab.Problem:
"""The PEtab problem that corresponds to the model.
- Implemented as a property such that the `petab.Problem` object
+ Implemented as a property such that the :class:`petab.Problem` object
is only constructed if explicitly requested.
Improves speed of operations on models by a lot. For example, analysis of models
@@ -90,7 +92,7 @@ def get_llh(self) -> float:
"""Get the log-likelihood."""
llh = self.model.get_criterion(Criterion.LLH, compute=False)
if llh is None:
- llh = math.log(self.get_lh())
+ llh = np.log(self.get_lh())
return llh
def get_lh(self) -> float:
@@ -102,9 +104,9 @@ def get_lh(self) -> float:
if lh is not None:
return lh
elif llh is not None:
- return math.exp(llh)
+ return np.exp(llh)
elif nllh is not None:
- return math.exp(-1 * nllh)
+ return np.exp(-1 * nllh)
raise ValueError(
'Please supply the likelihood (LH) or a compatible transformation. Compatible transformations: log(LH), -log(LH).'
@@ -205,9 +207,11 @@ def calculate_aicc(
Returns:
The AICc value.
"""
- return calculate_aic(n_estimated, nllh) + 2 * n_estimated * (
- n_estimated + 1
- ) / (n_measurements + n_priors - n_estimated - 1)
+ return calculate_aic(
+ nllh=nllh, n_estimated=n_estimated
+ ) + 2 * n_estimated * (n_estimated + 1) / (
+ n_measurements + n_priors - n_estimated - 1
+ )
def calculate_bic(
@@ -232,4 +236,4 @@ def calculate_bic(
Returns:
The BIC value.
"""
- return n_estimated * math.log(n_measurements + n_priors) + 2 * nllh
+ return n_estimated * np.log(n_measurements + n_priors) + 2 * nllh
diff --git a/petab_select/handlers.py b/petab_select/handlers.py
index 50daa78e..573acf7d 100644
--- a/petab_select/handlers.py
+++ b/petab_select/handlers.py
@@ -1,7 +1,4 @@
-from pathlib import Path
-from typing import Any, Callable, List, Optional, Union
-
-import numpy as np
+from typing import Callable, Union
# `float` for `np.inf`
TYPE_LIMIT = Union[float, int]
diff --git a/petab_select/misc.py b/petab_select/misc.py
index 0c1b8f9c..bb9cbcb9 100644
--- a/petab_select/misc.py
+++ b/petab_select/misc.py
@@ -1,7 +1,7 @@
import hashlib
# import json
-from typing import Any, Dict, List, Union
+from typing import Any, List, Union
from .constants import ( # TYPE_PARAMETER_OPTIONS_DICT,
ESTIMATE,
@@ -9,6 +9,10 @@
TYPE_PARAMETER_OPTIONS,
)
+__all__ = [
+ 'parameter_string_to_value',
+]
+
def hashify(x: Any) -> str:
"""Generate a hash.
@@ -27,7 +31,7 @@ def hashify(x: Any) -> str:
def hash_parameter_dict(dict_: TYPE_PARAMETER_DICT):
"""Hash a dictionary of parameter values."""
- value = tuple(zip(dict_.keys(), dict_.values()))
+ value = tuple((k, dict_[k]) for k in sorted(dict_))
return hashify(value)
diff --git a/petab_select/model.py b/petab_select/model.py
index c08bf8a5..55ddf56f 100644
--- a/petab_select/model.py
+++ b/petab_select/model.py
@@ -1,11 +1,9 @@
"""The `Model` class."""
-import abc
import warnings
from os.path import relpath
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
-import numpy as np
import petab
import yaml
from more_itertools import one
@@ -38,6 +36,13 @@
)
from .petab import PetabMixin
+__all__ = [
+ 'Model',
+ 'default_compare',
+ 'models_from_yaml_list',
+ 'models_to_yaml_list',
+]
+
class Model(PetabMixin):
"""A (possibly uncalibrated) model.
@@ -48,16 +53,16 @@ class Model(PetabMixin):
Attributes:
converters_load:
- Functions to convert attributes from YAML to `Model`.
+ Functions to convert attributes from YAML to :class:`Model`.
converters_save:
- Functions to convert attributes from `Model` to YAML.
+ Functions to convert attributes from :class:`Model` to YAML.
criteria:
The criteria values of the calibrated model (e.g. AIC).
hash_attributes:
This attribute is currently not used.
Attributes that will be used to calculate the hash of the
- `Model` instance. NB: this hash is used during pairwise comparison
- to determine whether any two `Model` instances are unique. The
+ :class:`Model` instance. NB: this hash is used during pairwise comparison
+ to determine whether any two :class:`Model` instances are unique. The
model instances are compared by their parameter estimation
problems, as opposed to parameter estimation results, which may
differ due to e.g. floating-point arithmetic.
@@ -74,7 +79,7 @@ class Model(PetabMixin):
Select model YAML. These are untransformed values (i.e., not on
log scale).
saved_attributes:
- Attributes that will be saved to disk by the `Model.to_yaml`
+ Attributes that will be saved to disk by the :meth:`Model.to_yaml`
method.
"""
@@ -192,7 +197,7 @@ def set_criterion(self, criterion: Criterion, value: float) -> None:
Args:
criterion:
- The criterion (e.g. `petab_select.constants.Criterion.AIC`).
+ The criterion (e.g. ``petab_select.constants.Criterion.AIC``).
value:
The criterion value for the (presumably calibrated) model.
"""
@@ -224,45 +229,66 @@ def get_criterion(
self,
criterion: Criterion,
compute: bool = True,
+ raise_on_failure: bool = True,
) -> Union[TYPE_CRITERION, None]:
"""Get a criterion value for the model.
Args:
criterion:
- The ID of the criterion (e.g. `petab_select.constants.Criterion.AIC`).
+ The ID of the criterion (e.g. ``petab_select.constants.Criterion.AIC``).
compute:
Whether to try to compute the criterion value based on other model
- attributes. For example, if the `'AIC'` criterion is requested, this
+ attributes. For example, if the ``'AIC'`` criterion is requested, this
can be computed from a predetermined model likelihood and its
number of estimated parameters.
+ raise_on_failure:
+ 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
"""
if criterion not in self.criteria and compute:
- self.compute_criterion(criterion=criterion)
+ 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) -> TYPE_CRITERION:
+ def compute_criterion(
+ self,
+ criterion: Criterion,
+ raise_on_failure: bool = True,
+ ) -> TYPE_CRITERION:
"""Compute a criterion value for the model.
The value will also be stored, which will overwrite any previously stored value
for the criterion.
Args:
- id:
- The ID of the criterion (e.g. `petab_select.constants.Criterion.AIC`).
+ criterion:
+ The ID of the criterion
+ (e.g. :obj:`petab_select.constants.Criterion.AIC`).
+ raise_on_failure:
+ Whether to raise a `ValueError` if the criterion could not be
+ computed. If `False`, `None` is returned.
Returns:
The criterion value.
"""
- criterion_value = self.criterion_computer(criterion)
- self.set_criterion(criterion, criterion_value)
- return criterion_value
+ try:
+ criterion_value = self.criterion_computer(criterion)
+ self.set_criterion(criterion, criterion_value)
+ result = criterion_value
+ except ValueError:
+ if raise_on_failure:
+ raise
+ result = None
+ return result
def set_estimated_parameters(
self,
@@ -275,9 +301,9 @@ def set_estimated_parameters(
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 ``estimated_parameters`` values are on the scale
+ defined in the PEtab problem (``True``), or untransformed
+ (``False``).
"""
if scaled:
estimated_parameters = self.petab_problem.unscale_parameters(
@@ -298,15 +324,15 @@ def from_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 `Model.__init__` method.
+ 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
- `Model.petab_yaml` may be relative).
+ :meth:`Model.petab_yaml` may be relative).
petab_problem:
Optionally provide the PEtab problem, to avoid loading it multiple
times.
- NB: This may causes issues if multiple models write to the same PEtab
+ NB: This may cause issues if multiple models write to the same PEtab
problem in memory.
Returns:
@@ -363,20 +389,20 @@ def to_dict(
resolve_paths: bool = True,
paths_relative_to: Union[str, Path] = None,
) -> Dict[str, Any]:
- """Generate a dictionary from the attributes of a `Model` instance.
+ """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`.
+ 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 `Model.__init__` method.
+ the :meth:`Model.__init__` method.
"""
model_dict = {}
for attribute in self.saved_attributes:
@@ -398,14 +424,14 @@ def to_dict(
return model_dict
def to_yaml(self, petab_yaml: TYPE_PATH, *args, **kwargs) -> None:
- """Generate a PEtab Select model YAML file from a `Model` instance.
+ """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`.
+ Additional arguments are passed to ``self.to_dict``.
"""
# FIXME change `getattr(self, PETAB_YAML)` to be relative to
# destination?
@@ -417,27 +443,50 @@ def to_yaml(self, petab_yaml: TYPE_PATH, *args, **kwargs) -> None:
def to_petab(
self,
output_path: TYPE_PATH = None,
- ) -> Tuple[petab.Problem, TYPE_PATH]:
+ set_estimated_parameters: Optional[bool] = None,
+ ) -> Dict[str, Union[petab.Problem, TYPE_PATH]]:
"""Generate a PEtab problem.
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.
+ 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`.
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
+ ``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. If
+ first value) into any PEtab-compatible tool.
"""
# TODO could use `copy.deepcopy(self.petab_problem)` from PetabMixin?
petab_problem = petab.Problem.from_yaml(str(self.petab_yaml))
+
+ if set_estimated_parameters is None and self.estimated_parameters:
+ set_estimated_parameters = True
+
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]
# Else the parameter is to be fixed.
else:
petab_problem.parameter_df.loc[parameter_id, ESTIMATE] = 0
@@ -466,7 +515,7 @@ def get_hash(self) -> int:
is calibrated twice and the two calibrated models differ in their parameter
estimates, then they will still have the same hash.
- This is not implemented as `__hash__` because Python automatically truncates
+ 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__ ).
@@ -503,8 +552,9 @@ def __str__(self):
return f'{header}\n{data}'
def get_mle(self) -> Dict[str, float]:
- """Get the maximum likelihood estimate of the model.
-
+ """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.
@@ -576,10 +626,10 @@ def get_parameter_values(
) -> List[TYPE_PARAMETER]:
"""Get parameter values.
- Includes `ESTIMATE` for parameters that should be estimated.
+ Includes ``ESTIMATE`` for parameters that should be estimated.
- The ordering is by `parameter_ids` if supplied, else
- `self.petab_parameters`.
+ The ordering is by ``parameter_ids`` if supplied, else
+ ``self.petab_parameters``.
Args:
parameter_ids:
@@ -609,8 +659,8 @@ def default_compare(
) -> bool:
"""Compare two calibrated models by their criterion values.
- It is assumed that the model `model0` provides a value for the criterion
- `criterion`, or is the `VIRTUAL_INITIAL_MODEL`.
+ It is assumed that the model ``model0`` provides a value for the criterion
+ ``criterion``, or is the ``VIRTUAL_INITIAL_MODEL``.
Args:
model0:
@@ -624,8 +674,8 @@ def default_compare(
model. Should be non-negative.
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(
@@ -672,11 +722,11 @@ def models_from_yaml_list(
model_list_yaml:
The path to the PEtab Select list of model YAML file.
petab_problem:
- See `Model.from_dict`.
+ 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 an error will be raised.
+ a 1-element list), if ``True`` then the single model will be read in,
+ else a ``ValueError`` will be raised.
Returns:
A list of model instances, initialized with the provided
diff --git a/petab_select/model_space.py b/petab_select/model_space.py
index 470dbdc6..2b19c53c 100644
--- a/petab_select/model_space.py
+++ b/petab_select/model_space.py
@@ -1,41 +1,34 @@
"""The `ModelSpace` class and related methods."""
-import abc
import itertools
import logging
+import warnings
from pathlib import Path
from tempfile import NamedTemporaryFile
-from typing import (
- Any,
- Callable,
- Iterable,
- List,
- Optional,
- TextIO,
- Union,
- get_args,
-)
+from typing import Any, Iterable, List, Optional, TextIO, Union, get_args
import numpy as np
import pandas as pd
-from more_itertools import nth
from .candidate_space import CandidateSpace
from .constants import (
- ESTIMATE,
HEADER_ROW,
- MODEL_ID,
MODEL_ID_COLUMN,
- MODEL_SPACE_FILE_NON_PARAMETER_COLUMNS,
MODEL_SUBSPACE_ID,
PARAMETER_DEFINITIONS_START,
PARAMETER_VALUE_DELIMITER,
- PETAB_YAML,
PETAB_YAML_COLUMN,
TYPE_PATH,
)
from .model import Model
from .model_subspace import ModelSubspace
+__all__ = [
+ "ModelSpace",
+ "get_model_space_df",
+ "read_model_space_file",
+ "write_model_space_df",
+]
+
def read_model_space_file(filename: str) -> TextIO:
"""Read a model space file.
@@ -49,7 +42,9 @@ def read_model_space_file(filename: str) -> TextIO:
Returns:
A temporary file object, which is the unpacked file.
-
+ """
+ """
+ FIXME(dilpath)
Todo:
* Consider alternatives to `_{n}` suffix for model `modelId`
* How should the selected model be reported to the user? Remove the
@@ -113,13 +108,13 @@ def line2row(
delimiter:
The string that separates columns in the file.
unpacked:
- Whether the line format is in the unpacked format. If False,
- parameter values are not converted to `float`.
+ Whether the line format is in the unpacked format. If ``False``,
+ parameter values are not converted to ``float``.
convert_parameters_to_float:
- Whether parameters should be converted to `float`.
+ Whether parameters should be converted to ``float``.
Returns:
- A list of column values. Parameter values are converted to `float`.
+ A list of column values. Parameter values are converted to ``float``.
"""
columns = line.strip().split(delimiter)
metadata = columns[:PARAMETER_DEFINITIONS_START]
@@ -265,7 +260,7 @@ def search_subspaces(only_one_subspace: bool = False):
def __len__(self):
"""Get the number of models in this space."""
- subspace_coumts = [len(s) for s in self.model_subspaces]
+ subspace_counts = [len(s) for s in self.model_subspaces]
total_count = sum(subspace_counts)
return total_count
diff --git a/petab_select/model_subspace.py b/petab_select/model_subspace.py
index 95efdedd..bfed9596 100644
--- a/petab_select/model_subspace.py
+++ b/petab_select/model_subspace.py
@@ -1,27 +1,20 @@
import math
import warnings
-from itertools import chain, product
+from itertools import product
from pathlib import Path
from typing import Any, Dict, Iterable, Iterator, List, Optional, Union
import numpy as np
import pandas as pd
-import petab
-from more_itertools import one, powerset
-from petab.C import NOMINAL_VALUE
+from more_itertools import powerset
from .candidate_space import CandidateSpace
from .constants import (
- CODE_DELIMITER,
ESTIMATE,
MODEL_SPACE_FILE_NON_PARAMETER_COLUMNS,
- MODEL_SUBSPACE_ID,
PARAMETER_VALUE_DELIMITER,
- PETAB_ESTIMATE_FALSE,
- PETAB_ESTIMATE_TRUE,
PETAB_YAML,
STEPWISE_METHODS,
- TYPE_PARAMETER,
TYPE_PARAMETER_DICT,
TYPE_PARAMETER_OPTIONS,
TYPE_PARAMETER_OPTIONS_DICT,
@@ -33,6 +26,10 @@
from .model import Model
from .petab import PetabMixin
+__all__ = [
+ 'ModelSubspace',
+]
+
class ModelSubspace(PetabMixin):
"""Efficient representation of exponentially large model subspaces.
@@ -45,13 +42,17 @@ class ModelSubspace(PetabMixin):
parameters:
The key is the ID of the parameter. The value is a list of values
that the parameter can take (including `ESTIMATE`).
- #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`).
exclusions:
Hashes of models that have been previously submitted to a candidate space
- for consideration (`CandidateSpace.consider`).
+ 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__(
@@ -100,7 +101,7 @@ def check_compatibility_stepwise_method(
'(e.g. forward or backward). '
f'This model subspace: `{self.model_subspace_id}`. '
'This model subspace PEtab YAML: '
- f'`{self.petab_yaml}`.'
+ f'`{self.petab_yaml}`. '
'The candidate space PEtab YAML: '
f'`{candidate_space.predecessor_model.petab_yaml}`. '
)
@@ -110,7 +111,7 @@ def check_compatibility_stepwise_method(
def get_models(self, estimated_parameters: List[str]) -> Iterator[Model]:
"""Get models in the subspace by estimated parameters.
- All models that have the provided `estimated_parameters` are returned.
+ All models that have the provided ``estimated_parameters`` are returned.
Args:
estimated_parameters:
@@ -119,10 +120,12 @@ def get_models(self, estimated_parameters: List[str]) -> Iterator[Model]:
in the subset of PEtab parameters that exist in the model
subspace definition. Parameters in the PEtab problem but not
the model subspace definition should not be included here.
+
+ FIXME(dilpath)
TODO support the full set of PEtab parameters? Then would need
- to turn off estimation for parameters that are not
- provided in `estimated_parameters` -- maybe unexpected for
- users.
+ to turn off estimation for parameters that are not
+ provided in `estimated_parameters` -- maybe unexpected for
+ users.
Returns:
A list of models.
@@ -185,7 +188,7 @@ def search(
"""Search for candidate models in this model subspace.
Nothing is returned, as the result is managed by the
- `candidate_space`.
+ ``candidate_space``.
Args:
candidate_space:
@@ -233,8 +236,10 @@ def continue_searching(
if candidate_space.predecessor_model == VIRTUAL_INITIAL_MODEL:
if candidate_space.method == Method.FORWARD:
old_estimated_all = set()
+ old_fixed_all = set(self.parameters)
elif candidate_space.method == Method.BACKWARD:
old_estimated_all = set(self.parameters)
+ old_fixed_all = set()
else:
# Should already be handled elsewhere (e.g.
# `self.check_compatibility_stepwise_method`).
@@ -243,10 +248,16 @@ def continue_searching(
)
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
+ ]
# Parameters that are fixed in the candidate space
# predecessor model but are necessarily estimated in this subspace.
@@ -265,6 +276,7 @@ def continue_searching(
# Parameters related to minimal changes compared to the predecessor model.
old_estimated = set(old_estimated_all).intersection(self.can_estimate)
+ old_fixed = set(old_fixed_all).intersection(self.can_fix)
new_must_estimate = set(new_must_estimate_all).intersection(
self.parameters
)
@@ -305,14 +317,14 @@ def continue_searching(
new_must_estimate_all
or candidate_space.predecessor_model == VIRTUAL_INITIAL_MODEL
):
- # Consider minimal models that have all necessary parameters
- # estimated. As this subspace necessarily has more estimated
- # parameters than the predecessor model, all parameters that
- # can be fixed, will be fixed.
+ # Consider minimal models that have all necessarily-estimated
+ # parameters.
estimated_parameters = {
parameter_id: ESTIMATE
- for parameter_id in set(self.parameters).difference(
- self.can_fix
+ for parameter_id in (
+ set(self.parameters)
+ .difference(self.can_fix)
+ .union(old_estimated)
)
}
models = self.get_models(
@@ -395,14 +407,14 @@ def continue_searching(
new_must_fix_all
or candidate_space.predecessor_model == VIRTUAL_INITIAL_MODEL
):
- # Consider minimal models that have all necessarily fixed parameters.
- # As this subspace necessarily has fewer estimated parameters than the
- # predecessor model, all parameters that can be estimated, will be
- # estimated.
+ # Consider minimal models that have all necessarily-fixed
+ # parameters.
estimated_parameters = {
parameter_id: ESTIMATE
- for parameter_id in set(self.parameters).difference(
- self.must_fix
+ for parameter_id in (
+ set(self.parameters)
+ .difference(self.must_fix)
+ .difference(old_fixed)
)
}
models = self.get_models(
@@ -642,8 +654,8 @@ def exclude_model(self, model: Model) -> None:
def exclude_models(self, models: Iterable[Model]) -> None:
"""Exclude models from the model subspace.
- Models are excluded in `ModelSubspace.indices_to_model`, which contains the
- only call to `Model.__init__` in the `ModelSubspace` class.
+ Models are excluded in :meth:`ModelSubspace.indices_to_model`, which contains the
+ only call to :meth:`Model.__init__` in the :class:`ModelSubspace` class.
Args:
models:
@@ -683,7 +695,7 @@ def from_definition(
definition: Union[Dict[str, str], pd.Series],
parent_path: TYPE_PATH = None,
) -> 'ModelSubspace':
- """Create a `ModelSubspace` from a definition.
+ """Create a :class:`ModelSubspace` from a definition.
Args:
model_subspace_id:
@@ -718,13 +730,13 @@ def indices_to_model(self, indices: List[int]) -> Union[Model, None]:
Args:
indices:
- The indices of the lists in the values of the `ModelSubspace.parameters`
+ The indices of the lists in the values of the ``ModelSubspace.parameters``
dictionary, ordered by the keys of this dictionary.
Returns:
A model with the PEtab problem of this subspace and the parameterization
that corresponds to the indices.
- `None`, if the model is excluded from the subspace.
+ ``None``, if the model is excluded from the subspace.
"""
model = Model(
petab_yaml=self.petab_yaml,
@@ -745,7 +757,7 @@ def indices_to_parameters(
Args:
indices:
- See `ModelSubspace.indices_to_model`.
+ See :meth:`ModelSubspace.indices_to_model`.
Returns:
The parameterization that corresponds to the indices.
@@ -796,7 +808,7 @@ def parameters_to_model(
Returns:
A model with the PEtab problem of this subspace and the parameterization.
- `None`, if the model is excluded from the subspace.
+ ``None``, if the model is excluded from the subspace.
"""
indices = self.parameters_to_indices(parameters)
model = self.indices_to_model(indices)
diff --git a/petab_select/petab.py b/petab_select/petab.py
index 46dfb0ac..c827f93d 100644
--- a/petab_select/petab.py
+++ b/petab_select/petab.py
@@ -2,6 +2,7 @@
from typing import List, Optional
import petab
+from more_itertools import one
from petab.C import ESTIMATE, NOMINAL_VALUE
from .constants import PETAB_ESTIMATE_FALSE, TYPE_PARAMETER_DICT, TYPE_PATH
@@ -11,6 +12,16 @@ class PetabMixin:
"""Useful things for classes that contain a PEtab problem.
All attributes/methods are prefixed with `petab_`.
+
+ Attributes:
+ petab_yaml:
+ The location of the PEtab problem YAML file.
+ 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__(
@@ -47,6 +58,11 @@ def __init__(
@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()
@@ -55,6 +71,11 @@ def petab_parameter_ids_estimated(self) -> List[str]:
@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
@@ -64,6 +85,7 @@ def petab_parameter_ids_fixed(self) -> List[str]:
@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
diff --git a/petab_select/problem.py b/petab_select/problem.py
index cc7695dd..7b97fb19 100644
--- a/petab_select/problem.py
+++ b/petab_select/problem.py
@@ -1,7 +1,6 @@
"""The model selection problem class."""
import abc
from functools import partial
-from itertools import chain
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, Optional, Union
@@ -21,6 +20,10 @@
from .model import Model, default_compare
from .model_space import ModelSpace
+__all__ = [
+ 'Problem',
+]
+
class Problem(abc.ABC):
"""Handle everything related to the model selection problem.
@@ -30,12 +33,13 @@ class Problem(abc.ABC):
The model space.
calibrated_models:
Calibrated models. Will be used to augment the model selection problem (e.g.
- by excluding them from the model space). FIXME refactor out
+ by excluding them from the model space).
+ FIXME(dilpath) refactor out
candidate_space_arguments:
Custom options that are used to construct the candidate space.
compare:
A method that compares models by selection criterion. See
- `petab_select.model.default_compare` for an example.
+ :func:`petab_select.model.default_compare` for an example.
criterion:
The criterion used to compare models.
method:
@@ -45,15 +49,19 @@ class Problem(abc.ABC):
yaml_path:
The location of the selection problem YAML file. Used for relative
paths that exist in e.g. the model space files.
- TODO should the relative paths be relative to the YAML or the
- file that contains them?
+ TODO should the relative paths be relative to the YAML or the file that contains them?
+
+ """
+
+ """
+ FIXME(dilpath)
Unsaved attributes:
candidate_space:
The candidate space that will be used.
Reason for not saving:
- Essentially reproducible from `Problem.method` and
- `Problem.calibrated_models`.
+ Essentially reproducible from :attr:`Problem.method` and
+ :attr:`Problem.calibrated_models`.
"""
def __init__(
@@ -64,13 +72,13 @@ def __init__(
criterion: Criterion = None,
method: str = None,
version: str = None,
- yaml_path: str = None,
+ yaml_path: Union[Path, str] = None,
):
self.model_space = model_space
self.criterion = criterion
self.method = method
self.version = version
- self.yaml_path = yaml_path
+ self.yaml_path = Path(yaml_path)
self.candidate_space_arguments = candidate_space_arguments
if self.candidate_space_arguments is None:
@@ -90,7 +98,8 @@ def get_path(self, relative_path: Union[str, Path]) -> Path:
Returns:
The path to the resource.
-
+ """
+ """
TODO:
Unused?
"""
@@ -117,7 +126,7 @@ def exclude_model_hashes(
"""Exclude models from the model space, by model hashes.
Args:
- models:
+ model_hashes:
The model hashes.
"""
self.model_space.exclude_model_hashes(model_hashes)
@@ -200,7 +209,7 @@ def get_best(
The best model will be taken from these models.
criterion:
The criterion by which models will be compared. Defaults to
- `self.criterion` (e.g. as defined in the PEtab Select problem YAML
+ ``self.criterion`` (e.g. as defined in the PEtab Select problem YAML
file).
compute_criterion:
Whether to try computing criterion values, if sufficient
@@ -235,11 +244,11 @@ def new_candidate_space(
*args,
method: Method = None,
**kwargs,
- ) -> None:
+ ) -> CandidateSpace:
"""Construct a new candidate space.
Args:
- *args, **kwargs:
+ args, kwargs:
Arguments are passed to the candidate space constructor.
method:
The model selection method.
diff --git a/petab_select/ui.py b/petab_select/ui.py
index 65b296cd..bdf1ea36 100644
--- a/petab_select/ui.py
+++ b/petab_select/ui.py
@@ -1,22 +1,29 @@
-import csv
-import os.path
+import copy
from pathlib import Path
-from typing import Any, Dict, List, Optional, Tuple, Union
+from typing import Dict, List, Optional, Union
import numpy as np
import petab
-from .candidate_space import CandidateSpace
+from .candidate_space import CandidateSpace, FamosCandidateSpace
from .constants import (
- ESTIMATE,
INITIAL_MODEL_METHODS,
TYPE_PATH,
+ VIRTUAL_INITIAL_MODEL,
Criterion,
Method,
)
from .model import Model, default_compare
from .problem import Problem
+__all__ = [
+ 'candidates',
+ 'model_to_petab',
+ 'models_to_petab',
+ 'best',
+ 'write_summary_tsv',
+]
+
def candidates(
problem: Problem,
@@ -31,8 +38,8 @@ def candidates(
) -> CandidateSpace:
"""Search the model space for candidate models.
- A predecessor model is chosen from `newly_calibrated_models` if available,
- otherwise from `calibrated_models`, and is used for applicable methods.
+ A predecessor model is chosen from ``newly_calibrated_models`` if available,
+ otherwise from ``calibrated_models``, and is used for applicable methods.
Args:
problem:
@@ -84,10 +91,30 @@ def candidates(
candidate_space.exclude_hashes(calibrated_models)
# Set the predecessor model to the previous predecessor model.
+ predecessor_model = candidate_space.previous_predecessor_model
+
+ # If the predecessor model has not yet been calibrated, then calibrate it.
+ if predecessor_model != VIRTUAL_INITIAL_MODEL:
+ if (
+ predecessor_model.get_criterion(
+ criterion,
+ raise_on_failure=False,
+ )
+ is None
+ ):
+ candidate_space.models = [copy.deepcopy(predecessor_model)]
+ # Dummy zero likelihood, which the predecessor model will
+ # improve on after it's actually calibrated.
+ predecessor_model.set_criterion(Criterion.LH, 0.0)
+ return candidate_space
+
+ # Exclude the calibrated predecessor model.
+ if not candidate_space.excluded(predecessor_model):
+ candidate_space.exclude(predecessor_model)
+
# Set the new predecessor_model from the initial model or
# by calling ui.best to find the best model to jump to if
# this is not the first step of the search.
- predecessor_model = candidate_space.previous_predecessor_model
if newly_calibrated_models:
predecessor_model = problem.get_best(
newly_calibrated_models.values(),
@@ -117,7 +144,7 @@ def candidates(
# Else, in case we jumped to most distant in this iteration, go into
# calibration with only the model we've jumped to.
if (
- candidate_space.governing_method == Method.FAMOS
+ isinstance(candidate_space, FamosCandidateSpace)
and candidate_space.jumped_to_most_distant
):
return candidate_space
@@ -142,15 +169,32 @@ def candidates(
problem.model_space.exclude_model_hashes(
model_hashes=excluded_model_hashes
)
- if do_search:
+ while do_search:
problem.model_space.search(candidate_space, limit=limit_sent)
- write_summary_tsv(
- problem=problem,
- candidate_space=candidate_space,
- previous_predecessor_model=candidate_space.previous_predecessor_model,
- predecessor_model=predecessor_model,
- )
+ write_summary_tsv(
+ problem=problem,
+ candidate_space=candidate_space,
+ previous_predecessor_model=candidate_space.previous_predecessor_model,
+ predecessor_model=predecessor_model,
+ )
+
+ if candidate_space.models:
+ break
+
+ # No models were found. Repeat the search with the same candidate space,
+ # if the candidate space is able to switch methods.
+ # N.B.: candidate spaces that switch methods must raise `StopIteration`
+ # when they stop switching.
+ if isinstance(candidate_space, FamosCandidateSpace):
+ try:
+ candidate_space.update_after_calibration(
+ calibrated_models=calibrated_models,
+ newly_calibrated_models={},
+ criterion=criterion,
+ )
+ except StopIteration:
+ break
candidate_space.previous_predecessor_model = predecessor_model
@@ -273,7 +317,7 @@ def write_summary_tsv(
# FIXME remove once MostDistantCandidateSpace exists...
method = candidate_space.method
if (
- candidate_space.governing_method == Method.FAMOS
+ isinstance(candidate_space, FamosCandidateSpace)
and isinstance(candidate_space.predecessor_model, Model)
and candidate_space.predecessor_model.predecessor_model_hash is None
):
diff --git a/petab_select/version.py b/petab_select/version.py
index 1e29a996..e4414d2b 100644
--- a/petab_select/version.py
+++ b/petab_select/version.py
@@ -1,2 +1,2 @@
"""Version of the model selection extension for PEtab."""
-__version__ = '0.1.10'
+__version__ = '0.1.11'
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 311bf079..d97e855d 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,3 +1,4 @@
tox >= 3.12.4
pre-commit >= 2.10.1
flake8 >= 4.0.1
+pytest
diff --git a/setup.py b/setup.py
index 2d91a4c3..7b5c98d4 100644
--- a/setup.py
+++ b/setup.py
@@ -88,17 +88,20 @@ def absolute_links(txt):
'pypesto > 0.2.13',
# 'pypesto @ git+https://github.com/ICB-DCM/pyPESTO.git@develop#egg=pypesto',
],
- #'reports': ['Jinja2'],
- #'combine': ['python-libcombine>=0.2.6'],
- #'doc': [
- # 'sphinx>=3.5.3',
- # 'sphinxcontrib-napoleon>=0.7',
- # 'sphinx-markdown-tables>=0.0.15',
- # 'sphinx-rtd-theme>=0.5.1',
- # 'recommonmark>=0.7.1',
- # 'nbsphinx>=0.8.2',
- # 'm2r>=0.2.1',
- # 'ipython>=7.21.0',
- # ]
+ 'doc': [
+ 'sphinx>=3.5.3,<7',
+ 'sphinxcontrib-napoleon>=0.7',
+ 'sphinx-markdown-tables>=0.0.15',
+ 'sphinx-rtd-theme>=0.5.1',
+ 'recommonmark>=0.7.1',
+ # pin until ubuntu comes with newer pandoc:
+ # /home/docs/checkouts/readthedocs.org/user_builds/petab-select/envs/63/lib/python3.11/site-packages/nbsphinx/__init__.py:1058: RuntimeWarning: You are using an unsupported version of pandoc (2.9.2.1).
+ # Your version must be at least (2.14.2) but less than (4.0.0).
+ 'nbsphinx==0.9.1',
+ 'nbconvert<7.5.0',
+ 'ipython>=7.21.0',
+ 'readthedocs-sphinx-ext @ git+https://github.com/readthedocs/readthedocs-sphinx-ext',
+ 'sphinx-autodoc-typehints',
+ ],
},
)
diff --git a/test/candidate_space/test_candidate_space.py b/test/candidate_space/test_candidate_space.py
index 9a993ef6..523f42d7 100644
--- a/test/candidate_space/test_candidate_space.py
+++ b/test/candidate_space/test_candidate_space.py
@@ -5,9 +5,13 @@
from more_itertools import one
import petab_select
-from petab_select.candidate_space import ( # BackwardCandidateSpace,; BruteForceCandidateSpace,; ForwardCandidateSpace,; ForwardAndBackwardCandidateSpace,; LateralCandidateSpace,
- BidirectionalCandidateSpace,
-)
+
+# from petab_select.candidate_space import (
+# BackwardCandidateSpace,
+# BruteForceCandidateSpace,
+# ForwardCandidateSpace,
+# LateralCandidateSpace,
+# )
from petab_select.constants import (
ESTIMATE,
MODEL_SUBSPACE_ID,
@@ -107,129 +111,3 @@ def model_space(calibrated_model_space) -> pd.DataFrame:
df = get_model_space_df(df)
model_space = ModelSpace.from_df(df)
return model_space
-
-
-def test_bidirectional(
- model_space, calibrated_model_space, ordered_model_parameterizations
-):
- criterion = Criterion.AIC
- model_id_length = one(
- set([len(model_id) for model_id in calibrated_model_space])
- )
-
- candidate_space = BidirectionalCandidateSpace()
- calibrated_models = []
-
- # Perform bidirectional search until no more models are found.
- search_iterations = 0
- while True:
- new_calibrated_models = []
- search_iterations += 1
-
- # Get models.
- model_space.search(candidate_space)
-
- # Calibrate models.
- for model in candidate_space.models:
- model_id = model.model_subspace_id[-model_id_length:]
- model.set_criterion(
- criterion=criterion, value=calibrated_model_space[model_id]
- )
- new_calibrated_models.append(model)
-
- # End if no more models were found.
- if not new_calibrated_models:
- break
-
- # Get next predecessor model as best of new models.
- best_new_model = None
- for model in new_calibrated_models:
- if best_new_model is None:
- best_new_model = model
- continue
- if default_compare(
- model0=best_new_model, model1=model, criterion=criterion
- ):
- best_new_model = model
-
- # Set next predecessor and exclusions.
- calibrated_model_hashes = [
- model.get_hash() for model in calibrated_models
- ]
- candidate_space.reset(
- predecessor_model=best_new_model,
- exclusions=calibrated_model_hashes,
- )
-
- # exclude calibrated model hashes from model space too?
- model_space.exclude_model_hashes(model_hashes=calibrated_model_hashes)
-
- # Check that expected models are found at each iteration.
- (
- good_model_parameterizations_ascending,
- bad_model_parameterizations,
- ) = ordered_model_parameterizations
- search_iteration = 0
- for history_item in candidate_space.method_history:
- models = history_item[MODELS]
- if not models:
- continue
- model_parameterizations = [
- model.model_subspace_id[-5:] for model in models
- ]
-
- good_model_parameterization = good_model_parameterizations_ascending[
- search_iteration
- ]
- # The expected good model was found.
- assert good_model_parameterization in model_parameterizations
- model_parameterizations.remove(good_model_parameterization)
-
- if search_iteration == 0:
- # All parameterizations have been correctly identified and removed.
- assert not model_parameterizations
- search_iteration += 1
- continue
-
- previous_model_parameterization = (
- good_model_parameterizations_ascending[search_iteration - 1]
- )
-
- # The expected bad model is also found.
- # If a bad model is the same dimension and also represents a similar stepwise move away from the previous
- # model parameterization, it should also be in the parameterizations.
- for bad_model_parameterization in bad_model_parameterizations:
- # Skip if different dimensions
- if sum(map(int, bad_model_parameterization)) != sum(
- map(int, good_model_parameterization)
- ):
- continue
- # Skip if different distances from previous model parameterization
- if sum(
- [
- a != b
- for a, b in zip(
- bad_model_parameterization,
- previous_model_parameterization,
- )
- ]
- ) != sum(
- [
- a != b
- for a, b in zip(
- good_model_parameterization,
- previous_model_parameterization,
- )
- ]
- ):
- continue
- assert bad_model_parameterization in model_parameterizations
- model_parameterizations.remove(bad_model_parameterization)
-
- # All parameterizations have been correctly identified and removed.
- assert not model_parameterizations
- search_iteration += 1
-
- # End test if all good models were found in the correct order.
- if search_iteration >= len(good_model_parameterizations_ascending):
- break
diff --git a/test/candidate_space/test_famos.py b/test/candidate_space/test_famos.py
index b0dde6f2..3e1efb37 100644
--- a/test/candidate_space/test_famos.py
+++ b/test/candidate_space/test_famos.py
@@ -76,6 +76,7 @@ def expected_progress_list():
]
+@pytest.mark.skip(reason="FIXME")
def test_famos(
petab_select_problem,
expected_criterion_values,
diff --git a/test/cli/expected_output/model/parameters.tsv b/test/cli/expected_output/model/parameters.tsv
index 24f7485d..15b3ec6e 100644
--- a/test/cli/expected_output/model/parameters.tsv
+++ b/test/cli/expected_output/model/parameters.tsv
@@ -1,6 +1,6 @@
parameterId parameterName parameterScale lowerBound upperBound nominalValue estimate
k1 k_{1} lin 0.0 1000.0 0.2 0
-k2 k_{2} lin 0.0 1000.0 0.1 1
+k2 k_{2} lin 0.0 1000.0 0.15 1
k3 k_{3} lin 0.0 1000.0 0.0 1
k4 k_{4} lin 0.0 1000.0 0.0 0
k5 k_{5} lin 0.0 1000.0 0.0 0
diff --git a/test/cli/expected_output/models/model_1/parameters.tsv b/test/cli/expected_output/models/model_1/parameters.tsv
index 24f7485d..15b3ec6e 100644
--- a/test/cli/expected_output/models/model_1/parameters.tsv
+++ b/test/cli/expected_output/models/model_1/parameters.tsv
@@ -1,6 +1,6 @@
parameterId parameterName parameterScale lowerBound upperBound nominalValue estimate
k1 k_{1} lin 0.0 1000.0 0.2 0
-k2 k_{2} lin 0.0 1000.0 0.1 1
+k2 k_{2} lin 0.0 1000.0 0.15 1
k3 k_{3} lin 0.0 1000.0 0.0 1
k4 k_{4} lin 0.0 1000.0 0.0 0
k5 k_{5} lin 0.0 1000.0 0.0 0
diff --git a/test/model/expected_output/petab/parameters.tsv b/test/model/expected_output/petab/parameters.tsv
index 24f7485d..15b3ec6e 100644
--- a/test/model/expected_output/petab/parameters.tsv
+++ b/test/model/expected_output/petab/parameters.tsv
@@ -1,6 +1,6 @@
parameterId parameterName parameterScale lowerBound upperBound nominalValue estimate
k1 k_{1} lin 0.0 1000.0 0.2 0
-k2 k_{2} lin 0.0 1000.0 0.1 1
+k2 k_{2} lin 0.0 1000.0 0.15 1
k3 k_{3} lin 0.0 1000.0 0.0 1
k4 k_{4} lin 0.0 1000.0 0.0 0
k5 k_{5} lin 0.0 1000.0 0.0 0
diff --git a/test/pypesto/test_pypesto.py b/test/pypesto/test_pypesto.py
index 3432c225..04da0da5 100644
--- a/test/pypesto/test_pypesto.py
+++ b/test/pypesto/test_pypesto.py
@@ -46,6 +46,7 @@ def objective_customizer(obj):
obj.amici_solver.setRelativeTolerance(1e-12)
+@pytest.mark.skip(reason="FIXME")
def test_pypesto():
for test_case_path in test_cases_path.glob('*'):
if test_cases and test_case_path.stem not in test_cases: