certaraiq 2.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- certaraiq-2.2.3/PKG-INFO +55 -0
- certaraiq-2.2.3/README.md +29 -0
- certaraiq-2.2.3/pyproject.toml +116 -0
- certaraiq-2.2.3/src/certaraiq/__init__.py +8 -0
- certaraiq-2.2.3/src/certaraiq/_exports.py +8 -0
- certaraiq-2.2.3/src/certaraiq/_helper/__init__.py +0 -0
- certaraiq-2.2.3/src/certaraiq/_helper/argument_parser.py +157 -0
- certaraiq-2.2.3/src/certaraiq/_helper/covariance_matrix.py +174 -0
- certaraiq-2.2.3/src/certaraiq/_helper/create_map.py +180 -0
- certaraiq-2.2.3/src/certaraiq/_helper/display.py +24 -0
- certaraiq-2.2.3/src/certaraiq/_helper/get_dose_schedules.py +32 -0
- certaraiq-2.2.3/src/certaraiq/_helper/get_model.py +107 -0
- certaraiq-2.2.3/src/certaraiq/_helper/get_table.py +70 -0
- certaraiq-2.2.3/src/certaraiq/_helper/helper.py +189 -0
- certaraiq-2.2.3/src/certaraiq/_helper/initialize_parameter_table.py +25 -0
- certaraiq-2.2.3/src/certaraiq/_helper/nested_map.py +64 -0
- certaraiq-2.2.3/src/certaraiq/_helper/parameter.py +16 -0
- certaraiq-2.2.3/src/certaraiq/_helper/parse_output_times.py +48 -0
- certaraiq-2.2.3/src/certaraiq/_helper/parse_schedule.py +131 -0
- certaraiq-2.2.3/src/certaraiq/_helper/parser.py +84 -0
- certaraiq-2.2.3/src/certaraiq/_helper/posterior_sample_list_diagnostics.py +28 -0
- certaraiq-2.2.3/src/certaraiq/_helper/profile_likelihood.py +286 -0
- certaraiq-2.2.3/src/certaraiq/_helper/validate_columns.py +207 -0
- certaraiq-2.2.3/src/certaraiq/_labelset.py +75 -0
- certaraiq-2.2.3/src/certaraiq/_optimize.py +1152 -0
- certaraiq-2.2.3/src/certaraiq/_scan.py +108 -0
- certaraiq-2.2.3/src/certaraiq/_scan_ast.py +261 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/__init__.py +3 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/array.py +74 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/client_interface.py +449 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/contract.py +54 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/data_frame.py +248 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/data_pipe.py +304 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/data_pipe_builder.py +346 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/data_pipe_result.py +35 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/data_type.py +137 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/data_type_parser.py +23 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/dataclass_helpers.py +19 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/distribution_sample.py +152 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/exceptions.py +15 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/expression.py +5 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/job.py +120 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/legacy.py +294 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/__init__.py +30 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/allen.py +65 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/base.py +22 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/__init__.py +35 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/base.py +23 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/distributions.py +36 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/histogram.py +46 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/loguniform.py +46 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/multivariate_lognormal.py +45 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/multivariate_normal.py +45 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/uniform.py +46 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/measurements.py +75 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_fisher_information_matrix.py +15 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_gradient.py +17 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_measurement_likelihood_sample.py +16 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_model.py +81 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_model_reference.py +39 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_optimization.py +40 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_optimization_batch.py +39 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_optimization_configuration.py +78 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_optimization_result.py +21 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_parameter_posterior_sample.py +43 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_posterior_sample_configuration.py +36 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_prediction.py +22 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_proposal_population_sample.py +45 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_residual.py +23 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_residual_batch.py +26 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_simulation.py +74 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_simulation_batch.py +47 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_value.py +26 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/ode_virtual_population_sample.py +42 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/progress.py +57 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/__init__.py +2 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/from_bytes.py +20 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/get_all_subclasses.py +14 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/indexing.py +34 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/metadata.py +194 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/model.py +558 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/simulation_configuration.py +16 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/reaction_model.py +308 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/scenario.py +96 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/solver_configuration.py +99 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/status.py +9 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/time_course.py +32 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/times.py +35 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/to_latex_ode_model.py +53 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/to_matlab_ode_simulation.py +55 -0
- certaraiq-2.2.3/src/certaraiq/_sdk/to_simbiology_ode_simulation.py +55 -0
- certaraiq-2.2.3/src/certaraiq/_simulate.py +599 -0
- certaraiq-2.2.3/src/certaraiq/_units/ast.py +139 -0
- certaraiq-2.2.3/src/certaraiq/_units/comparison.py +15 -0
- certaraiq-2.2.3/src/certaraiq/_units/deparser.py +64 -0
- certaraiq-2.2.3/src/certaraiq/_units/parser.py +58 -0
- certaraiq-2.2.3/src/certaraiq/_units/to_pint.py +52 -0
- certaraiq-2.2.3/src/certaraiq/_units/valid_units.py +64 -0
- certaraiq-2.2.3/src/certaraiq/_vpop.py +658 -0
certaraiq-2.2.3/PKG-INFO
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: certaraiq
|
|
3
|
+
Version: 2.2.3
|
|
4
|
+
Summary: A Python client for the Certara IQ platform
|
|
5
|
+
Author: Certara IQ Support
|
|
6
|
+
Author-email: Certara IQ Support <iq-support@certara.com>
|
|
7
|
+
License: Proprietary
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
11
|
+
Classifier: Operating System :: POSIX
|
|
12
|
+
Requires-Dist: httpx>=0.28
|
|
13
|
+
Requires-Dist: ipython>=9.10
|
|
14
|
+
Requires-Dist: numpy>=2.4,<3
|
|
15
|
+
Requires-Dist: ordered-set>=4.1,<5
|
|
16
|
+
Requires-Dist: pandas>=2.3,<3
|
|
17
|
+
Requires-Dist: parsita>=2.2,<2.3
|
|
18
|
+
Requires-Dist: pint>=0.25,<0.26
|
|
19
|
+
Requires-Dist: prettytable>=3.17,<4
|
|
20
|
+
Requires-Dist: scipy>=1.17,<2
|
|
21
|
+
Requires-Dist: serialite>=0.5,<0.6
|
|
22
|
+
Requires-Dist: tabeline[pandas,polars]>=0.6,<0.7
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Project-URL: Repository, https://github.com/certara/certara-iq-v2
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Certara IQ Python client
|
|
28
|
+
|
|
29
|
+
This is a Python client for the Certara IQ platform. It is useless alone and must be
|
|
30
|
+
installed in an environment with a valid access token to a deployed Certara IQ server.
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
The client searches the following paths for a configuration file:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
${XDG_CONFIG_HOME:-~/.config}/certaraiq/client.toml
|
|
38
|
+
|
|
39
|
+
/etc/certaraiq/client.toml
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Example configuration file:
|
|
43
|
+
|
|
44
|
+
```toml
|
|
45
|
+
api_origin = 'https://dev01.dev-abm.com'
|
|
46
|
+
auth_token_path = '/home/jovyan/.jupyterhub/services-blue/auth_token'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `api_origin`: The protocol and host for the Certara IQ server API. All requests will be made against this API. By
|
|
50
|
+
default, the client will request against `http://localhost:5100`
|
|
51
|
+
- `auth_token_path`: Path to a file containing a bearer token that will be used to authenticate
|
|
52
|
+
requests. By default, the client will attempt requests with no authentication.
|
|
53
|
+
|
|
54
|
+
The search stops at the first configuration file found. If no configuration file is found, the defaults for
|
|
55
|
+
all values are used.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Certara IQ Python client
|
|
2
|
+
|
|
3
|
+
This is a Python client for the Certara IQ platform. It is useless alone and must be
|
|
4
|
+
installed in an environment with a valid access token to a deployed Certara IQ server.
|
|
5
|
+
|
|
6
|
+
## Configuration
|
|
7
|
+
|
|
8
|
+
The client searches the following paths for a configuration file:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
${XDG_CONFIG_HOME:-~/.config}/certaraiq/client.toml
|
|
12
|
+
|
|
13
|
+
/etc/certaraiq/client.toml
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Example configuration file:
|
|
17
|
+
|
|
18
|
+
```toml
|
|
19
|
+
api_origin = 'https://dev01.dev-abm.com'
|
|
20
|
+
auth_token_path = '/home/jovyan/.jupyterhub/services-blue/auth_token'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `api_origin`: The protocol and host for the Certara IQ server API. All requests will be made against this API. By
|
|
24
|
+
default, the client will request against `http://localhost:5100`
|
|
25
|
+
- `auth_token_path`: Path to a file containing a bearer token that will be used to authenticate
|
|
26
|
+
requests. By default, the client will attempt requests with no authentication.
|
|
27
|
+
|
|
28
|
+
The search stops at the first configuration file found. If no configuration file is found, the defaults for
|
|
29
|
+
all values are used.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "certaraiq"
|
|
3
|
+
version = "2.2.3"
|
|
4
|
+
description = "A Python client for the Certara IQ platform"
|
|
5
|
+
authors = [{ name = "Certara IQ Support", email = "iq-support@certara.com" }]
|
|
6
|
+
license = { text = "Proprietary" }
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 5 - Production/Stable",
|
|
11
|
+
"Intended Audience :: Science/Research",
|
|
12
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
13
|
+
"Operating System :: POSIX",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"httpx >=0.28",
|
|
17
|
+
"ipython >=9.10",
|
|
18
|
+
"numpy >=2.4,<3",
|
|
19
|
+
"ordered-set >=4.1,<5",
|
|
20
|
+
"pandas >=2.3,<3",
|
|
21
|
+
"parsita >=2.2,<2.3",
|
|
22
|
+
"pint >=0.25,<0.26",
|
|
23
|
+
"prettytable >=3.17,<4",
|
|
24
|
+
"scipy >=1.17,<2",
|
|
25
|
+
"serialite >=0.5,<0.6",
|
|
26
|
+
"tabeline[pandas,polars] >=0.6,<0.7",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Repository = "https://github.com/certara/certara-iq-v2"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"nox >=2026.2.9",
|
|
35
|
+
"nox-uv >=0.7.1",
|
|
36
|
+
|
|
37
|
+
# Test
|
|
38
|
+
"coverage[toml] >=7.13",
|
|
39
|
+
"filelock >= 3.25",
|
|
40
|
+
"pytest >=9.0",
|
|
41
|
+
"pytest-cov >=7.0",
|
|
42
|
+
"pytest-xdist >=3.8",
|
|
43
|
+
|
|
44
|
+
# Docs
|
|
45
|
+
"mkdocs >=1.6",
|
|
46
|
+
"mkdocs-material >=9.7",
|
|
47
|
+
"mkdocstrings[python] >=1.0",
|
|
48
|
+
"pygments >=2.19",
|
|
49
|
+
"pytkdocs[numpy-style] >=0.16",
|
|
50
|
+
|
|
51
|
+
# Linting and formatting
|
|
52
|
+
"ruff >=0.15",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[build-system]
|
|
56
|
+
requires = ["uv-build"]
|
|
57
|
+
build-backend = "uv_build"
|
|
58
|
+
|
|
59
|
+
[tool.uv]
|
|
60
|
+
python-preference = "only-managed"
|
|
61
|
+
|
|
62
|
+
[tool.coverage.run]
|
|
63
|
+
branch = true
|
|
64
|
+
|
|
65
|
+
[tool.coverage.report]
|
|
66
|
+
exclude_lines = [
|
|
67
|
+
"pragma: no cover",
|
|
68
|
+
"raise NotImplementedError",
|
|
69
|
+
"@(abc\\.)?abstractmethod",
|
|
70
|
+
"if TYPE_CHECKING:",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.coverage.paths]
|
|
74
|
+
source = [
|
|
75
|
+
"src/",
|
|
76
|
+
".nox/test*/lib/python*/site-packages/",
|
|
77
|
+
".nox/test*/Lib/site-packages/",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.ruff]
|
|
81
|
+
src = ["src"]
|
|
82
|
+
line-length = 120
|
|
83
|
+
target-version = "py312"
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint]
|
|
86
|
+
extend-select = [
|
|
87
|
+
"I", # isort
|
|
88
|
+
"N", # pep8-naming
|
|
89
|
+
"RUF", # ruff
|
|
90
|
+
"B", # flake8-bugbear
|
|
91
|
+
"C4", # flake8-comprehensions
|
|
92
|
+
"PIE", # flake8-pie
|
|
93
|
+
"PT", # flake8-pytest-style
|
|
94
|
+
"PTH", # flake8-use-pathlib
|
|
95
|
+
"ERA", # flake8-eradicate
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
# B006, B008 and RUF009: disable function calls and mutable data structures in defaults
|
|
99
|
+
# N802, N803, N806: Uppercase variables are fine in science
|
|
100
|
+
# RUF003: Backticks are fine in docstrings
|
|
101
|
+
# ERA001: Too many false positives about commented-out code
|
|
102
|
+
extend-ignore = [
|
|
103
|
+
"B006",
|
|
104
|
+
"B008",
|
|
105
|
+
"RUF009",
|
|
106
|
+
"N802",
|
|
107
|
+
"N803",
|
|
108
|
+
"N806",
|
|
109
|
+
"RUF003",
|
|
110
|
+
"ERA001",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
[tool.ruff.lint.per-file-ignores]
|
|
114
|
+
# Allow unused imports and star imports in __init__.py files
|
|
115
|
+
"__init__.py" = ["F401", "F403"]
|
|
116
|
+
"./src/certaraiq/_units/*.py" = ["F403", "F405"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from ._exports import *
|
|
2
|
+
from ._helper.helper import linspace, logspace
|
|
3
|
+
from ._helper.initialize_parameter_table import initialize_parameter_table
|
|
4
|
+
from ._optimize import optimize
|
|
5
|
+
from ._scan import fold_scan, value_scan
|
|
6
|
+
from ._sdk.data_pipe_builder import concatenate_columns, concatenate_rows
|
|
7
|
+
from ._simulate import simulate
|
|
8
|
+
from ._vpop import virtual_population
|
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from inspect import Parameter
|
|
3
|
+
from typing import Any, Generic
|
|
4
|
+
|
|
5
|
+
from parsita import Parser, lit, reg
|
|
6
|
+
from parsita.state import Continue, Output, Reader, State
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class ArgumentParserParameter(Generic[Output]):
|
|
11
|
+
name: str
|
|
12
|
+
parser: Parser[str, Output]
|
|
13
|
+
default: Output = Parameter.empty
|
|
14
|
+
|
|
15
|
+
def __repr__(self):
|
|
16
|
+
if self.default == Parameter.empty:
|
|
17
|
+
return f"{self.__class__.__name__}({self.name}, {self.parser.name_or_repr()})"
|
|
18
|
+
else:
|
|
19
|
+
return f"{self.__class__.__name__}({self.name}, {self.parser.name_or_repr()}, {self.default})"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ArgumentParser(Parser[str, dict[str, Any]]):
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
positional_only: list[ArgumentParserParameter] = [],
|
|
26
|
+
keyword_only: list[ArgumentParserParameter] = [],
|
|
27
|
+
*,
|
|
28
|
+
keyword: Parser = reg(r"[A-Za-z_][A-Za-z_0-9]*"),
|
|
29
|
+
separator: Parser = lit(","),
|
|
30
|
+
equals: Parser = lit("="),
|
|
31
|
+
):
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.positional_only = positional_only
|
|
34
|
+
self.keyword_only = keyword_only
|
|
35
|
+
self.separator = separator
|
|
36
|
+
self.equals = equals
|
|
37
|
+
self.keyword = keyword
|
|
38
|
+
self.keyword_only_by_name = {parameter.name: parameter for parameter in keyword_only}
|
|
39
|
+
|
|
40
|
+
def _consume(self, state: State, reader: Reader[str]):
|
|
41
|
+
output_positional = {}
|
|
42
|
+
output_keyword = {}
|
|
43
|
+
remainder = reader
|
|
44
|
+
|
|
45
|
+
# no argument
|
|
46
|
+
if len(self.keyword_only) == 0 and len(self.positional_only) == 0:
|
|
47
|
+
return Continue(remainder, {})
|
|
48
|
+
|
|
49
|
+
# positional arguments
|
|
50
|
+
for param in self.positional_only:
|
|
51
|
+
status = param.parser.consume(state, remainder)
|
|
52
|
+
if isinstance(status, Continue):
|
|
53
|
+
remainder = status.remainder
|
|
54
|
+
else:
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
output_positional[param.name] = status.value
|
|
58
|
+
|
|
59
|
+
status = self.separator.consume(state, remainder)
|
|
60
|
+
if isinstance(status, Continue):
|
|
61
|
+
remainder = status.remainder
|
|
62
|
+
else:
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
# keyword arguments
|
|
66
|
+
if len(self.keyword_only) > 0:
|
|
67
|
+
while True:
|
|
68
|
+
# if keywords are following by positional, the comma after positional is not optional anymore
|
|
69
|
+
if len(self.positional_only) > 0 and status is None:
|
|
70
|
+
break
|
|
71
|
+
status = self.keyword.consume(state, remainder)
|
|
72
|
+
if isinstance(status, Continue):
|
|
73
|
+
keyword_name = status.value
|
|
74
|
+
if keyword_name not in self.keyword_only_by_name.keys():
|
|
75
|
+
break
|
|
76
|
+
remainder = status.remainder
|
|
77
|
+
else:
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
status = self.equals.consume(state, remainder)
|
|
81
|
+
if isinstance(status, Continue):
|
|
82
|
+
remainder = status.remainder
|
|
83
|
+
else:
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
status = self.keyword_only_by_name[keyword_name].parser.consume(state, remainder)
|
|
87
|
+
if isinstance(status, Continue):
|
|
88
|
+
remainder = status.remainder
|
|
89
|
+
else:
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
output_keyword[keyword_name] = status.value
|
|
93
|
+
|
|
94
|
+
status = self.separator.consume(state, remainder)
|
|
95
|
+
if isinstance(status, Continue):
|
|
96
|
+
remainder = status.remainder
|
|
97
|
+
else:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
must_positional_argument = {
|
|
101
|
+
parameter.name for parameter in self.positional_only if parameter.default is Parameter.empty
|
|
102
|
+
}
|
|
103
|
+
provided_positional_argument = set(output_positional.keys())
|
|
104
|
+
|
|
105
|
+
must_keyword_arguments = {
|
|
106
|
+
parameter.name for parameter in self.keyword_only if parameter.default is Parameter.empty
|
|
107
|
+
}
|
|
108
|
+
provided_keyword_arguments = set(output_keyword.keys())
|
|
109
|
+
|
|
110
|
+
if not must_positional_argument.issubset(provided_positional_argument):
|
|
111
|
+
state.register_failure(
|
|
112
|
+
f"{must_positional_argument} positional arguments but {provided_positional_argument} are provided",
|
|
113
|
+
reader,
|
|
114
|
+
)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
if not must_keyword_arguments.issubset(provided_keyword_arguments):
|
|
118
|
+
state.register_failure(
|
|
119
|
+
f"{must_keyword_arguments} keyword arguments but {provided_keyword_arguments} are provided", reader
|
|
120
|
+
)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
for i, param in enumerate(self.positional_only):
|
|
124
|
+
if param.name not in provided_positional_argument:
|
|
125
|
+
output_positional[param] = self.positional_only[i].default
|
|
126
|
+
|
|
127
|
+
for key in self.keyword_only_by_name.keys():
|
|
128
|
+
if key not in provided_keyword_arguments:
|
|
129
|
+
output_keyword[key] = self.keyword_only_by_name[key].default
|
|
130
|
+
|
|
131
|
+
output = output_positional | output_keyword
|
|
132
|
+
|
|
133
|
+
return Continue(remainder, output)
|
|
134
|
+
|
|
135
|
+
def __repr__(self):
|
|
136
|
+
keyword_list = [param.__repr__() for param in self.keyword_only]
|
|
137
|
+
positional_list = [param.__repr__() for param in self.positional_only]
|
|
138
|
+
keyword_arg_strings = f"keyword_only=[{', '.join(keyword_list)}]" if len(keyword_list) > 0 else ""
|
|
139
|
+
positional_arg_strings = f"positional_only=[{', '.join(positional_list)}]" if len(positional_list) > 0 else ""
|
|
140
|
+
return self.name_or_nothing() + f"arguments({','.join([keyword_arg_strings, positional_arg_strings])})"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def arguments(
|
|
144
|
+
positional_only: list[ArgumentParserParameter] = [],
|
|
145
|
+
keyword_only: list[ArgumentParserParameter] = [],
|
|
146
|
+
*,
|
|
147
|
+
keyword: Parser = reg(r"[A-Za-z_][A-Za-z_0-9]*"),
|
|
148
|
+
separator: Parser = lit(","),
|
|
149
|
+
equals: Parser = lit("="),
|
|
150
|
+
):
|
|
151
|
+
return ArgumentParser(
|
|
152
|
+
positional_only,
|
|
153
|
+
keyword_only,
|
|
154
|
+
keyword=keyword,
|
|
155
|
+
separator=separator,
|
|
156
|
+
equals=equals,
|
|
157
|
+
)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
__all__ = ["AnnotatedFisherRow", "confidence_intervals"]
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .._sdk.data_frame import DataFrame
|
|
8
|
+
from .._sdk.ode_optimization_result import UnittedValue
|
|
9
|
+
from .._sdk.scenario import Prior
|
|
10
|
+
from .helper import scale_parameter, unscale_parameter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
14
|
+
class AnnotatedFisherRow:
|
|
15
|
+
"""A row of the Fisher information matrix along with the corresponding parameter value and prior."""
|
|
16
|
+
|
|
17
|
+
parameter_value: float
|
|
18
|
+
parameter_prior: Prior
|
|
19
|
+
values: dict[str, UnittedValue]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _theta(annotated_matrix: dict[str, AnnotatedFisherRow]) -> np.ndarray:
|
|
23
|
+
return np.asarray([row.parameter_value for row in annotated_matrix.values()])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _theta_is_logscale(annotated_matrix: dict[str, AnnotatedFisherRow]) -> list[bool]:
|
|
27
|
+
return [row.parameter_prior.is_logscaled for row in annotated_matrix.values()]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _information_unit(annotated_matrix: dict[str, AnnotatedFisherRow]):
|
|
31
|
+
return np.asarray([[val.unit for val in row.values.values()] for row in annotated_matrix.values()])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _information_matrix(annotated_matrix: dict[str, AnnotatedFisherRow]) -> list[list[float]]:
|
|
35
|
+
return [[val.value for val in row.values.values()] for row in annotated_matrix.values()]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def covariance_matrix(
|
|
39
|
+
annotated_fisher_information: dict[str, AnnotatedFisherRow],
|
|
40
|
+
uncertainty_ceiling: float = float("inf"),
|
|
41
|
+
) -> list[list[float]]:
|
|
42
|
+
"""Transform information matrix into covariance intervals.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
fisher_information: dict[str, AnnotatedFisherRow]
|
|
47
|
+
uncertainty_ceiling : float, default = inf
|
|
48
|
+
If some parameters are numerically non-identifiable, the information
|
|
49
|
+
on those parameters is very low and the uncertainties very high. In
|
|
50
|
+
this situation, inverting the information matrix is numerically
|
|
51
|
+
unstable, which can result in a bunch of junk for the covariance
|
|
52
|
+
matrix. A value like 1e8 is a good
|
|
53
|
+
number to try if the scenario appears to be non-identifiable.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
List[List[float]]
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
eigenvalue_threshold = 1.0 / uncertainty_ceiling**2
|
|
61
|
+
|
|
62
|
+
information_matrix_values = np.asarray(_information_matrix(annotated_fisher_information))
|
|
63
|
+
theta = _theta(annotated_fisher_information)
|
|
64
|
+
theta_is_logscale = _theta_is_logscale(annotated_fisher_information)
|
|
65
|
+
theta_array = np.where(theta_is_logscale, theta, 1.0)
|
|
66
|
+
|
|
67
|
+
information_matrix = np.einsum("i,ij,j->ij", theta_array, information_matrix_values, theta_array)
|
|
68
|
+
|
|
69
|
+
modified_covariance = fisher_information_inverse(information_matrix, eigenvalue_threshold)
|
|
70
|
+
|
|
71
|
+
return modified_covariance
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def confidence_intervals(
|
|
75
|
+
annotated_fisher_information: dict[str, AnnotatedFisherRow],
|
|
76
|
+
fraction: float,
|
|
77
|
+
uncertainty_ceiling: float = float("inf"),
|
|
78
|
+
) -> DataFrame:
|
|
79
|
+
"""Transform information matrix into confidence intervals.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
annotated_fisher_information: dict[str, AnnotatedFisherRow]
|
|
84
|
+
fraction : float
|
|
85
|
+
The fraction of the distribution to contain within the CI. Use 0.95
|
|
86
|
+
for 95% confidence intervals.
|
|
87
|
+
uncertainty_ceiling : float, default = inf
|
|
88
|
+
If some parameters are numerically non-identifiable, the information
|
|
89
|
+
on those parameters is very low and the uncertainties very high. In
|
|
90
|
+
this situation, inverting the information matrix is numerically
|
|
91
|
+
unstable, which can result in a bunch of junk for the confidence
|
|
92
|
+
intervals. Choosing a ceiling will prevent uncertainties from going
|
|
93
|
+
larger than that, which can prevent large uncertainties from
|
|
94
|
+
polluting the rest of the uncertainties. A value like 1e8 is a good
|
|
95
|
+
number to try if the scenario appears to be non-identifiable.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
DataFrame with columns:
|
|
100
|
+
parameter: str
|
|
101
|
+
value: float
|
|
102
|
+
unit: str
|
|
103
|
+
scale: "linear" | "log"
|
|
104
|
+
lower: float
|
|
105
|
+
upper: float
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
from scipy import stats
|
|
109
|
+
|
|
110
|
+
if uncertainty_ceiling <= 0.0:
|
|
111
|
+
raise ValueError("Argument 'uncertainty_ceiling' must be positive.")
|
|
112
|
+
|
|
113
|
+
modified_covariance = covariance_matrix(annotated_fisher_information, uncertainty_ceiling)
|
|
114
|
+
|
|
115
|
+
sigma = np.sqrt(np.diag(modified_covariance))
|
|
116
|
+
lower = []
|
|
117
|
+
upper = []
|
|
118
|
+
theta = _theta(annotated_fisher_information)
|
|
119
|
+
theta_is_logscale = _theta_is_logscale(annotated_fisher_information)
|
|
120
|
+
|
|
121
|
+
for t, maybe_scaled_sigma, is_log in zip(theta, sigma, theta_is_logscale, strict=True):
|
|
122
|
+
scaled_mean = scale_parameter(is_log, t)
|
|
123
|
+
scaled_lower, scaled_upper = stats.norm.interval(fraction, scaled_mean, maybe_scaled_sigma)
|
|
124
|
+
lower.append(unscale_parameter(is_log, scaled_lower))
|
|
125
|
+
upper.append(unscale_parameter(is_log, scaled_upper))
|
|
126
|
+
|
|
127
|
+
# These units are ugly and also different (in terms of formatting) from what _optimize returns. This
|
|
128
|
+
# should be fixed in the future when Renan's work for diagnostics is in.
|
|
129
|
+
parameter_units = [f"(1/({unit}))^0.5" for unit in np.diag(_information_unit(annotated_fisher_information))]
|
|
130
|
+
|
|
131
|
+
return DataFrame(
|
|
132
|
+
parameter=list(annotated_fisher_information.keys()),
|
|
133
|
+
value=theta.tolist(),
|
|
134
|
+
unit=parameter_units,
|
|
135
|
+
scale=["log" if is_log else "linear" for is_log in theta_is_logscale],
|
|
136
|
+
lower=lower,
|
|
137
|
+
upper=upper,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def fisher_information_inverse(F: np.ndarray, eigenvalue_threshold: float = 0.0) -> np.ndarray:
|
|
142
|
+
# for stable inversion of Fisher information matrix; used in confidence_intervals() below
|
|
143
|
+
|
|
144
|
+
# locate inf/-inf on diag
|
|
145
|
+
finite_indices = ~np.isinf(np.diag(F))
|
|
146
|
+
|
|
147
|
+
# cut out zero/inf indices before inversion
|
|
148
|
+
F_finite = F[np.ix_(finite_indices, finite_indices)]
|
|
149
|
+
L, Q = np.linalg.eigh(F_finite)
|
|
150
|
+
|
|
151
|
+
# floor small eigenvalues at threshold
|
|
152
|
+
L[L < eigenvalue_threshold] = eigenvalue_threshold
|
|
153
|
+
|
|
154
|
+
# identify indices of exactly zero eigenvalues
|
|
155
|
+
zero_eigs = L == 0.0
|
|
156
|
+
|
|
157
|
+
# invert floored eigenvalues, compose modified inverse
|
|
158
|
+
Finv_finite = Q[:, ~zero_eigs] @ np.diag(1.0 / L[~zero_eigs]) @ Q[:, ~zero_eigs].T
|
|
159
|
+
|
|
160
|
+
# For exactly zero eigenvalues after flooring, examine entries of corresponding eigenvectors. Nonzero (> Qthresh)
|
|
161
|
+
# eigenvector entries correspond to columns of F_finite which are nontrivial contributions to this kernel. Such
|
|
162
|
+
# columns/rows of Finv are set to zero with inf on diagonal.
|
|
163
|
+
Qthresh = F_finite.shape[0] * np.finfo("float").eps
|
|
164
|
+
kernel_cols = np.any(np.abs(Q[:, zero_eigs]) > Qthresh, axis=1)
|
|
165
|
+
Finv_finite[kernel_cols, :] = 0.0
|
|
166
|
+
Finv_finite[:, kernel_cols] = 0.0
|
|
167
|
+
Finv_finite[kernel_cols, kernel_cols] = np.inf
|
|
168
|
+
|
|
169
|
+
# Initialize full output, fill in modified inverse
|
|
170
|
+
# (Note inf on diag(A) -> 0 on corresponding row/col of inverse).
|
|
171
|
+
Finv_out = np.zeros(F.shape)
|
|
172
|
+
Finv_out[np.ix_(finite_indices, finite_indices)] = Finv_finite
|
|
173
|
+
|
|
174
|
+
return Finv_out
|