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.
Files changed (99) hide show
  1. certaraiq-2.2.3/PKG-INFO +55 -0
  2. certaraiq-2.2.3/README.md +29 -0
  3. certaraiq-2.2.3/pyproject.toml +116 -0
  4. certaraiq-2.2.3/src/certaraiq/__init__.py +8 -0
  5. certaraiq-2.2.3/src/certaraiq/_exports.py +8 -0
  6. certaraiq-2.2.3/src/certaraiq/_helper/__init__.py +0 -0
  7. certaraiq-2.2.3/src/certaraiq/_helper/argument_parser.py +157 -0
  8. certaraiq-2.2.3/src/certaraiq/_helper/covariance_matrix.py +174 -0
  9. certaraiq-2.2.3/src/certaraiq/_helper/create_map.py +180 -0
  10. certaraiq-2.2.3/src/certaraiq/_helper/display.py +24 -0
  11. certaraiq-2.2.3/src/certaraiq/_helper/get_dose_schedules.py +32 -0
  12. certaraiq-2.2.3/src/certaraiq/_helper/get_model.py +107 -0
  13. certaraiq-2.2.3/src/certaraiq/_helper/get_table.py +70 -0
  14. certaraiq-2.2.3/src/certaraiq/_helper/helper.py +189 -0
  15. certaraiq-2.2.3/src/certaraiq/_helper/initialize_parameter_table.py +25 -0
  16. certaraiq-2.2.3/src/certaraiq/_helper/nested_map.py +64 -0
  17. certaraiq-2.2.3/src/certaraiq/_helper/parameter.py +16 -0
  18. certaraiq-2.2.3/src/certaraiq/_helper/parse_output_times.py +48 -0
  19. certaraiq-2.2.3/src/certaraiq/_helper/parse_schedule.py +131 -0
  20. certaraiq-2.2.3/src/certaraiq/_helper/parser.py +84 -0
  21. certaraiq-2.2.3/src/certaraiq/_helper/posterior_sample_list_diagnostics.py +28 -0
  22. certaraiq-2.2.3/src/certaraiq/_helper/profile_likelihood.py +286 -0
  23. certaraiq-2.2.3/src/certaraiq/_helper/validate_columns.py +207 -0
  24. certaraiq-2.2.3/src/certaraiq/_labelset.py +75 -0
  25. certaraiq-2.2.3/src/certaraiq/_optimize.py +1152 -0
  26. certaraiq-2.2.3/src/certaraiq/_scan.py +108 -0
  27. certaraiq-2.2.3/src/certaraiq/_scan_ast.py +261 -0
  28. certaraiq-2.2.3/src/certaraiq/_sdk/__init__.py +3 -0
  29. certaraiq-2.2.3/src/certaraiq/_sdk/array.py +74 -0
  30. certaraiq-2.2.3/src/certaraiq/_sdk/client_interface.py +449 -0
  31. certaraiq-2.2.3/src/certaraiq/_sdk/contract.py +54 -0
  32. certaraiq-2.2.3/src/certaraiq/_sdk/data_frame.py +248 -0
  33. certaraiq-2.2.3/src/certaraiq/_sdk/data_pipe.py +304 -0
  34. certaraiq-2.2.3/src/certaraiq/_sdk/data_pipe_builder.py +346 -0
  35. certaraiq-2.2.3/src/certaraiq/_sdk/data_pipe_result.py +35 -0
  36. certaraiq-2.2.3/src/certaraiq/_sdk/data_type.py +137 -0
  37. certaraiq-2.2.3/src/certaraiq/_sdk/data_type_parser.py +23 -0
  38. certaraiq-2.2.3/src/certaraiq/_sdk/dataclass_helpers.py +19 -0
  39. certaraiq-2.2.3/src/certaraiq/_sdk/distribution_sample.py +152 -0
  40. certaraiq-2.2.3/src/certaraiq/_sdk/exceptions.py +15 -0
  41. certaraiq-2.2.3/src/certaraiq/_sdk/expression.py +5 -0
  42. certaraiq-2.2.3/src/certaraiq/_sdk/job.py +120 -0
  43. certaraiq-2.2.3/src/certaraiq/_sdk/legacy.py +294 -0
  44. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/__init__.py +30 -0
  45. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/allen.py +65 -0
  46. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/base.py +22 -0
  47. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/__init__.py +35 -0
  48. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/base.py +23 -0
  49. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/distributions.py +36 -0
  50. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/histogram.py +46 -0
  51. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/loguniform.py +46 -0
  52. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/multivariate_lognormal.py +45 -0
  53. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/multivariate_normal.py +45 -0
  54. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/distributions/uniform.py +46 -0
  55. certaraiq-2.2.3/src/certaraiq/_sdk/likelihood/measurements.py +75 -0
  56. certaraiq-2.2.3/src/certaraiq/_sdk/ode_fisher_information_matrix.py +15 -0
  57. certaraiq-2.2.3/src/certaraiq/_sdk/ode_gradient.py +17 -0
  58. certaraiq-2.2.3/src/certaraiq/_sdk/ode_measurement_likelihood_sample.py +16 -0
  59. certaraiq-2.2.3/src/certaraiq/_sdk/ode_model.py +81 -0
  60. certaraiq-2.2.3/src/certaraiq/_sdk/ode_model_reference.py +39 -0
  61. certaraiq-2.2.3/src/certaraiq/_sdk/ode_optimization.py +40 -0
  62. certaraiq-2.2.3/src/certaraiq/_sdk/ode_optimization_batch.py +39 -0
  63. certaraiq-2.2.3/src/certaraiq/_sdk/ode_optimization_configuration.py +78 -0
  64. certaraiq-2.2.3/src/certaraiq/_sdk/ode_optimization_result.py +21 -0
  65. certaraiq-2.2.3/src/certaraiq/_sdk/ode_parameter_posterior_sample.py +43 -0
  66. certaraiq-2.2.3/src/certaraiq/_sdk/ode_posterior_sample_configuration.py +36 -0
  67. certaraiq-2.2.3/src/certaraiq/_sdk/ode_prediction.py +22 -0
  68. certaraiq-2.2.3/src/certaraiq/_sdk/ode_proposal_population_sample.py +45 -0
  69. certaraiq-2.2.3/src/certaraiq/_sdk/ode_residual.py +23 -0
  70. certaraiq-2.2.3/src/certaraiq/_sdk/ode_residual_batch.py +26 -0
  71. certaraiq-2.2.3/src/certaraiq/_sdk/ode_simulation.py +74 -0
  72. certaraiq-2.2.3/src/certaraiq/_sdk/ode_simulation_batch.py +47 -0
  73. certaraiq-2.2.3/src/certaraiq/_sdk/ode_value.py +26 -0
  74. certaraiq-2.2.3/src/certaraiq/_sdk/ode_virtual_population_sample.py +42 -0
  75. certaraiq-2.2.3/src/certaraiq/_sdk/progress.py +57 -0
  76. certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/__init__.py +2 -0
  77. certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/from_bytes.py +20 -0
  78. certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/get_all_subclasses.py +14 -0
  79. certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/indexing.py +34 -0
  80. certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/metadata.py +194 -0
  81. certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/model.py +558 -0
  82. certaraiq-2.2.3/src/certaraiq/_sdk/qsp_designer_model/simulation_configuration.py +16 -0
  83. certaraiq-2.2.3/src/certaraiq/_sdk/reaction_model.py +308 -0
  84. certaraiq-2.2.3/src/certaraiq/_sdk/scenario.py +96 -0
  85. certaraiq-2.2.3/src/certaraiq/_sdk/solver_configuration.py +99 -0
  86. certaraiq-2.2.3/src/certaraiq/_sdk/status.py +9 -0
  87. certaraiq-2.2.3/src/certaraiq/_sdk/time_course.py +32 -0
  88. certaraiq-2.2.3/src/certaraiq/_sdk/times.py +35 -0
  89. certaraiq-2.2.3/src/certaraiq/_sdk/to_latex_ode_model.py +53 -0
  90. certaraiq-2.2.3/src/certaraiq/_sdk/to_matlab_ode_simulation.py +55 -0
  91. certaraiq-2.2.3/src/certaraiq/_sdk/to_simbiology_ode_simulation.py +55 -0
  92. certaraiq-2.2.3/src/certaraiq/_simulate.py +599 -0
  93. certaraiq-2.2.3/src/certaraiq/_units/ast.py +139 -0
  94. certaraiq-2.2.3/src/certaraiq/_units/comparison.py +15 -0
  95. certaraiq-2.2.3/src/certaraiq/_units/deparser.py +64 -0
  96. certaraiq-2.2.3/src/certaraiq/_units/parser.py +58 -0
  97. certaraiq-2.2.3/src/certaraiq/_units/to_pint.py +52 -0
  98. certaraiq-2.2.3/src/certaraiq/_units/valid_units.py +64 -0
  99. certaraiq-2.2.3/src/certaraiq/_vpop.py +658 -0
@@ -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
@@ -0,0 +1,8 @@
1
+ __all__ = ["client_version", "list_contracts", "server_version"]
2
+
3
+
4
+ from ._sdk import client
5
+
6
+ client_version = client.client_version
7
+ list_contracts = client.list_contracts
8
+ server_version = client.server_version
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