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

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/>. 

26 

27""" 

28Declares the **Blocks** and their **Local Nodes** 

29""" 

30 

31from __future__ import annotations 

32 

33from physioblocks.computing.models import ( 

34 Block, 

35 Expression, 

36 ExpressionDefinition, 

37 ModelComponent, 

38 TermDefinition, 

39) 

40from physioblocks.registers.type_register import register_type 

41 

42# Separator for model parameter names 

43ID_SEPARATOR = "." 

44 

45# Model description type id 

46MODEL_DESCRIPTION_TYPE_ID = "model_description" 

47 

48 

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. 

54 

55 **Model Components** have no fluxes and their description can not interact directly 

56 with the net. 

57 

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. 

62 

63 :param unique_id: the model name in the net 

64 :type unique_id: str 

65 

66 :param model_type: the described **ModelComponent type** 

67 :type model_type: type[ModelComponent] 

68 

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] 

72 

73 :param submodels: mapping of all the model submodels with their name. 

74 :type submodels: dict[str, ModelComponentDescription] 

75 """ 

76 

77 _unique_id: str 

78 

79 _submodels: dict[str, ModelComponentDescription] 

80 

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 

90 

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 

104 

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 } 

110 

111 # update with user ids 

112 self._global_ids.update(user_ids) 

113 

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 ) 

121 

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) 

128 

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 ] 

149 

150 @property 

151 def name(self) -> str: 

152 """Get the model component name. 

153 

154 :return: the model component name 

155 :rtype: str 

156 """ 

157 return self._unique_id 

158 

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. 

163 

164 :return: the global ids of the model 

165 :rtype: dict[str, str] 

166 """ 

167 return self._global_ids.copy() 

168 

169 @property 

170 def described_type(self) -> type[ModelComponent]: 

171 """Get the described :class:`~physioblocks.computing.models.ModelComponent` 

172 type. 

173 

174 :return: the model component type 

175 :rtype: type[ModelComponent] 

176 """ 

177 return self._described_type 

178 

179 @property 

180 def submodels(self) -> dict[str, ModelComponentDescription]: 

181 """Get the submodels descriptions. 

182 

183 :return: the submodel descriptions 

184 :rtype: dict[str, ModelComponentDescription] 

185 """ 

186 return self._submodels.copy() 

187 

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. 

193 

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 ] 

201 

202 for model in self.submodels.values(): 

203 internal_variables.extend(model.internal_variables) 

204 

205 return internal_variables 

206 

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**. 

212 

213 :return: all the model internal equations 

214 :rtype: list[ExpressionDefinition] 

215 """ 

216 return self._internal_defs 

217 

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. 

223 

224 :return: the saved quantities name and sizes 

225 :rtype: list[tuple[str, int]] 

226 """ 

227 

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 ] 

232 

233 for model in self.submodels.values(): 

234 saved_quantities.extend(model.saved_quantities) 

235 

236 return saved_quantities 

237 

238 @property 

239 def saved_quantities_expressions(self) -> list[ExpressionDefinition]: 

240 """ 

241 Get all saved quantities expressions definitions for model 

242 

243 :return: all the model saved quantities expression definitions 

244 :rtype: list[ExpressionDefinition] 

245 """ 

246 return self._saved_quantities_defs 

247 

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. 

251 

252 If no name is a match, then no name are changed. 

253 

254 :param old_id: the global name to rename 

255 :type old_id: str 

256 

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 

263 

264 for submodel in self.submodels.values(): 

265 submodel.rename_global_id(old_id, new_id) 

266 

267 def add_submodel( 

268 self, local_model_id: str, model_description: ModelComponentDescription 

269 ) -> ModelComponentDescription: 

270 """ 

271 Add a submodel to the model. 

272 

273 Create and return a copy of the input model description 

274 updated with correct ids. 

275 

276 :param model_id: The submodel name 

277 :type model_id: str 

278 

279 :param model_description: The model to add 

280 :type model_type: type[ModelComponent] 

281 

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]) 

286 

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 } 

293 

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] 

301 

302 def remove_submodel(self, model_id: str) -> ModelComponentDescription: 

303 """ 

304 Remove the submodel. 

305 

306 :param model_id: the id of the submodel to remove 

307 :type model_id: str 

308 

309 :return: the removed submodel description 

310 :rtype: ModelComponentDescritpion 

311 """ 

312 return self._submodels.pop(model_id) 

313 

314 

315# Id for the model description type 

316BLOCK_DESCRIPTION_TYPE_ID = "block_description" 

317 

318 

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. 

324 

325 Block descriptions connect their block's flux to 

326 :type:`~physioblocks.description.nets.Node` objects to share 

327 them across the net. 

328 

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) 

333 

334 :param block_id: the block name in the net 

335 :type block_id: str 

336 

337 :param block_type: the described block type 

338 :type block_type: type[Block] 

339 

340 :param flux_type: the type of flux exchanged by the block 

341 :type flux_type: str 

342 

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] 

346 

347 :param submodels: mapping of all the block submodels with their name. 

348 :type submodels: dict[str, ModelComponentDescription] 

349 

350 Example 

351 ^^^^^^^ 

352 

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 """ 

364 

365 _block_id: str 

366 _flux_type: str 

367 _described_type: type[Block] 

368 

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 

379 

380 @property 

381 def described_type(self) -> type[Block]: 

382 """Get the described :class:`~physioblocks.computing.models.Block` type. 

383 

384 :return: the block type 

385 :rtype: type[Block] 

386 """ 

387 return self._described_type 

388 

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. 

394 

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 } 

409 

410 @property 

411 def flux_type(self) -> str: 

412 """ 

413 Get the type of the block's flux. 

414 

415 :return: the flux type 

416 :rtype: str 

417 """ 

418 return self._flux_type