|
4 | 4 | from dataclasses import dataclass, field, fields |
5 | 5 | from enum import Enum |
6 | 6 | from pathlib import Path |
7 | | -from typing import ClassVar, NamedTuple |
| 7 | +from typing import ClassVar, NamedTuple, NotRequired, TypedDict |
8 | 8 |
|
9 | 9 | import cobra |
| 10 | +import numpy as np |
| 11 | +import numpy.typing as npt |
10 | 12 | import pandas as pd |
11 | 13 | from loguru import logger |
12 | 14 |
|
@@ -78,11 +80,6 @@ class SourceTypes(str, Enum): |
78 | 80 | proteomics = "proteomics" |
79 | 81 |
|
80 | 82 |
|
81 | | -class PeakIdentificationParameters(NamedTuple): |
82 | | - height: float = 0.02 |
83 | | - distance: float = 1.0 |
84 | | - |
85 | | - |
86 | 83 | class CobraCompartments: |
87 | 84 | """Convert from compartment "long-hand" to "short-hand". |
88 | 85 |
|
@@ -135,7 +132,9 @@ class CobraCompartments: |
135 | 132 | "s": ["eyespot", "eyespot apparatus", "stigma"], |
136 | 133 | } |
137 | 134 |
|
138 | | - _REVERSE_LOOKUP: ClassVar[dict[str, list[str]]] = {value.lower(): key for key, values in SHORTHAND.items() for value in values} |
| 135 | + _REVERSE_LOOKUP: ClassVar[dict[str, list[str]]] = { |
| 136 | + value.lower(): key for key, values in SHORTHAND.items() for value in values |
| 137 | + } |
139 | 138 |
|
140 | 139 | @classmethod |
141 | 140 | def get_shorthand(cls, longhand: str) -> str | None: |
@@ -163,14 +162,6 @@ def get_longhand(cls, shorthand: str) -> str | None: |
163 | 162 | return longhand[0] if longhand else None |
164 | 163 |
|
165 | 164 |
|
166 | | -class _BuildResults(NamedTuple): |
167 | | - """Results of building a context specific model.""" |
168 | | - |
169 | | - model: cobra.Model |
170 | | - expression_index_list: list[int] |
171 | | - infeasible_reactions: pd.DataFrame |
172 | | - |
173 | | - |
174 | 165 | class _BoundaryReactions(NamedTuple): |
175 | 166 | """Boundary reactions to be used in the context specific model.""" |
176 | 167 |
|
@@ -269,3 +260,104 @@ class _SourceWeights(_BaseDataType): |
269 | 260 | mrna: int |
270 | 261 | scrna: int |
271 | 262 | proteomics: int |
| 263 | + |
| 264 | + |
| 265 | +@dataclass |
| 266 | +class ModelBuildSettings: |
| 267 | + troppo_epsilon: float = 1e-4 |
| 268 | + min_reaction_flux: float = 1e-7 |
| 269 | + solver_timeout: int = 1800 # time in seconds, defaults to 30 minutes |
| 270 | + |
| 271 | + """ |
| 272 | + Verbosity |
| 273 | + Type: int |
| 274 | + Default value: 0 |
| 275 | + Range: [0, 3] |
| 276 | + From: https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#csclientlog |
| 277 | + """ |
| 278 | + solver_verbosity: int = 0 |
| 279 | + |
| 280 | + """ |
| 281 | + Feasibility |
| 282 | + Type: double |
| 283 | + Default value: 1e-6 |
| 284 | + Range: [1e-9, 1e-2] |
| 285 | + From: https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#feasibilitytol |
| 286 | + """ |
| 287 | + solver_feasibility: float = 1e-6 |
| 288 | + |
| 289 | + """ |
| 290 | + OptimalityTol |
| 291 | + Type: double |
| 292 | + Default value: 1e-6 |
| 293 | + Range: [1e-9, 1e-2] |
| 294 | + From: https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#optimalitytol |
| 295 | + """ |
| 296 | + solver_optimality: float = 1e-6 |
| 297 | + |
| 298 | + """ |
| 299 | + IntFeasTol |
| 300 | + Type: double |
| 301 | + Default value: 1e-5 |
| 302 | + Range: [1e-9, 1e-1] |
| 303 | + From: https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#intfeastol |
| 304 | + """ |
| 305 | + solver_integrality: float = 1e-6 |
| 306 | + |
| 307 | + """ |
| 308 | + MIPGap |
| 309 | + Relative MIP optimality gap |
| 310 | + Default value: 1e-4 |
| 311 | + Range: [0, inf) |
| 312 | + From: https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#mipgap |
| 313 | + """ |
| 314 | + gurobi_mipgap: float = 1e-4 |
| 315 | + |
| 316 | + """ |
| 317 | + IntegralityFocus |
| 318 | + Type: int |
| 319 | + Default value: 0 |
| 320 | + Range: [0, 1] |
| 321 | + From: https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#integralityfocus |
| 322 | + """ |
| 323 | + gurobi_integrality_focus: int = 0 |
| 324 | + |
| 325 | + """ |
| 326 | + NumericFocus |
| 327 | + Type: int |
| 328 | + Default value: 0 |
| 329 | + Range: [0, 3] |
| 330 | + From: https://docs.gurobi.com/projects/optimizer/en/current/reference/parameters.html#numericfocus |
| 331 | + """ |
| 332 | + gurobi_numeric_focus: int = 2 |
| 333 | + |
| 334 | + def __post_init__(self): # noqa: C901 |
| 335 | + """Validate provided arguments. |
| 336 | +
|
| 337 | + :raises: ValueError if any check fails. |
| 338 | + """ |
| 339 | + if self.troppo_epsilon < 0: |
| 340 | + raise ValueError("ModelBuildSettings: `troppo_epsilon` must be a non-negative float") |
| 341 | + if self.min_reaction_flux < 0: |
| 342 | + raise ValueError("ModelBuildSettings: `min_reaction_flux` must be a non-negative float") |
| 343 | + if self.solver_verbosity not in {0, 1, 2, 3} or not isinstance(self.solver_verbosity, int): |
| 344 | + raise ValueError("ModelBuildSettings: `solver_verbosity` must be an integer in the range [0, 3]") |
| 345 | + if self.solver_timeout < 0 or not isinstance(self.solver_timeout, int): |
| 346 | + raise ValueError("ModelBuildSettings: `solver_timeout` must be a non-negative integer") |
| 347 | + if not (1e-9 < self.solver_feasibility < 1e-2): |
| 348 | + raise ValueError("ModelBuildSettings: `solver_feasibility` must be a float in the range [1e-9, 1e-2]") |
| 349 | + if not (1e-9 < self.solver_optimality < 1e-2): |
| 350 | + raise ValueError("ModelBuildSettings: `solver_optimality` must be a float in the range [1e-9, 1e-2]") |
| 351 | + if not (1e-9 < self.solver_integrality < 1e-1): |
| 352 | + raise ValueError("ModelBuildSettings: `solver_integrality` must be a float in the range [1e-9, 1e-1]") |
| 353 | + if self.gurobi_mipgap < 0: |
| 354 | + raise ValueError("ModelBuildSettings: `gurobi_mipgap` must be a float in the range [1e-4, inf.)") |
| 355 | + if self.gurobi_integrality_focus not in {0, 1} or not isinstance(self.gurobi_integrality_focus, int): |
| 356 | + raise ValueError("ModelBuildSettings: `gurobi_integrality_focus` must be an integer in the range [0, 1]") |
| 357 | + if self.gurobi_numeric_focus not in {0, 1, 2, 3} or not isinstance(self.gurobi_numeric_focus, int): |
| 358 | + raise ValueError("ModelBuildSettings: `gurobi_numeric_focus` must be an integer in the range [0, 3]") |
| 359 | + if self.troppo_epsilon < self.min_reaction_flux * 1000: |
| 360 | + logger.warning( |
| 361 | + "ModelBuildSettings: `troppo_epsilon` and `min_reaction_flux` have similar values. " |
| 362 | + "This can cause inconsistent model builds. Consider ~1000x fold change between the two. " |
| 363 | + ) |
0 commit comments