certaraiq 2.2.3__py3-none-any.whl

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/__init__.py +8 -0
  2. certaraiq/_exports.py +8 -0
  3. certaraiq/_helper/__init__.py +0 -0
  4. certaraiq/_helper/argument_parser.py +157 -0
  5. certaraiq/_helper/covariance_matrix.py +174 -0
  6. certaraiq/_helper/create_map.py +180 -0
  7. certaraiq/_helper/display.py +24 -0
  8. certaraiq/_helper/get_dose_schedules.py +32 -0
  9. certaraiq/_helper/get_model.py +107 -0
  10. certaraiq/_helper/get_table.py +70 -0
  11. certaraiq/_helper/helper.py +189 -0
  12. certaraiq/_helper/initialize_parameter_table.py +25 -0
  13. certaraiq/_helper/nested_map.py +64 -0
  14. certaraiq/_helper/parameter.py +16 -0
  15. certaraiq/_helper/parse_output_times.py +48 -0
  16. certaraiq/_helper/parse_schedule.py +131 -0
  17. certaraiq/_helper/parser.py +84 -0
  18. certaraiq/_helper/posterior_sample_list_diagnostics.py +28 -0
  19. certaraiq/_helper/profile_likelihood.py +286 -0
  20. certaraiq/_helper/validate_columns.py +207 -0
  21. certaraiq/_labelset.py +75 -0
  22. certaraiq/_optimize.py +1152 -0
  23. certaraiq/_scan.py +108 -0
  24. certaraiq/_scan_ast.py +261 -0
  25. certaraiq/_sdk/__init__.py +3 -0
  26. certaraiq/_sdk/array.py +74 -0
  27. certaraiq/_sdk/client_interface.py +449 -0
  28. certaraiq/_sdk/contract.py +54 -0
  29. certaraiq/_sdk/data_frame.py +248 -0
  30. certaraiq/_sdk/data_pipe.py +304 -0
  31. certaraiq/_sdk/data_pipe_builder.py +346 -0
  32. certaraiq/_sdk/data_pipe_result.py +35 -0
  33. certaraiq/_sdk/data_type.py +137 -0
  34. certaraiq/_sdk/data_type_parser.py +23 -0
  35. certaraiq/_sdk/dataclass_helpers.py +19 -0
  36. certaraiq/_sdk/distribution_sample.py +152 -0
  37. certaraiq/_sdk/exceptions.py +15 -0
  38. certaraiq/_sdk/expression.py +5 -0
  39. certaraiq/_sdk/job.py +120 -0
  40. certaraiq/_sdk/legacy.py +294 -0
  41. certaraiq/_sdk/likelihood/__init__.py +30 -0
  42. certaraiq/_sdk/likelihood/allen.py +65 -0
  43. certaraiq/_sdk/likelihood/base.py +22 -0
  44. certaraiq/_sdk/likelihood/distributions/__init__.py +35 -0
  45. certaraiq/_sdk/likelihood/distributions/base.py +23 -0
  46. certaraiq/_sdk/likelihood/distributions/distributions.py +36 -0
  47. certaraiq/_sdk/likelihood/distributions/histogram.py +46 -0
  48. certaraiq/_sdk/likelihood/distributions/loguniform.py +46 -0
  49. certaraiq/_sdk/likelihood/distributions/multivariate_lognormal.py +45 -0
  50. certaraiq/_sdk/likelihood/distributions/multivariate_normal.py +45 -0
  51. certaraiq/_sdk/likelihood/distributions/uniform.py +46 -0
  52. certaraiq/_sdk/likelihood/measurements.py +75 -0
  53. certaraiq/_sdk/ode_fisher_information_matrix.py +15 -0
  54. certaraiq/_sdk/ode_gradient.py +17 -0
  55. certaraiq/_sdk/ode_measurement_likelihood_sample.py +16 -0
  56. certaraiq/_sdk/ode_model.py +81 -0
  57. certaraiq/_sdk/ode_model_reference.py +39 -0
  58. certaraiq/_sdk/ode_optimization.py +40 -0
  59. certaraiq/_sdk/ode_optimization_batch.py +39 -0
  60. certaraiq/_sdk/ode_optimization_configuration.py +78 -0
  61. certaraiq/_sdk/ode_optimization_result.py +21 -0
  62. certaraiq/_sdk/ode_parameter_posterior_sample.py +43 -0
  63. certaraiq/_sdk/ode_posterior_sample_configuration.py +36 -0
  64. certaraiq/_sdk/ode_prediction.py +22 -0
  65. certaraiq/_sdk/ode_proposal_population_sample.py +45 -0
  66. certaraiq/_sdk/ode_residual.py +23 -0
  67. certaraiq/_sdk/ode_residual_batch.py +26 -0
  68. certaraiq/_sdk/ode_simulation.py +74 -0
  69. certaraiq/_sdk/ode_simulation_batch.py +47 -0
  70. certaraiq/_sdk/ode_value.py +26 -0
  71. certaraiq/_sdk/ode_virtual_population_sample.py +42 -0
  72. certaraiq/_sdk/progress.py +57 -0
  73. certaraiq/_sdk/qsp_designer_model/__init__.py +2 -0
  74. certaraiq/_sdk/qsp_designer_model/from_bytes.py +20 -0
  75. certaraiq/_sdk/qsp_designer_model/get_all_subclasses.py +14 -0
  76. certaraiq/_sdk/qsp_designer_model/indexing.py +34 -0
  77. certaraiq/_sdk/qsp_designer_model/metadata.py +194 -0
  78. certaraiq/_sdk/qsp_designer_model/model.py +558 -0
  79. certaraiq/_sdk/qsp_designer_model/simulation_configuration.py +16 -0
  80. certaraiq/_sdk/reaction_model.py +308 -0
  81. certaraiq/_sdk/scenario.py +96 -0
  82. certaraiq/_sdk/solver_configuration.py +99 -0
  83. certaraiq/_sdk/status.py +9 -0
  84. certaraiq/_sdk/time_course.py +32 -0
  85. certaraiq/_sdk/times.py +35 -0
  86. certaraiq/_sdk/to_latex_ode_model.py +53 -0
  87. certaraiq/_sdk/to_matlab_ode_simulation.py +55 -0
  88. certaraiq/_sdk/to_simbiology_ode_simulation.py +55 -0
  89. certaraiq/_simulate.py +599 -0
  90. certaraiq/_units/ast.py +139 -0
  91. certaraiq/_units/comparison.py +15 -0
  92. certaraiq/_units/deparser.py +64 -0
  93. certaraiq/_units/parser.py +58 -0
  94. certaraiq/_units/to_pint.py +52 -0
  95. certaraiq/_units/valid_units.py +64 -0
  96. certaraiq/_vpop.py +658 -0
  97. certaraiq-2.2.3.dist-info/METADATA +55 -0
  98. certaraiq-2.2.3.dist-info/RECORD +99 -0
  99. certaraiq-2.2.3.dist-info/WHEEL +4 -0
certaraiq/__init__.py ADDED
@@ -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
certaraiq/_exports.py ADDED
@@ -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
@@ -0,0 +1,180 @@
1
+ import tabeline as tl
2
+
3
+ from .._labelset import LabelSet
4
+ from .nested_map import NestedMap
5
+ from .parameter import Parameter
6
+
7
+
8
+ def create_parameters_map(df: tl.DataFrame, parameter_label_columns: list[str]) -> NestedMap:
9
+ labels = LabelSet.from_df(df) # This is necessary because labels need to be standardized
10
+
11
+ param_map = NestedMap()
12
+
13
+ for i, lb in enumerate(labels):
14
+ param_map.set_value(
15
+ # if the label does not exist it is wildcard.
16
+ # Add the parameter column to the list of columns to be used as keys for the last layer and
17
+ # it is guaranteed that it is not wildcard
18
+ [lb.labels.get(k, "*") for k in [*parameter_label_columns, "parameter"]],
19
+ {df[i, "parameter"]: Parameter(value=df[i, "value"], unit=df[i, "unit"])},
20
+ )
21
+
22
+ return param_map
23
+
24
+
25
+ def create_fitting_parameters_map(df: tl.DataFrame, parameter_label_columns: list[str]) -> NestedMap:
26
+ labels = LabelSet.from_df(df) # This is necessary because labels need to be standardized
27
+
28
+ param_map = NestedMap()
29
+
30
+ for i, lb in enumerate(labels):
31
+ parameter_i = {
32
+ "parameter": df[i, "parameter"],
33
+ "value": df[i, "value"],
34
+ "unit": df[i, "unit"],
35
+ "is_fit": df[i, "is_fit"],
36
+ "lower_bound": df[i, "lower_bound"],
37
+ "upper_bound": df[i, "upper_bound"],
38
+ "global_parameter_name": df[i, "global_parameter_name"],
39
+ }
40
+
41
+ if "prior_distribution" in df.column_names:
42
+ parameter_i["prior_distribution"] = df[i, "prior_distribution"]
43
+ if "location" in df.column_names:
44
+ parameter_i["location"] = df[i, "location"]
45
+ if "scale" in df.column_names:
46
+ parameter_i["scale"] = df[i, "scale"]
47
+
48
+ param_map.set_value(
49
+ # if the label does not exist it is wildcard.
50
+ # Add the parameter column to the list of columns to be used as keys for the last layer and
51
+ # it is guaranteed that it is not wildcard
52
+ [lb.labels.get(k, "*") for k in [*parameter_label_columns, "parameter"]],
53
+ {df[i, "parameter"]: parameter_i},
54
+ )
55
+
56
+ return param_map
57
+
58
+
59
+ def create_doses_map(df: tl.DataFrame, dose_label_columns: list[str]) -> NestedMap:
60
+ labels = LabelSet.from_df(df) # This is necessary because labels need to be standardized
61
+ dose_map = NestedMap()
62
+ has_amounts = "amounts" in df.column_names
63
+ has_durations = "durations" in df.column_names
64
+
65
+ for i, lb in enumerate(labels):
66
+ schedule_i = {
67
+ "route": df[i, "route"],
68
+ "times": df[i, "times"],
69
+ "time_unit": df[i, "time_unit"],
70
+ }
71
+ if has_amounts:
72
+ schedule_i.update({"amounts": df[i, "amounts"], "amount_unit": df[i, "amount_unit"]})
73
+ if has_durations:
74
+ schedule_i.update({"durations": df[i, "durations"], "duration_unit": df[i, "duration_unit"]})
75
+ # if the label does not exist it is wildcard.
76
+ # Add the route column to the list of columns to be used as keys for the last layer and
77
+ # it is guaranteed that it is not wildcard
78
+ dose_map.set_value(
79
+ [lb.labels.get(k, "*") for k in [*dose_label_columns, "route"]], {df[i, "route"]: schedule_i}
80
+ )
81
+
82
+ return dose_map
83
+
84
+
85
+ def create_models_map(df: tl.DataFrame, model_label_columns: list[str]) -> NestedMap:
86
+ labels = LabelSet.from_df(df) # This is necessary because labels need to be standardized
87
+ model_map = NestedMap()
88
+
89
+ for i, lb in enumerate(labels):
90
+ # if the label does not exist it is wildcard.
91
+ # Add the route column to the list of columns to be used as keys for the last layer and
92
+ # it is guaranteed that it is not wildcard
93
+ model_map.set_value(
94
+ [lb.labels.get(k, "*") for k in [*model_label_columns, "model"]],
95
+ {df[i, "model"]: {"model": df[i, "model"]}},
96
+ )
97
+
98
+ return model_map
99
+
100
+
101
+ def create_times_map(df: tl.DataFrame, time_label_columns: list[str]) -> NestedMap:
102
+ labels = LabelSet.from_df(df) # This is necessary because labels need to be standardized
103
+ time_map = NestedMap()
104
+
105
+ for i, lb in enumerate(labels):
106
+ # if the label does not exist it is wildcard.
107
+ # Add the index to the list of columns to be used as keys for the last layer
108
+ # for times duplication is fine
109
+ time_map.set_value(
110
+ [lb.labels.get(k, "*") for k in time_label_columns] + [f"{i}"],
111
+ {"times": df[i, "times"], "times_unit": df[i, "times_unit"]},
112
+ )
113
+
114
+ return time_map
115
+
116
+
117
+ def create_measurements_map(df: tl.DataFrame, measurement_label_columns: list[str]) -> NestedMap:
118
+ labels = LabelSet.from_df(df) # This is necessary because labels need to be standardized
119
+ measurement_map = NestedMap()
120
+
121
+ has_exponential_error = "exponential_error" in df.column_names
122
+ has_constant_error = "constant_error" in df.column_names
123
+ has_proportional_error = "proportional_error" in df.column_names
124
+
125
+ for i, lb in enumerate(labels):
126
+ measurement_i = {
127
+ "time": df[i, "time"],
128
+ "time_unit": df[i, "time_unit"],
129
+ "output": df[i, "output"],
130
+ "measurement": df[i, "measurement"],
131
+ "measurement_unit": df[i, "measurement_unit"],
132
+ }
133
+ if has_exponential_error:
134
+ measurement_i.update({"exponential_error": df[i, "exponential_error"]})
135
+ if has_constant_error:
136
+ measurement_i.update({"constant_error": df[i, "constant_error"]})
137
+ if has_proportional_error:
138
+ measurement_i.update({"proportional_error": df[i, "proportional_error"]})
139
+
140
+ # if the label does not exist it is wildcard.
141
+ # Add the index to the list of columns to be used as keys for the last layer
142
+ # for measurements duplication is fine
143
+ measurement_map.set_value([lb.labels.get(k, "*") for k in measurement_label_columns] + [f"{i}"], measurement_i)
144
+
145
+ return measurement_map
146
+
147
+
148
+ def create_distributions_map(df: tl.DataFrame, distribution_label_columns: list[str]) -> NestedMap:
149
+ labels = LabelSet.from_df(df) # This is necessary because labels need to be standardized
150
+ distribution_map = NestedMap()
151
+
152
+ has_histogram_cols = "bin_edges" in df.column_names
153
+ has_mvn_cols = "covariance" in df.column_names
154
+
155
+ for i, lb in enumerate(labels):
156
+ distribution_i = {
157
+ "time": df[i, "time"],
158
+ "time_unit": df[i, "time_unit"],
159
+ "target": df[i, "target"],
160
+ "distribution": df[i, "distribution"],
161
+ "distribution_unit": df[i, "distribution_unit"],
162
+ }
163
+ if has_histogram_cols:
164
+ distribution_i.update({"bin_edges": df[i, "bin_edges"]})
165
+ distribution_i.update({"bin_probabilities": df[i, "bin_probabilities"]})
166
+ distribution_i.update({"smoothness": df[i, "smoothness"]})
167
+
168
+ if has_mvn_cols:
169
+ distribution_i.update({"measurements": df[i, "measurements"]})
170
+ distribution_i.update({"measurements_unit": df[i, "measurements_unit"]})
171
+ distribution_i.update({"variance": df[i, "covariance"]}) # TODO: yes, this is stupid
172
+
173
+ # if the label does not exist it is wildcard.
174
+ # Add the index to the list of columns to be used as keys for the last layer
175
+ # for measurements duplication is fine
176
+ distribution_map.set_value(
177
+ [lb.labels.get(k, "*") for k in distribution_label_columns] + [f"{i}"], distribution_i
178
+ )
179
+
180
+ return distribution_map
@@ -0,0 +1,24 @@
1
+ __all__ = ["Display"]
2
+
3
+ from dataclasses import dataclass
4
+ from uuid import uuid4
5
+
6
+ from IPython import get_ipython
7
+ from IPython.display import DisplayObject, display, update_display
8
+
9
+
10
+ @dataclass(kw_only=True, slots=True)
11
+ class Display:
12
+ display_id: str | None = None
13
+
14
+ def display(self, display_object: DisplayObject) -> None:
15
+ if get_ipython() is None:
16
+ message = display_object.data
17
+ else:
18
+ message = display_object
19
+
20
+ if self.display_id is None:
21
+ self.display_id = str(uuid4())
22
+ display(message, display_id=self.display_id)
23
+ else:
24
+ update_display(message, display_id=self.display_id)
@@ -0,0 +1,32 @@
1
+ from pathlib import Path
2
+
3
+ import pandas as pd
4
+ import tabeline as tl
5
+
6
+ from certaraiq._sdk.data_frame import DataFrame
7
+ from certaraiq._sdk.reaction_model import Schedule
8
+
9
+ from .helper import find_duplicate_keys
10
+ from .parse_schedule import parse_schedule
11
+
12
+ DataFrameLike = pd.DataFrame | tl.DataFrame | DataFrame
13
+ PathLike = str | Path
14
+
15
+
16
+ def get_dose_schedules(
17
+ doses: list[dict[str, dict[str, int | float | str]]], labels: dict[str, str | float | int | bool]
18
+ ) -> dict[str, Schedule]:
19
+ duplicate_routes = find_duplicate_keys(doses)
20
+ if len(duplicate_routes) > 0:
21
+ if len(labels) > 0:
22
+ raise ValueError(
23
+ "Duplicate routes found for label(s)"
24
+ f" {', '.join([f'{key}={value}' for key, value in labels.items()])}:"
25
+ f" {', '.join(duplicate_routes)}"
26
+ )
27
+ else:
28
+ raise ValueError(f"Duplicate routes found: {', '.join(duplicate_routes)}")
29
+
30
+ route_schedules_i = {r: parse_schedule(d) for dose in doses for r, d in dose.items()}
31
+
32
+ return route_schedules_i