Coverage for physioblocks / description / blocks.py: 98%
89 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-09 16:40 +0100
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-09 16:40 +0100
1# SPDX-FileCopyrightText: Copyright INRIA
2#
3# SPDX-License-Identifier: LGPL-3.0-only
4#
5# Copyright INRIA
6#
7# This file is part of PhysioBlocks, a library mostly developed by the
8# [Ananke project-team](https://team.inria.fr/ananke) at INRIA.
9#
10# Authors:
11# - Colin Drieu
12# - Dominique Chapelle
13# - François Kimmig
14# - Philippe Moireau
15#
16# PhysioBlocks is free software: you can redistribute it and/or modify it under the
17# terms of the GNU Lesser General Public License as published by the Free Software
18# Foundation, version 3 of the License.
19#
20# PhysioBlocks is distributed in the hope that it will be useful, but WITHOUT ANY
21# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
22# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
23#
24# You should have received a copy of the GNU Lesser General Public License along with
25# PhysioBlocks. If not, see <https://www.gnu.org/licenses/>.
27"""
28Declares the **Blocks** and their **Local Nodes**
29"""
31from __future__ import annotations
33from physioblocks.computing.models import (
34 Block,
35 Expression,
36 ExpressionDefinition,
37 ModelComponent,
38 TermDefinition,
39)
40from physioblocks.registers.type_register import register_type
42# Separator for model parameter names
43ID_SEPARATOR = "."
45# Model description type id
46MODEL_DESCRIPTION_TYPE_ID = "model_description"
49@register_type(MODEL_DESCRIPTION_TYPE_ID)
50class ModelComponentDescription:
51 """
52 Description of a :class:`~physioblocks.computing.models.ModelComponent` object
53 in a :class:`~physioblocks.description.nets.Net` object.
55 **Model Components** have no fluxes and their description can not interact directly
56 with the net.
58 To use a model component in a net, it has to be added has a sub-model of a
59 :class:`~physioblocks.description.blocks.BlockDescription` object or
60 of a another :class:`~physioblocks.description.blocks.ModelComponentDescription`
61 object.
63 :param unique_id: the model name in the net
64 :type unique_id: str
66 :param model_type: the described **ModelComponent type**
67 :type model_type: type[ModelComponent]
69 :param global_ids: mapping of all the component local parameter name with
70 their global names in the net
71 :type global_ids: dict[str, str]
73 :param submodels: mapping of all the model submodels with their name.
74 :type submodels: dict[str, ModelComponentDescription]
75 """
77 _unique_id: str
79 _submodels: dict[str, ModelComponentDescription]
81 def __init__(
82 self,
83 unique_id: str,
84 model_type: type[ModelComponent],
85 global_ids: dict[str, str] | None = None,
86 submodels: dict[str, ModelComponentDescription] | None = None,
87 ):
88 self._unique_id = unique_id
89 self._described_type = model_type
91 # check user defined ids
92 user_ids = {}
93 if global_ids is not None:
94 for key, item in global_ids.items():
95 if key not in model_type.local_ids:
96 raise AttributeError(
97 str.format(
98 "{0} has no attribute named {1}.",
99 model_type.__name__,
100 key,
101 )
102 )
103 user_ids[key] = item
105 # initialise default ids
106 self._global_ids = {
107 local_id: ID_SEPARATOR.join([self.name, local_id])
108 for local_id in self._described_type.local_ids
109 }
111 # update with user ids
112 self._global_ids.update(user_ids)
114 # build expressions definitions once
115 self._internal_defs = self._get_global_expressions_definitions(
116 self._described_type.internal_expressions
117 )
118 self._saved_quantities_defs = self._get_global_expressions_definitions(
119 self._described_type.saved_quantities_expressions
120 )
122 # Initialise submodels
123 self._submodels = {}
124 if submodels is not None:
125 for model_id, model in submodels.items():
126 # rename default ids with submodel new id
127 self.add_submodel(model_id, model)
129 def _get_global_expressions_definitions(
130 self, local_definitions: list[ExpressionDefinition]
131 ) -> list[ExpressionDefinition]:
132 return [
133 ExpressionDefinition(
134 Expression(
135 expression_def.expression.size,
136 expression_def.expression.expr_func,
137 {
138 self.global_ids[grad_key]: grad_expr
139 for grad_key, grad_expr in expression_def.expression.expr_gradients.items() # noqa: E501
140 },
141 ),
142 [
143 TermDefinition(self.global_ids[term.term_id], term.size, term.index)
144 for term in expression_def.terms
145 ],
146 )
147 for expression_def in local_definitions
148 ]
150 @property
151 def name(self) -> str:
152 """Get the model component name.
154 :return: the model component name
155 :rtype: str
156 """
157 return self._unique_id
159 @property
160 def global_ids(self) -> dict[str, str]:
161 """Get a mapping of all local name of the quantities matching
162 their global name in the net.
164 :return: the global ids of the model
165 :rtype: dict[str, str]
166 """
167 return self._global_ids.copy()
169 @property
170 def described_type(self) -> type[ModelComponent]:
171 """Get the described :class:`~physioblocks.computing.models.ModelComponent`
172 type.
174 :return: the model component type
175 :rtype: type[ModelComponent]
176 """
177 return self._described_type
179 @property
180 def submodels(self) -> dict[str, ModelComponentDescription]:
181 """Get the submodels descriptions.
183 :return: the submodel descriptions
184 :rtype: dict[str, ModelComponentDescription]
185 """
186 return self._submodels.copy()
188 @property
189 def internal_variables(self) -> list[tuple[str, int]]:
190 """
191 Get the model **Internal Variables** global names recursively for the
192 model and its submodels.
194 :return: the internal variables name and sizes
195 :rtype: list[tuple[str, int]]
196 """
197 internal_variables = [
198 (self._global_ids[term_def.term_id], term_def.size)
199 for term_def in self._described_type.internal_variables
200 ]
202 for model in self.submodels.values():
203 internal_variables.extend(model.internal_variables)
205 return internal_variables
207 @property
208 def internal_expressions(self) -> list[ExpressionDefinition]:
209 """
210 Get the :class:`~physioblocks.computing.models.ExpressionDefinition` object
211 representing model's **Internal equations**.
213 :return: all the model internal equations
214 :rtype: list[ExpressionDefinition]
215 """
216 return self._internal_defs
218 @property
219 def saved_quantities(self) -> list[tuple[str, int]]:
220 """
221 Get the model **Saved Quantities** global names and sizes recursivly
222 for the model and its submodels.
224 :return: the saved quantities name and sizes
225 :rtype: list[tuple[str, int]]
226 """
228 saved_quantities = [
229 (self._global_ids[term_def.term_id], term_def.size)
230 for term_def in self._described_type.saved_quantities
231 ]
233 for model in self.submodels.values():
234 saved_quantities.extend(model.saved_quantities)
236 return saved_quantities
238 @property
239 def saved_quantities_expressions(self) -> list[ExpressionDefinition]:
240 """
241 Get all saved quantities expressions definitions for model
243 :return: all the model saved quantities expression definitions
244 :rtype: list[ExpressionDefinition]
245 """
246 return self._saved_quantities_defs
248 def rename_global_id(self, old_id: str, new_id: str) -> None:
249 """Rename the global name with the new name in the current model and
250 all its submodels.
252 If no name is a match, then no name are changed.
254 :param old_id: the global name to rename
255 :type old_id: str
257 :param new_id: the new name to set
258 :type new_id: str
259 """
260 for local_id, global_id in self._global_ids.items():
261 if old_id == global_id:
262 self._global_ids[local_id] = new_id
264 for submodel in self.submodels.values():
265 submodel.rename_global_id(old_id, new_id)
267 def add_submodel(
268 self, local_model_id: str, model_description: ModelComponentDescription
269 ) -> ModelComponentDescription:
270 """
271 Add a submodel to the model.
273 Create and return a copy of the input model description
274 updated with correct ids.
276 :param model_id: The submodel name
277 :type model_id: str
279 :param model_description: The model to add
280 :type model_type: type[ModelComponent]
282 :return: the submodel description in the current model description
283 :rtype: ModelComponentDescritpion
284 """
285 submodel_id = ID_SEPARATOR.join([self.name, local_model_id])
287 renamed_ids = {
288 local_id: global_id
289 if global_id != ID_SEPARATOR.join([model_description.name, local_id])
290 else ID_SEPARATOR.join([self.name, local_model_id, local_id])
291 for local_id, global_id in model_description.global_ids.items()
292 }
294 self._submodels[local_model_id] = ModelComponentDescription(
295 submodel_id,
296 model_description.described_type,
297 renamed_ids,
298 model_description.submodels,
299 )
300 return self._submodels[local_model_id]
302 def remove_submodel(self, model_id: str) -> ModelComponentDescription:
303 """
304 Remove the submodel.
306 :param model_id: the id of the submodel to remove
307 :type model_id: str
309 :return: the removed submodel description
310 :rtype: ModelComponentDescritpion
311 """
312 return self._submodels.pop(model_id)
315# Id for the model description type
316BLOCK_DESCRIPTION_TYPE_ID = "block_description"
319@register_type(BLOCK_DESCRIPTION_TYPE_ID)
320class BlockDescription(ModelComponentDescription):
321 """
322 Extend the :class:`~.ModelComponentDescription` to describe
323 :class:`~physioblocks.computing.models.Block` object in the net.
325 Block descriptions connect their block's flux to
326 :type:`~physioblocks.description.nets.Node` objects to share
327 them across the net.
329 .. note:: **Internal variables** can be empty, but described
330 :type:`~physioblocks.computing.models.Block` type should at
331 least define one **Flux** (otherwise, it can not interact with the others
332 blocks in the net)
334 :param block_id: the block name in the net
335 :type block_id: str
337 :param block_type: the described block type
338 :type block_type: type[Block]
340 :param flux_type: the type of flux exchanged by the block
341 :type flux_type: str
343 :param global_ids: mapping of all the block local parameter name with
344 their global names in the net
345 :type global_ids: dict[str, str]
347 :param submodels: mapping of all the block submodels with their name.
348 :type submodels: dict[str, ModelComponentDescription]
350 Example
351 ^^^^^^^
353 >>> block_description = BlockDescription(
354 "rc_block_1", # block name
355 RCBlock, # block type
356 "flow", # block flux type
357 {
358 "resistance": "r1", # rename "rc_block_1.resistance" to "r1"
359 "capacitance": "c1", # rename "rc_block_1.capacitance" to "c1"
360 }
361 # no submodels defined
362 )
363 """
365 _block_id: str
366 _flux_type: str
367 _described_type: type[Block]
369 def __init__(
370 self,
371 block_id: str,
372 block_type: type[Block],
373 flux_type: str,
374 global_ids: dict[str, str] | None = None,
375 submodels: dict[str, ModelComponentDescription] | None = None,
376 ):
377 super().__init__(block_id, block_type, global_ids, submodels)
378 self._flux_type = flux_type
380 @property
381 def described_type(self) -> type[Block]:
382 """Get the described :class:`~physioblocks.computing.models.Block` type.
384 :return: the block type
385 :rtype: type[Block]
386 """
387 return self._described_type
389 @property
390 def fluxes(self) -> dict[int, Expression]:
391 """
392 Get a mapping of flux `~physioblocks.computing.models.Expression` associated
393 with their **Local Node** index.
395 :return: the flux expression at each local node
396 :rtype: dict[int, Expression]
397 """
398 return {
399 loc_node_index: Expression(
400 flux_def.expression.size,
401 flux_def.expression.expr_func,
402 {
403 self.global_ids[grad_key]: grad_expr
404 for grad_key, grad_expr in flux_def.expression.expr_gradients.items() # noqa: E501
405 },
406 )
407 for loc_node_index, flux_def in self.described_type.fluxes_expressions.items() # noqa: E501
408 }
410 @property
411 def flux_type(self) -> str:
412 """
413 Get the type of the block's flux.
415 :return: the flux type
416 :rtype: str
417 """
418 return self._flux_type