"""Model generators used to generate simple models from a set of inputs."""
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import field
from typing import TYPE_CHECKING
from typing import Literal
from glotaran.model import Model
from glotaran.parameter.parameter_group import ParameterGroup
if TYPE_CHECKING:
from glotaran.utils.ipython import MarkdownStr
def _split_iterable_in_non_dict_and_dict_items(
input_list: list[float, dict[str, bool | float]],
) -> tuple[list[float], list[dict[str, bool | float]]]:
"""Split an iterable (list) into non-dict and dict items.
Parameters
----------
input_list : list[float, dict[str, bool | float]]
A list of values of type `float` and a dict with parameter options, e.g.
`[1, 2, 3, {"vary": False, "non-negative": True}]`
Returns
-------
tuple[list[float], list[dict[str, bool | float]]]
Split a list into non-dict (`values`) and dict items (`defaults`),
return a tuple (`values`, `defaults`)
"""
values: list = [val for val in input_list if not isinstance(val, dict)]
defaults: list = [val for val in input_list if isinstance(val, dict)]
return values, defaults
[docs]@dataclass
class SimpleModelGenerator:
"""A minimal boilerplate model and parameters generator.
Generates a model (together with the parameters specification) based on
parameter input values assigned to the generator's attributes
"""
rates: list[float] = field(default_factory=list)
"""A list of values representing decay rates"""
k_matrix: Literal["parallel", "sequential"] | dict[tuple[str, str], str] = "parallel"
""""A `dict` with a k_matrix specification or `Literal["parallel", "sequential"]`"""
compartments: list[str] | None = None
"""A list of compartment names"""
irf: dict[str, float] = field(default_factory=dict)
"""A dict of items specifying an irf"""
initial_concentration: list[float] = field(default_factory=list)
"""A list values representing the initial concentration"""
dispersion_coefficients: list[float] = field(default_factory=list)
"""A list of values representing the dispersion coefficients"""
dispersion_center: float | None = None
"""A value representing the dispersion center"""
default_megacomplex: str = "decay"
"""The default_megacomplex identifier"""
# TODO: add support for a spectral model:
# shapes: list[float] = field(default_factory=list, init=False)
@property
def valid(self) -> bool:
"""Check if the generator state is valid.
Returns
-------
bool
Generator state obtained by calling the generated model's
`valid` function with the generated parameters as input.
"""
try:
return self.model.valid(parameters=self.parameters)
except ValueError:
return False
[docs] def validate(self) -> str:
"""Call `validate` on the generated model and return its output.
Returns
-------
str
A string listing problems in the generated model and parameters if any.
"""
return self.model.validate(parameters=self.parameters)
@property
def model(self) -> Model:
"""Return the generated model.
Returns
-------
Model
The generated model of type :class:`glotaran.model.Model`.
"""
return Model.from_dict(self.model_dict)
@property
def model_dict(self) -> dict:
"""Return a dict representation of the generated model.
Returns
-------
dict
A dict representation of the generated model.
"""
return self._model_dict()
@property
def parameters(self) -> ParameterGroup:
"""Return the generated parameters of type :class:`glotaran.parameter.ParameterGroup`.
Returns
-------
ParameterGroup
The generated parameters of type of type :class:`glotaran.parameter.ParameterGroup`.
"""
return ParameterGroup.from_dict(self.parameters_dict)
@property
def parameters_dict(self) -> dict:
"""Return a dict representation of the generated parameters.
Returns
-------
dict
A dict representing the generated parameters.
"""
return self._parameters_dict()
@property
def model_and_parameters(self) -> tuple[Model, ParameterGroup]:
"""Return generated model and parameters.
Returns
-------
tuple[Model, ParameterGroup]
A model of type :class:`glotaran.model.Model` and
and parameters of type :class:`glotaran.parameter.ParameterGroup`.
"""
return self.model, self.parameters
@property
def _rates(self) -> tuple[list[float], list[dict[str, bool | float]]]:
"""Validate input to rates, return a tuple of rates and parameter defaults.
Returns
-------
tuple[list[float], list[dict[str, bool | float]]]
A tuple of a list of rates and a dict containing parameter defaults
Raises
------
ValueError
Raised if rates is not a list of at least one number.
"""
if not isinstance(self.rates, list):
raise ValueError(f"generator.rates: must be a `list`, got: {self.rates}")
if len(self.rates) == 0:
raise ValueError("generator.rates: must be a `list` with 1 or more rates")
if not isinstance(self.rates[0], (int, float)):
raise ValueError(f"generator.rates: 1st element must be numeric, got: {self.rates[0]}")
return _split_iterable_in_non_dict_and_dict_items(self.rates)
def _parameters_dict_items(self) -> dict:
"""Return a dict with items used in constructing the parameters.
Returns
-------
dict
A dict with items used in constructing a parameters dict.
"""
rates, rates_defaults = self._rates
items = {"rates": rates}
if rates_defaults:
items["rates_defaults"] = rates_defaults[0]
items["irf"] = [[key, value] for key, value in self.irf.items()]
if self.initial_concentration:
items["inputs"] = self.initial_concentration
elif self.k_matrix == "parallel":
items["inputs"] = [
["1", 1],
{"vary": False},
]
elif self.k_matrix == "sequential":
items["inputs"] = [
["1", 1],
["0", 0],
{"vary": False},
]
return items
def _model_dict_items(self) -> dict:
"""Return a dict with items used in constructing the model.
Returns
-------
dict
A dict with items used in constructing a model dict.
"""
rates, _ = self._rates
nr = len(rates)
indices = list(range(1, 1 + nr))
items = {"default_megacomplex": self.default_megacomplex}
if self.irf:
items["irf"] = {
"type": "multi-gaussian",
"center": ["irf.center"],
"width": ["irf.width"],
}
if isinstance(self.k_matrix, dict):
items["k_matrix"] = self.k_matrix
items["input_parameters"] = [f"inputs.{i}" for i in indices]
items["compartments"] = [f"s{i}" for i in indices]
# TODO: get unique compartments from user defined k_matrix
if self.k_matrix == "parallel":
items["input_parameters"] = ["inputs.1"] * nr
items["k_matrix"] = {(f"s{i}", f"s{i}"): f"rates.{i}" for i in indices}
elif self.k_matrix == "sequential":
items["input_parameters"] = ["inputs.1"] + ["inputs.0"] * (nr - 1)
items["k_matrix"] = {
(f"s{i if i==nr else i+1}", f"s{i}"): f"rates.{i}" for i in indices
}
if self.k_matrix in ("parallel", "sequential"):
items["compartments"] = [f"s{i}" for i in indices]
return items
def _parameters_dict(self) -> dict:
"""Return a parameters dict.
Returns
-------
dict
A dict that can be passed to the `ParameterGroup` `from_dict` method.
"""
items = self._parameters_dict_items()
rates = items["rates"]
if "rates_defaults" in items:
rates += [items["rates_defaults"]]
result = {"rates": rates}
if items["irf"]:
result["irf"] = items["irf"]
result["inputs"] = items["inputs"]
return result
def _model_dict(self) -> dict:
"""Return a model dict.
Returns
-------
dict
A dict that can be passed to the `Model` `from_dict` method.
"""
items = self._model_dict_items()
result = {"default_megacomplex": items["default_megacomplex"]}
result.update(
{
"initial_concentration": {
"j1": {
"compartments": items["compartments"],
"parameters": items["input_parameters"],
},
},
"megacomplex": {
"mc1": {"k_matrix": ["k1"]},
},
"k_matrix": {"k1": {"matrix": items["k_matrix"]}},
"dataset": {
"dataset1": {
"initial_concentration": "j1",
"megacomplex": ["mc1"],
},
},
}
)
if "irf" in items:
result["dataset"]["dataset1"].update({"irf": "irf1"})
result["irf"] = {
"irf1": items["irf"],
}
return result
[docs] def markdown(self) -> MarkdownStr:
"""Return a markdown string representation of the generated model and parameters.
Returns
-------
MarkdownStr
A markdown string
"""
return self.model.markdown(parameters=self.parameters)