HydPy 6.2.dev1__cp313-cp313-win_amd64.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 (890) hide show
  1. hydpy/__init__.py +275 -0
  2. hydpy/aliases.py +2554 -0
  3. hydpy/auxs/__init__.py +0 -0
  4. hydpy/auxs/anntools.py +1305 -0
  5. hydpy/auxs/armatools.py +883 -0
  6. hydpy/auxs/calibtools.py +3337 -0
  7. hydpy/auxs/interptools.py +1094 -0
  8. hydpy/auxs/iuhtools.py +543 -0
  9. hydpy/auxs/networktools.py +597 -0
  10. hydpy/auxs/ppolytools.py +809 -0
  11. hydpy/auxs/quadtools.py +61 -0
  12. hydpy/auxs/roottools.py +228 -0
  13. hydpy/auxs/smoothtools.py +273 -0
  14. hydpy/auxs/statstools.py +2125 -0
  15. hydpy/auxs/validtools.py +81 -0
  16. hydpy/conf/HydPyConfigBase.xsd +68637 -0
  17. hydpy/conf/HydPyConfigBase.xsdt +358 -0
  18. hydpy/conf/HydPyConfigMultipleRuns.xsd +25 -0
  19. hydpy/conf/HydPyConfigSingleRun.xsd +24 -0
  20. hydpy/conf/__init__.py +0 -0
  21. hydpy/conf/a_coefficients_explicit_lobatto_sequence.npy +0 -0
  22. hydpy/conf/support_points_for_smoothpar_logistic2.npy +0 -0
  23. hydpy/config.py +42 -0
  24. hydpy/core/__init__.py +0 -0
  25. hydpy/core/aliastools.py +214 -0
  26. hydpy/core/autodoctools.py +1947 -0
  27. hydpy/core/auxfiletools.py +1169 -0
  28. hydpy/core/devicetools.py +3810 -0
  29. hydpy/core/exceptiontools.py +269 -0
  30. hydpy/core/filetools.py +1985 -0
  31. hydpy/core/hydpytools.py +3089 -0
  32. hydpy/core/importtools.py +1398 -0
  33. hydpy/core/indextools.py +345 -0
  34. hydpy/core/itemtools.py +1849 -0
  35. hydpy/core/masktools.py +460 -0
  36. hydpy/core/modeltools.py +4868 -0
  37. hydpy/core/netcdftools.py +2683 -0
  38. hydpy/core/objecttools.py +2023 -0
  39. hydpy/core/optiontools.py +1045 -0
  40. hydpy/core/parametertools.py +4674 -0
  41. hydpy/core/printtools.py +222 -0
  42. hydpy/core/propertytools.py +643 -0
  43. hydpy/core/pubtools.py +254 -0
  44. hydpy/core/selectiontools.py +1571 -0
  45. hydpy/core/sequencetools.py +4476 -0
  46. hydpy/core/seriestools.py +339 -0
  47. hydpy/core/testtools.py +2483 -0
  48. hydpy/core/timetools.py +3567 -0
  49. hydpy/core/typingtools.py +333 -0
  50. hydpy/core/variabletools.py +2615 -0
  51. hydpy/cythons/__init__.py +24 -0
  52. hydpy/cythons/annutils.pxd +33 -0
  53. hydpy/cythons/annutils.pyi +25 -0
  54. hydpy/cythons/annutils.pyx +120 -0
  55. hydpy/cythons/autogen/__init__.py +0 -0
  56. hydpy/cythons/autogen/annutils.cp313-win_amd64.pyd +0 -0
  57. hydpy/cythons/autogen/annutils.pxd +42 -0
  58. hydpy/cythons/autogen/annutils.pyx +129 -0
  59. hydpy/cythons/autogen/c_arma.cp313-win_amd64.pyd +0 -0
  60. hydpy/cythons/autogen/c_arma.pxd +179 -0
  61. hydpy/cythons/autogen/c_arma.pyx +356 -0
  62. hydpy/cythons/autogen/c_arma_rimorido.cp313-win_amd64.pyd +0 -0
  63. hydpy/cythons/autogen/c_arma_rimorido.pxd +179 -0
  64. hydpy/cythons/autogen/c_arma_rimorido.pyx +356 -0
  65. hydpy/cythons/autogen/c_conv.cp313-win_amd64.pyd +0 -0
  66. hydpy/cythons/autogen/c_conv.pxd +198 -0
  67. hydpy/cythons/autogen/c_conv.pyx +491 -0
  68. hydpy/cythons/autogen/c_conv_idw.cp313-win_amd64.pyd +0 -0
  69. hydpy/cythons/autogen/c_conv_idw.pxd +124 -0
  70. hydpy/cythons/autogen/c_conv_idw.pyx +264 -0
  71. hydpy/cythons/autogen/c_conv_idw_ed.cp313-win_amd64.pyd +0 -0
  72. hydpy/cythons/autogen/c_conv_idw_ed.pxd +197 -0
  73. hydpy/cythons/autogen/c_conv_idw_ed.pyx +481 -0
  74. hydpy/cythons/autogen/c_conv_nn.cp313-win_amd64.pyd +0 -0
  75. hydpy/cythons/autogen/c_conv_nn.pxd +120 -0
  76. hydpy/cythons/autogen/c_conv_nn.pyx +224 -0
  77. hydpy/cythons/autogen/c_dam.cp313-win_amd64.pyd +0 -0
  78. hydpy/cythons/autogen/c_dam.pxd +805 -0
  79. hydpy/cythons/autogen/c_dam.pyx +1477 -0
  80. hydpy/cythons/autogen/c_dam_llake.cp313-win_amd64.pyd +0 -0
  81. hydpy/cythons/autogen/c_dam_llake.pxd +364 -0
  82. hydpy/cythons/autogen/c_dam_llake.pyx +705 -0
  83. hydpy/cythons/autogen/c_dam_lreservoir.cp313-win_amd64.pyd +0 -0
  84. hydpy/cythons/autogen/c_dam_lreservoir.pxd +365 -0
  85. hydpy/cythons/autogen/c_dam_lreservoir.pyx +708 -0
  86. hydpy/cythons/autogen/c_dam_lretention.cp313-win_amd64.pyd +0 -0
  87. hydpy/cythons/autogen/c_dam_lretention.pxd +340 -0
  88. hydpy/cythons/autogen/c_dam_lretention.pyx +625 -0
  89. hydpy/cythons/autogen/c_dam_pump.cp313-win_amd64.pyd +0 -0
  90. hydpy/cythons/autogen/c_dam_pump.pxd +402 -0
  91. hydpy/cythons/autogen/c_dam_pump.pyx +724 -0
  92. hydpy/cythons/autogen/c_dam_pump_sluice.cp313-win_amd64.pyd +0 -0
  93. hydpy/cythons/autogen/c_dam_pump_sluice.pxd +452 -0
  94. hydpy/cythons/autogen/c_dam_pump_sluice.pyx +829 -0
  95. hydpy/cythons/autogen/c_dam_sluice.cp313-win_amd64.pyd +0 -0
  96. hydpy/cythons/autogen/c_dam_sluice.pxd +404 -0
  97. hydpy/cythons/autogen/c_dam_sluice.pyx +726 -0
  98. hydpy/cythons/autogen/c_dam_v001.cp313-win_amd64.pyd +0 -0
  99. hydpy/cythons/autogen/c_dam_v001.pxd +452 -0
  100. hydpy/cythons/autogen/c_dam_v001.pyx +816 -0
  101. hydpy/cythons/autogen/c_dam_v002.cp313-win_amd64.pyd +0 -0
  102. hydpy/cythons/autogen/c_dam_v002.pxd +394 -0
  103. hydpy/cythons/autogen/c_dam_v002.pyx +703 -0
  104. hydpy/cythons/autogen/c_dam_v003.cp313-win_amd64.pyd +0 -0
  105. hydpy/cythons/autogen/c_dam_v003.pxd +417 -0
  106. hydpy/cythons/autogen/c_dam_v003.pyx +744 -0
  107. hydpy/cythons/autogen/c_dam_v004.cp313-win_amd64.pyd +0 -0
  108. hydpy/cythons/autogen/c_dam_v004.pxd +486 -0
  109. hydpy/cythons/autogen/c_dam_v004.pyx +891 -0
  110. hydpy/cythons/autogen/c_dam_v005.cp313-win_amd64.pyd +0 -0
  111. hydpy/cythons/autogen/c_dam_v005.pxd +524 -0
  112. hydpy/cythons/autogen/c_dam_v005.pyx +928 -0
  113. hydpy/cythons/autogen/c_dummy.cp313-win_amd64.pyd +0 -0
  114. hydpy/cythons/autogen/c_dummy.pxd +151 -0
  115. hydpy/cythons/autogen/c_dummy.pyx +263 -0
  116. hydpy/cythons/autogen/c_dummy_interceptedwater.cp313-win_amd64.pyd +0 -0
  117. hydpy/cythons/autogen/c_dummy_interceptedwater.pxd +69 -0
  118. hydpy/cythons/autogen/c_dummy_interceptedwater.pyx +104 -0
  119. hydpy/cythons/autogen/c_dummy_node2node.cp313-win_amd64.pyd +0 -0
  120. hydpy/cythons/autogen/c_dummy_node2node.pxd +89 -0
  121. hydpy/cythons/autogen/c_dummy_node2node.pyx +148 -0
  122. hydpy/cythons/autogen/c_dummy_snowalbedo.cp313-win_amd64.pyd +0 -0
  123. hydpy/cythons/autogen/c_dummy_snowalbedo.pxd +69 -0
  124. hydpy/cythons/autogen/c_dummy_snowalbedo.pyx +104 -0
  125. hydpy/cythons/autogen/c_dummy_snowcover.cp313-win_amd64.pyd +0 -0
  126. hydpy/cythons/autogen/c_dummy_snowcover.pxd +69 -0
  127. hydpy/cythons/autogen/c_dummy_snowcover.pyx +104 -0
  128. hydpy/cythons/autogen/c_dummy_snowycanopy.cp313-win_amd64.pyd +0 -0
  129. hydpy/cythons/autogen/c_dummy_snowycanopy.pxd +69 -0
  130. hydpy/cythons/autogen/c_dummy_snowycanopy.pyx +104 -0
  131. hydpy/cythons/autogen/c_dummy_soilwater.cp313-win_amd64.pyd +0 -0
  132. hydpy/cythons/autogen/c_dummy_soilwater.pxd +69 -0
  133. hydpy/cythons/autogen/c_dummy_soilwater.pyx +104 -0
  134. hydpy/cythons/autogen/c_evap.cp313-win_amd64.pyd +0 -0
  135. hydpy/cythons/autogen/c_evap.pxd +1029 -0
  136. hydpy/cythons/autogen/c_evap.pyx +2601 -0
  137. hydpy/cythons/autogen/c_evap_aet_hbv96.cp313-win_amd64.pyd +0 -0
  138. hydpy/cythons/autogen/c_evap_aet_hbv96.pxd +227 -0
  139. hydpy/cythons/autogen/c_evap_aet_hbv96.pyx +584 -0
  140. hydpy/cythons/autogen/c_evap_aet_minhas.cp313-win_amd64.pyd +0 -0
  141. hydpy/cythons/autogen/c_evap_aet_minhas.pxd +193 -0
  142. hydpy/cythons/autogen/c_evap_aet_minhas.pyx +478 -0
  143. hydpy/cythons/autogen/c_evap_aet_morsim.cp313-win_amd64.pyd +0 -0
  144. hydpy/cythons/autogen/c_evap_aet_morsim.pxd +681 -0
  145. hydpy/cythons/autogen/c_evap_aet_morsim.pyx +1642 -0
  146. hydpy/cythons/autogen/c_evap_pet_ambav1.cp313-win_amd64.pyd +0 -0
  147. hydpy/cythons/autogen/c_evap_pet_ambav1.pxd +532 -0
  148. hydpy/cythons/autogen/c_evap_pet_ambav1.pyx +1296 -0
  149. hydpy/cythons/autogen/c_evap_pet_hbv96.cp313-win_amd64.pyd +0 -0
  150. hydpy/cythons/autogen/c_evap_pet_hbv96.pxd +179 -0
  151. hydpy/cythons/autogen/c_evap_pet_hbv96.pyx +328 -0
  152. hydpy/cythons/autogen/c_evap_pet_m.cp313-win_amd64.pyd +0 -0
  153. hydpy/cythons/autogen/c_evap_pet_m.pxd +124 -0
  154. hydpy/cythons/autogen/c_evap_pet_m.pyx +214 -0
  155. hydpy/cythons/autogen/c_evap_pet_mlc.cp313-win_amd64.pyd +0 -0
  156. hydpy/cythons/autogen/c_evap_pet_mlc.pxd +126 -0
  157. hydpy/cythons/autogen/c_evap_pet_mlc.pyx +214 -0
  158. hydpy/cythons/autogen/c_evap_ret_fao56.cp313-win_amd64.pyd +0 -0
  159. hydpy/cythons/autogen/c_evap_ret_fao56.pxd +305 -0
  160. hydpy/cythons/autogen/c_evap_ret_fao56.pyx +624 -0
  161. hydpy/cythons/autogen/c_evap_ret_io.cp313-win_amd64.pyd +0 -0
  162. hydpy/cythons/autogen/c_evap_ret_io.pxd +112 -0
  163. hydpy/cythons/autogen/c_evap_ret_io.pyx +176 -0
  164. hydpy/cythons/autogen/c_evap_ret_tw2002.cp313-win_amd64.pyd +0 -0
  165. hydpy/cythons/autogen/c_evap_ret_tw2002.pxd +139 -0
  166. hydpy/cythons/autogen/c_evap_ret_tw2002.pyx +273 -0
  167. hydpy/cythons/autogen/c_exch.cp313-win_amd64.pyd +0 -0
  168. hydpy/cythons/autogen/c_exch.pxd +230 -0
  169. hydpy/cythons/autogen/c_exch.pyx +462 -0
  170. hydpy/cythons/autogen/c_exch_branch_hbv96.cp313-win_amd64.pyd +0 -0
  171. hydpy/cythons/autogen/c_exch_branch_hbv96.pxd +134 -0
  172. hydpy/cythons/autogen/c_exch_branch_hbv96.pyx +255 -0
  173. hydpy/cythons/autogen/c_exch_waterlevel.cp313-win_amd64.pyd +0 -0
  174. hydpy/cythons/autogen/c_exch_waterlevel.pxd +54 -0
  175. hydpy/cythons/autogen/c_exch_waterlevel.pyx +78 -0
  176. hydpy/cythons/autogen/c_exch_weir_hbv96.cp313-win_amd64.pyd +0 -0
  177. hydpy/cythons/autogen/c_exch_weir_hbv96.pxd +156 -0
  178. hydpy/cythons/autogen/c_exch_weir_hbv96.pyx +282 -0
  179. hydpy/cythons/autogen/c_ga.cp313-win_amd64.pyd +0 -0
  180. hydpy/cythons/autogen/c_ga.pxd +353 -0
  181. hydpy/cythons/autogen/c_ga.pyx +1204 -0
  182. hydpy/cythons/autogen/c_ga_garto.cp313-win_amd64.pyd +0 -0
  183. hydpy/cythons/autogen/c_ga_garto.pxd +330 -0
  184. hydpy/cythons/autogen/c_ga_garto.pyx +1105 -0
  185. hydpy/cythons/autogen/c_ga_garto_submodel1.cp313-win_amd64.pyd +0 -0
  186. hydpy/cythons/autogen/c_ga_garto_submodel1.pxd +236 -0
  187. hydpy/cythons/autogen/c_ga_garto_submodel1.pyx +944 -0
  188. hydpy/cythons/autogen/c_gland.cp313-win_amd64.pyd +0 -0
  189. hydpy/cythons/autogen/c_gland.pxd +437 -0
  190. hydpy/cythons/autogen/c_gland.pyx +726 -0
  191. hydpy/cythons/autogen/c_gland_gr4.cp313-win_amd64.pyd +0 -0
  192. hydpy/cythons/autogen/c_gland_gr4.pxd +382 -0
  193. hydpy/cythons/autogen/c_gland_gr4.pyx +605 -0
  194. hydpy/cythons/autogen/c_gland_gr5.cp313-win_amd64.pyd +0 -0
  195. hydpy/cythons/autogen/c_gland_gr5.pxd +368 -0
  196. hydpy/cythons/autogen/c_gland_gr5.pyx +568 -0
  197. hydpy/cythons/autogen/c_gland_gr6.cp313-win_amd64.pyd +0 -0
  198. hydpy/cythons/autogen/c_gland_gr6.pxd +420 -0
  199. hydpy/cythons/autogen/c_gland_gr6.pyx +673 -0
  200. hydpy/cythons/autogen/c_hland.cp313-win_amd64.pyd +0 -0
  201. hydpy/cythons/autogen/c_hland.pxd +855 -0
  202. hydpy/cythons/autogen/c_hland.pyx +2486 -0
  203. hydpy/cythons/autogen/c_hland_96.cp313-win_amd64.pyd +0 -0
  204. hydpy/cythons/autogen/c_hland_96.pxd +631 -0
  205. hydpy/cythons/autogen/c_hland_96.pyx +1724 -0
  206. hydpy/cythons/autogen/c_hland_96c.cp313-win_amd64.pyd +0 -0
  207. hydpy/cythons/autogen/c_hland_96c.pxd +621 -0
  208. hydpy/cythons/autogen/c_hland_96c.pyx +1822 -0
  209. hydpy/cythons/autogen/c_hland_96p.cp313-win_amd64.pyd +0 -0
  210. hydpy/cythons/autogen/c_hland_96p.pxd +683 -0
  211. hydpy/cythons/autogen/c_hland_96p.pyx +1911 -0
  212. hydpy/cythons/autogen/c_kinw.cp313-win_amd64.pyd +0 -0
  213. hydpy/cythons/autogen/c_kinw.pxd +509 -0
  214. hydpy/cythons/autogen/c_kinw.pyx +965 -0
  215. hydpy/cythons/autogen/c_kinw_williams.cp313-win_amd64.pyd +0 -0
  216. hydpy/cythons/autogen/c_kinw_williams.pxd +409 -0
  217. hydpy/cythons/autogen/c_kinw_williams.pyx +763 -0
  218. hydpy/cythons/autogen/c_kinw_williams_ext.cp313-win_amd64.pyd +0 -0
  219. hydpy/cythons/autogen/c_kinw_williams_ext.pxd +220 -0
  220. hydpy/cythons/autogen/c_kinw_williams_ext.pyx +440 -0
  221. hydpy/cythons/autogen/c_lland.cp313-win_amd64.pyd +0 -0
  222. hydpy/cythons/autogen/c_lland.pxd +1386 -0
  223. hydpy/cythons/autogen/c_lland.pyx +3679 -0
  224. hydpy/cythons/autogen/c_lland_dd.cp313-win_amd64.pyd +0 -0
  225. hydpy/cythons/autogen/c_lland_dd.pxd +679 -0
  226. hydpy/cythons/autogen/c_lland_dd.pyx +1719 -0
  227. hydpy/cythons/autogen/c_lland_knauf.cp313-win_amd64.pyd +0 -0
  228. hydpy/cythons/autogen/c_lland_knauf.pxd +1096 -0
  229. hydpy/cythons/autogen/c_lland_knauf.pyx +2784 -0
  230. hydpy/cythons/autogen/c_lland_knauf_ic.cp313-win_amd64.pyd +0 -0
  231. hydpy/cythons/autogen/c_lland_knauf_ic.pxd +1369 -0
  232. hydpy/cythons/autogen/c_lland_knauf_ic.pyx +3625 -0
  233. hydpy/cythons/autogen/c_meteo.cp313-win_amd64.pyd +0 -0
  234. hydpy/cythons/autogen/c_meteo.pxd +469 -0
  235. hydpy/cythons/autogen/c_meteo.pyx +879 -0
  236. hydpy/cythons/autogen/c_meteo_clear_glob_io.cp313-win_amd64.pyd +0 -0
  237. hydpy/cythons/autogen/c_meteo_clear_glob_io.pxd +75 -0
  238. hydpy/cythons/autogen/c_meteo_clear_glob_io.pyx +107 -0
  239. hydpy/cythons/autogen/c_meteo_glob_fao56.cp313-win_amd64.pyd +0 -0
  240. hydpy/cythons/autogen/c_meteo_glob_fao56.pxd +209 -0
  241. hydpy/cythons/autogen/c_meteo_glob_fao56.pyx +339 -0
  242. hydpy/cythons/autogen/c_meteo_glob_io.cp313-win_amd64.pyd +0 -0
  243. hydpy/cythons/autogen/c_meteo_glob_io.pxd +63 -0
  244. hydpy/cythons/autogen/c_meteo_glob_io.pyx +91 -0
  245. hydpy/cythons/autogen/c_meteo_glob_morsim.cp313-win_amd64.pyd +0 -0
  246. hydpy/cythons/autogen/c_meteo_glob_morsim.pxd +289 -0
  247. hydpy/cythons/autogen/c_meteo_glob_morsim.pyx +527 -0
  248. hydpy/cythons/autogen/c_meteo_precip_io.cp313-win_amd64.pyd +0 -0
  249. hydpy/cythons/autogen/c_meteo_precip_io.pxd +112 -0
  250. hydpy/cythons/autogen/c_meteo_precip_io.pyx +176 -0
  251. hydpy/cythons/autogen/c_meteo_psun_sun_glob_io.cp313-win_amd64.pyd +0 -0
  252. hydpy/cythons/autogen/c_meteo_psun_sun_glob_io.pxd +87 -0
  253. hydpy/cythons/autogen/c_meteo_psun_sun_glob_io.pyx +123 -0
  254. hydpy/cythons/autogen/c_meteo_sun_fao56.cp313-win_amd64.pyd +0 -0
  255. hydpy/cythons/autogen/c_meteo_sun_fao56.pxd +209 -0
  256. hydpy/cythons/autogen/c_meteo_sun_fao56.pyx +343 -0
  257. hydpy/cythons/autogen/c_meteo_sun_morsim.cp313-win_amd64.pyd +0 -0
  258. hydpy/cythons/autogen/c_meteo_sun_morsim.pxd +286 -0
  259. hydpy/cythons/autogen/c_meteo_sun_morsim.pyx +519 -0
  260. hydpy/cythons/autogen/c_meteo_temp_io.cp313-win_amd64.pyd +0 -0
  261. hydpy/cythons/autogen/c_meteo_temp_io.pxd +112 -0
  262. hydpy/cythons/autogen/c_meteo_temp_io.pyx +176 -0
  263. hydpy/cythons/autogen/c_musk.cp313-win_amd64.pyd +0 -0
  264. hydpy/cythons/autogen/c_musk.pxd +282 -0
  265. hydpy/cythons/autogen/c_musk.pyx +605 -0
  266. hydpy/cythons/autogen/c_musk_classic.cp313-win_amd64.pyd +0 -0
  267. hydpy/cythons/autogen/c_musk_classic.pxd +138 -0
  268. hydpy/cythons/autogen/c_musk_classic.pyx +226 -0
  269. hydpy/cythons/autogen/c_musk_mct.cp313-win_amd64.pyd +0 -0
  270. hydpy/cythons/autogen/c_musk_mct.pxd +282 -0
  271. hydpy/cythons/autogen/c_musk_mct.pyx +609 -0
  272. hydpy/cythons/autogen/c_rconc.cp313-win_amd64.pyd +0 -0
  273. hydpy/cythons/autogen/c_rconc.pxd +119 -0
  274. hydpy/cythons/autogen/c_rconc.pyx +174 -0
  275. hydpy/cythons/autogen/c_rconc_nash.cp313-win_amd64.pyd +0 -0
  276. hydpy/cythons/autogen/c_rconc_nash.pxd +111 -0
  277. hydpy/cythons/autogen/c_rconc_nash.pyx +185 -0
  278. hydpy/cythons/autogen/c_rconc_uh.cp313-win_amd64.pyd +0 -0
  279. hydpy/cythons/autogen/c_rconc_uh.pxd +92 -0
  280. hydpy/cythons/autogen/c_rconc_uh.pyx +125 -0
  281. hydpy/cythons/autogen/c_sw1d.cp313-win_amd64.pyd +0 -0
  282. hydpy/cythons/autogen/c_sw1d.pxd +511 -0
  283. hydpy/cythons/autogen/c_sw1d.pyx +1263 -0
  284. hydpy/cythons/autogen/c_sw1d_channel.cp313-win_amd64.pyd +0 -0
  285. hydpy/cythons/autogen/c_sw1d_channel.pxd +119 -0
  286. hydpy/cythons/autogen/c_sw1d_channel.pyx +300 -0
  287. hydpy/cythons/autogen/c_sw1d_gate_out.cp313-win_amd64.pyd +0 -0
  288. hydpy/cythons/autogen/c_sw1d_gate_out.pxd +240 -0
  289. hydpy/cythons/autogen/c_sw1d_gate_out.pyx +476 -0
  290. hydpy/cythons/autogen/c_sw1d_lias.cp313-win_amd64.pyd +0 -0
  291. hydpy/cythons/autogen/c_sw1d_lias.pxd +320 -0
  292. hydpy/cythons/autogen/c_sw1d_lias.pyx +619 -0
  293. hydpy/cythons/autogen/c_sw1d_lias_sluice.cp313-win_amd64.pyd +0 -0
  294. hydpy/cythons/autogen/c_sw1d_lias_sluice.pxd +325 -0
  295. hydpy/cythons/autogen/c_sw1d_lias_sluice.pyx +644 -0
  296. hydpy/cythons/autogen/c_sw1d_network.cp313-win_amd64.pyd +0 -0
  297. hydpy/cythons/autogen/c_sw1d_network.pxd +90 -0
  298. hydpy/cythons/autogen/c_sw1d_network.pyx +246 -0
  299. hydpy/cythons/autogen/c_sw1d_pump.cp313-win_amd64.pyd +0 -0
  300. hydpy/cythons/autogen/c_sw1d_pump.pxd +256 -0
  301. hydpy/cythons/autogen/c_sw1d_pump.pyx +502 -0
  302. hydpy/cythons/autogen/c_sw1d_q_in.cp313-win_amd64.pyd +0 -0
  303. hydpy/cythons/autogen/c_sw1d_q_in.pxd +224 -0
  304. hydpy/cythons/autogen/c_sw1d_q_in.pyx +383 -0
  305. hydpy/cythons/autogen/c_sw1d_q_out.cp313-win_amd64.pyd +0 -0
  306. hydpy/cythons/autogen/c_sw1d_q_out.pxd +224 -0
  307. hydpy/cythons/autogen/c_sw1d_q_out.pyx +383 -0
  308. hydpy/cythons/autogen/c_sw1d_storage.cp313-win_amd64.pyd +0 -0
  309. hydpy/cythons/autogen/c_sw1d_storage.pxd +193 -0
  310. hydpy/cythons/autogen/c_sw1d_storage.pyx +349 -0
  311. hydpy/cythons/autogen/c_sw1d_weir_out.cp313-win_amd64.pyd +0 -0
  312. hydpy/cythons/autogen/c_sw1d_weir_out.pxd +212 -0
  313. hydpy/cythons/autogen/c_sw1d_weir_out.pyx +404 -0
  314. hydpy/cythons/autogen/c_test.cp313-win_amd64.pyd +0 -0
  315. hydpy/cythons/autogen/c_test.pxd +175 -0
  316. hydpy/cythons/autogen/c_test.pyx +348 -0
  317. hydpy/cythons/autogen/c_test_discontinous.cp313-win_amd64.pyd +0 -0
  318. hydpy/cythons/autogen/c_test_discontinous.pxd +146 -0
  319. hydpy/cythons/autogen/c_test_discontinous.pyx +256 -0
  320. hydpy/cythons/autogen/c_test_stiff0d.cp313-win_amd64.pyd +0 -0
  321. hydpy/cythons/autogen/c_test_stiff0d.pxd +146 -0
  322. hydpy/cythons/autogen/c_test_stiff0d.pyx +250 -0
  323. hydpy/cythons/autogen/c_test_stiff1d.cp313-win_amd64.pyd +0 -0
  324. hydpy/cythons/autogen/c_test_stiff1d.pxd +145 -0
  325. hydpy/cythons/autogen/c_test_stiff1d.pyx +294 -0
  326. hydpy/cythons/autogen/c_whmod.cp313-win_amd64.pyd +0 -0
  327. hydpy/cythons/autogen/c_whmod.pxd +482 -0
  328. hydpy/cythons/autogen/c_whmod.pyx +1156 -0
  329. hydpy/cythons/autogen/c_whmod_rural.cp313-win_amd64.pyd +0 -0
  330. hydpy/cythons/autogen/c_whmod_rural.pxd +411 -0
  331. hydpy/cythons/autogen/c_whmod_rural.pyx +982 -0
  332. hydpy/cythons/autogen/c_whmod_urban.cp313-win_amd64.pyd +0 -0
  333. hydpy/cythons/autogen/c_whmod_urban.pxd +482 -0
  334. hydpy/cythons/autogen/c_whmod_urban.pyx +1155 -0
  335. hydpy/cythons/autogen/c_wland.cp313-win_amd64.pyd +0 -0
  336. hydpy/cythons/autogen/c_wland.pxd +842 -0
  337. hydpy/cythons/autogen/c_wland.pyx +1890 -0
  338. hydpy/cythons/autogen/c_wland_gd.cp313-win_amd64.pyd +0 -0
  339. hydpy/cythons/autogen/c_wland_gd.pxd +829 -0
  340. hydpy/cythons/autogen/c_wland_gd.pyx +1847 -0
  341. hydpy/cythons/autogen/c_wland_wag.cp313-win_amd64.pyd +0 -0
  342. hydpy/cythons/autogen/c_wland_wag.pxd +810 -0
  343. hydpy/cythons/autogen/c_wland_wag.pyx +1780 -0
  344. hydpy/cythons/autogen/c_wq.cp313-win_amd64.pyd +0 -0
  345. hydpy/cythons/autogen/c_wq.pxd +275 -0
  346. hydpy/cythons/autogen/c_wq.pyx +652 -0
  347. hydpy/cythons/autogen/c_wq_trapeze.cp313-win_amd64.pyd +0 -0
  348. hydpy/cythons/autogen/c_wq_trapeze.pxd +170 -0
  349. hydpy/cythons/autogen/c_wq_trapeze.pyx +400 -0
  350. hydpy/cythons/autogen/c_wq_trapeze_strickler.cp313-win_amd64.pyd +0 -0
  351. hydpy/cythons/autogen/c_wq_trapeze_strickler.pxd +243 -0
  352. hydpy/cythons/autogen/c_wq_trapeze_strickler.pyx +578 -0
  353. hydpy/cythons/autogen/c_wq_walrus.cp313-win_amd64.pyd +0 -0
  354. hydpy/cythons/autogen/c_wq_walrus.pxd +61 -0
  355. hydpy/cythons/autogen/c_wq_walrus.pyx +82 -0
  356. hydpy/cythons/autogen/configutils.cp313-win_amd64.pyd +0 -0
  357. hydpy/cythons/autogen/configutils.pxd +17 -0
  358. hydpy/cythons/autogen/configutils.pyx +119 -0
  359. hydpy/cythons/autogen/interfaceutils.cp313-win_amd64.pyd +0 -0
  360. hydpy/cythons/autogen/interfaceutils.pxd +31 -0
  361. hydpy/cythons/autogen/interfaceutils.pyx +82 -0
  362. hydpy/cythons/autogen/interputils.cp313-win_amd64.pyd +0 -0
  363. hydpy/cythons/autogen/interputils.pxd +42 -0
  364. hydpy/cythons/autogen/interputils.pyx +118 -0
  365. hydpy/cythons/autogen/masterinterface.cp313-win_amd64.pyd +0 -0
  366. hydpy/cythons/autogen/masterinterface.pxd +153 -0
  367. hydpy/cythons/autogen/masterinterface.pyx +222 -0
  368. hydpy/cythons/autogen/pointerutils.cp313-win_amd64.pyd +0 -0
  369. hydpy/cythons/autogen/pointerutils.pxd +31 -0
  370. hydpy/cythons/autogen/pointerutils.pyx +650 -0
  371. hydpy/cythons/autogen/ppolyutils.cp313-win_amd64.pyd +0 -0
  372. hydpy/cythons/autogen/ppolyutils.pxd +35 -0
  373. hydpy/cythons/autogen/ppolyutils.pyx +59 -0
  374. hydpy/cythons/autogen/quadutils.cp313-win_amd64.pyd +0 -0
  375. hydpy/cythons/autogen/quadutils.pxd +26 -0
  376. hydpy/cythons/autogen/quadutils.pyx +973 -0
  377. hydpy/cythons/autogen/rootutils.cp313-win_amd64.pyd +0 -0
  378. hydpy/cythons/autogen/rootutils.pxd +28 -0
  379. hydpy/cythons/autogen/rootutils.pyx +109 -0
  380. hydpy/cythons/autogen/sequenceutils.cp313-win_amd64.pyd +0 -0
  381. hydpy/cythons/autogen/sequenceutils.pxd +45 -0
  382. hydpy/cythons/autogen/sequenceutils.pyx +101 -0
  383. hydpy/cythons/autogen/smoothutils.cp313-win_amd64.pyd +0 -0
  384. hydpy/cythons/autogen/smoothutils.pxd +29 -0
  385. hydpy/cythons/autogen/smoothutils.pyx +833 -0
  386. hydpy/cythons/configutils.pxd +8 -0
  387. hydpy/cythons/configutils.pyi +5 -0
  388. hydpy/cythons/configutils.pyx +110 -0
  389. hydpy/cythons/interfaceutils.pxd +22 -0
  390. hydpy/cythons/interfaceutils.pyi +15 -0
  391. hydpy/cythons/interfaceutils.pyx +73 -0
  392. hydpy/cythons/interputils.pxd +33 -0
  393. hydpy/cythons/interputils.pyi +32 -0
  394. hydpy/cythons/interputils.pyx +109 -0
  395. hydpy/cythons/modelutils.py +2990 -0
  396. hydpy/cythons/pointerutils.pxd +22 -0
  397. hydpy/cythons/pointerutils.pyi +89 -0
  398. hydpy/cythons/pointerutils.pyx +641 -0
  399. hydpy/cythons/ppolyutils.pxd +26 -0
  400. hydpy/cythons/ppolyutils.pyi +21 -0
  401. hydpy/cythons/ppolyutils.pyx +50 -0
  402. hydpy/cythons/quadutils.pxd +17 -0
  403. hydpy/cythons/quadutils.pyi +13 -0
  404. hydpy/cythons/quadutils.pyx +964 -0
  405. hydpy/cythons/rootutils.pxd +19 -0
  406. hydpy/cythons/rootutils.pyi +21 -0
  407. hydpy/cythons/rootutils.pyx +100 -0
  408. hydpy/cythons/sequenceutils.pxd +36 -0
  409. hydpy/cythons/sequenceutils.pyi +7 -0
  410. hydpy/cythons/sequenceutils.pyx +92 -0
  411. hydpy/cythons/smoothutils.pxd +20 -0
  412. hydpy/cythons/smoothutils.pyi +15 -0
  413. hydpy/cythons/smoothutils.pyx +824 -0
  414. hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/land_dill_assl.py +13 -0
  415. hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/land_lahn_kalk.py +13 -0
  416. hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/land_lahn_leun.py +14 -0
  417. hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/land_lahn_marb.py +13 -0
  418. hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/stream_dill_assl_lahn_leun.py +5 -0
  419. hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/stream_lahn_leun_lahn_kalk.py +5 -0
  420. hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/stream_lahn_marb_lahn_leun.py +5 -0
  421. hydpy/data/HydPy-H-Lahn/control/default/land.py +9 -0
  422. hydpy/data/HydPy-H-Lahn/control/default/land_dill_assl.py +57 -0
  423. hydpy/data/HydPy-H-Lahn/control/default/land_lahn_kalk.py +57 -0
  424. hydpy/data/HydPy-H-Lahn/control/default/land_lahn_leun.py +56 -0
  425. hydpy/data/HydPy-H-Lahn/control/default/land_lahn_marb.py +57 -0
  426. hydpy/data/HydPy-H-Lahn/control/default/stream_dill_assl_lahn_leun.py +7 -0
  427. hydpy/data/HydPy-H-Lahn/control/default/stream_lahn_leun_lahn_kalk.py +7 -0
  428. hydpy/data/HydPy-H-Lahn/control/default/stream_lahn_marb_lahn_leun.py +7 -0
  429. hydpy/data/HydPy-H-Lahn/multiple_runs.xml +309 -0
  430. hydpy/data/HydPy-H-Lahn/multiple_runs_alpha.xml +71 -0
  431. hydpy/data/HydPy-H-Lahn/network/default/headwaters.py +11 -0
  432. hydpy/data/HydPy-H-Lahn/network/default/nonheadwaters.py +11 -0
  433. hydpy/data/HydPy-H-Lahn/network/default/streams.py +8 -0
  434. hydpy/data/HydPy-H-Lahn/series/default/dill_assl_obs_q.asc +11387 -0
  435. hydpy/data/HydPy-H-Lahn/series/default/evap_pet_hbv96_input_normalairtemperature.nc +0 -0
  436. hydpy/data/HydPy-H-Lahn/series/default/evap_pet_hbv96_input_normalevapotranspiration.nc +0 -0
  437. hydpy/data/HydPy-H-Lahn/series/default/hland_96_input_p.nc +0 -0
  438. hydpy/data/HydPy-H-Lahn/series/default/hland_96_input_t.nc +0 -0
  439. hydpy/data/HydPy-H-Lahn/series/default/lahn_kalk_obs_q.asc +11387 -0
  440. hydpy/data/HydPy-H-Lahn/series/default/lahn_leun_obs_q.asc +11387 -0
  441. hydpy/data/HydPy-H-Lahn/series/default/lahn_marb_obs_q.asc +11387 -0
  442. hydpy/data/HydPy-H-Lahn/series/default/land_dill_assl_evap_pet_hbv96_input_normalairtemperature.asc +11387 -0
  443. hydpy/data/HydPy-H-Lahn/series/default/land_dill_assl_evap_pet_hbv96_input_normalevapotranspiration.asc +11387 -0
  444. hydpy/data/HydPy-H-Lahn/series/default/land_dill_assl_hland_96_input_p.asc +11387 -0
  445. hydpy/data/HydPy-H-Lahn/series/default/land_dill_assl_hland_96_input_t.asc +11387 -0
  446. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_kalk_evap_pet_hbv96_input_normalairtemperature.asc +11387 -0
  447. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_kalk_evap_pet_hbv96_input_normalevapotranspiration.asc +11387 -0
  448. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_kalk_hland_96_input_p.asc +11387 -0
  449. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_kalk_hland_96_input_t.asc +11387 -0
  450. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_leun_evap_pet_hbv96_input_normalairtemperature.asc +11387 -0
  451. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_leun_evap_pet_hbv96_input_normalevapotranspiration.asc +11387 -0
  452. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_leun_hland_96_input_p.asc +11387 -0
  453. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_leun_hland_96_input_t.asc +11387 -0
  454. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_marb_evap_pet_hbv96_input_normalairtemperature.asc +11387 -0
  455. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_marb_evap_pet_hbv96_input_normalevapotranspiration.asc +11387 -0
  456. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_marb_hland_96_input_p.asc +11387 -0
  457. hydpy/data/HydPy-H-Lahn/series/default/land_lahn_marb_hland_96_input_t.asc +11387 -0
  458. hydpy/data/HydPy-H-Lahn/series/default/obs_q.nc +0 -0
  459. hydpy/data/HydPy-H-Lahn/single_run.xml +152 -0
  460. hydpy/data/HydPy-H-Lahn/single_run.xmlt +143 -0
  461. hydpy/data/__init__.py +17 -0
  462. hydpy/docs/__init__.py +0 -0
  463. hydpy/docs/autofigs/__init__.py +0 -0
  464. hydpy/docs/bib/__init__.py +0 -0
  465. hydpy/docs/bib/refs.bib +566 -0
  466. hydpy/docs/combine_docversions.py +133 -0
  467. hydpy/docs/draw_model_sketches.py +1301 -0
  468. hydpy/docs/enable_autodoc.py +7 -0
  469. hydpy/docs/figs/HydPy-G-GR4.png +0 -0
  470. hydpy/docs/figs/HydPy-G-GR5.png +0 -0
  471. hydpy/docs/figs/HydPy-G-GR6.png +0 -0
  472. hydpy/docs/figs/HydPy-H-HBV96-COSERO.png +0 -0
  473. hydpy/docs/figs/HydPy-H-HBV96-PREVAH.png +0 -0
  474. hydpy/docs/figs/HydPy-H-HBV96.png +0 -0
  475. hydpy/docs/figs/HydPy-H-Lahn.png +0 -0
  476. hydpy/docs/figs/HydPy-KinW-Williams.png +0 -0
  477. hydpy/docs/figs/HydPy-L-DD.png +0 -0
  478. hydpy/docs/figs/HydPy-W-Wag.png +0 -0
  479. hydpy/docs/figs/HydPy_Logo.png +0 -0
  480. hydpy/docs/figs/HydPy_Logo_Text.png +0 -0
  481. hydpy/docs/figs/IDLE-editor.png +0 -0
  482. hydpy/docs/figs/IDLE-shell.png +0 -0
  483. hydpy/docs/figs/LAWA_river-basin-bumbers.png +0 -0
  484. hydpy/docs/figs/__init__.py +0 -0
  485. hydpy/docs/html_/__init__.py +0 -0
  486. hydpy/docs/polish_html.py +57 -0
  487. hydpy/docs/prepare.py +224 -0
  488. hydpy/docs/publish_docs.py +53 -0
  489. hydpy/docs/rst/HydPy-ARMA.rst +27 -0
  490. hydpy/docs/rst/HydPy-Conv.rst +22 -0
  491. hydpy/docs/rst/HydPy-Dam.rst +79 -0
  492. hydpy/docs/rst/HydPy-Dummy.rst +21 -0
  493. hydpy/docs/rst/HydPy-Evap.rst +26 -0
  494. hydpy/docs/rst/HydPy-Exch.rst +36 -0
  495. hydpy/docs/rst/HydPy-G.rst +40 -0
  496. hydpy/docs/rst/HydPy-GA.rst +34 -0
  497. hydpy/docs/rst/HydPy-H.rst +24 -0
  498. hydpy/docs/rst/HydPy-KinW.rst +32 -0
  499. hydpy/docs/rst/HydPy-L.rst +42 -0
  500. hydpy/docs/rst/HydPy-Meteo.rst +28 -0
  501. hydpy/docs/rst/HydPy-Musk.rst +21 -0
  502. hydpy/docs/rst/HydPy-Rconc.rst +17 -0
  503. hydpy/docs/rst/HydPy-SW1D.rst +49 -0
  504. hydpy/docs/rst/HydPy-Test.rst +19 -0
  505. hydpy/docs/rst/HydPy-W.rst +20 -0
  506. hydpy/docs/rst/HydPy-WHMod.rst +19 -0
  507. hydpy/docs/rst/HydPy-WQ.rst +20 -0
  508. hydpy/docs/rst/__init__.py +0 -0
  509. hydpy/docs/rst/additional_repositories.rst +40 -0
  510. hydpy/docs/rst/auxiliaries.rst +31 -0
  511. hydpy/docs/rst/continuous_integration.rst +75 -0
  512. hydpy/docs/rst/core.rst +75 -0
  513. hydpy/docs/rst/cythons.rst +47 -0
  514. hydpy/docs/rst/definitions.rst +506 -0
  515. hydpy/docs/rst/developer_guide.rst +54 -0
  516. hydpy/docs/rst/example_projects.rst +40 -0
  517. hydpy/docs/rst/execution.rst +22 -0
  518. hydpy/docs/rst/framework_tools.rst +56 -0
  519. hydpy/docs/rst/how_to_read_the_reference_manual.rst +156 -0
  520. hydpy/docs/rst/hydpydependencies.rst +55 -0
  521. hydpy/docs/rst/index.rst +125 -0
  522. hydpy/docs/rst/installation.rst +155 -0
  523. hydpy/docs/rst/model_families.rst +79 -0
  524. hydpy/docs/rst/model_overview.rst +291 -0
  525. hydpy/docs/rst/modelimports.rst +10 -0
  526. hydpy/docs/rst/options.rst +119 -0
  527. hydpy/docs/rst/programming_style.rst +572 -0
  528. hydpy/docs/rst/project_structure.rst +520 -0
  529. hydpy/docs/rst/quickstart.rst +304 -0
  530. hydpy/docs/rst/reference_manual.rst +29 -0
  531. hydpy/docs/rst/required_tools.rst +50 -0
  532. hydpy/docs/rst/simulation.rst +514 -0
  533. hydpy/docs/rst/submodel_interfaces.rst +32 -0
  534. hydpy/docs/rst/tests_and_documentation.rst +85 -0
  535. hydpy/docs/rst/user_guide.rst +38 -0
  536. hydpy/docs/rst/version_control.rst +116 -0
  537. hydpy/docs/rst/zbibliography.rst +8 -0
  538. hydpy/docs/sphinx/__init__.py +0 -0
  539. hydpy/docs/sphinx/_themes/basic_hydpy/changes/frameset.html +11 -0
  540. hydpy/docs/sphinx/_themes/basic_hydpy/changes/rstsource.html +15 -0
  541. hydpy/docs/sphinx/_themes/basic_hydpy/changes/versionchanges.html +33 -0
  542. hydpy/docs/sphinx/_themes/basic_hydpy/defindex.html +35 -0
  543. hydpy/docs/sphinx/_themes/basic_hydpy/domainindex.html +56 -0
  544. hydpy/docs/sphinx/_themes/basic_hydpy/genindex-single.html +63 -0
  545. hydpy/docs/sphinx/_themes/basic_hydpy/genindex-split.html +41 -0
  546. hydpy/docs/sphinx/_themes/basic_hydpy/genindex.html +76 -0
  547. hydpy/docs/sphinx/_themes/basic_hydpy/globaltoc.html +11 -0
  548. hydpy/docs/sphinx/_themes/basic_hydpy/layout.html +221 -0
  549. hydpy/docs/sphinx/_themes/basic_hydpy/localtoc.html +15 -0
  550. hydpy/docs/sphinx/_themes/basic_hydpy/opensearch.xml +13 -0
  551. hydpy/docs/sphinx/_themes/basic_hydpy/page.html +13 -0
  552. hydpy/docs/sphinx/_themes/basic_hydpy/relations.html +23 -0
  553. hydpy/docs/sphinx/_themes/basic_hydpy/search.html +65 -0
  554. hydpy/docs/sphinx/_themes/basic_hydpy/searchbox.html +21 -0
  555. hydpy/docs/sphinx/_themes/basic_hydpy/searchfield.html +23 -0
  556. hydpy/docs/sphinx/_themes/basic_hydpy/sourcelink.html +18 -0
  557. hydpy/docs/sphinx/_themes/basic_hydpy/static/basic.css_t +925 -0
  558. hydpy/docs/sphinx/_themes/basic_hydpy/static/doctools.js +156 -0
  559. hydpy/docs/sphinx/_themes/basic_hydpy/static/documentation_options.js_t +13 -0
  560. hydpy/docs/sphinx/_themes/basic_hydpy/static/file.png +0 -0
  561. hydpy/docs/sphinx/_themes/basic_hydpy/static/language_data.js_t +26 -0
  562. hydpy/docs/sphinx/_themes/basic_hydpy/static/minus.png +0 -0
  563. hydpy/docs/sphinx/_themes/basic_hydpy/static/plus.png +0 -0
  564. hydpy/docs/sphinx/_themes/basic_hydpy/static/searchtools.js +574 -0
  565. hydpy/docs/sphinx/_themes/basic_hydpy/static/sphinx_highlight.js +154 -0
  566. hydpy/docs/sphinx/_themes/basic_hydpy/theme.conf +16 -0
  567. hydpy/docs/sphinx/_themes/classic_hydpy/layout.html +23 -0
  568. hydpy/docs/sphinx/_themes/classic_hydpy/static/classic.css_t +358 -0
  569. hydpy/docs/sphinx/_themes/classic_hydpy/static/sidebar.js_t +72 -0
  570. hydpy/docs/sphinx/_themes/classic_hydpy/theme.conf +32 -0
  571. hydpy/docs/sphinx/conf.py +398 -0
  572. hydpy/docs/sphinx/defaultlinks_extension.py +36 -0
  573. hydpy/docs/sphinx/integrationtest_extension.py +104 -0
  574. hydpy/docs/sphinx/projectstructure_extension.py +58 -0
  575. hydpy/docs/sphinx/submodelgraph_extension.py +53 -0
  576. hydpy/exe/__init__.py +0 -0
  577. hydpy/exe/commandtools.py +651 -0
  578. hydpy/exe/hyd.py +277 -0
  579. hydpy/exe/modelimports.py +41 -0
  580. hydpy/exe/replacetools.py +216 -0
  581. hydpy/exe/servertools.py +2348 -0
  582. hydpy/exe/xmltools.py +3280 -0
  583. hydpy/interfaces/__init__.py +0 -0
  584. hydpy/interfaces/aetinterfaces.py +94 -0
  585. hydpy/interfaces/dischargeinterfaces.py +45 -0
  586. hydpy/interfaces/petinterfaces.py +117 -0
  587. hydpy/interfaces/precipinterfaces.py +42 -0
  588. hydpy/interfaces/radiationinterfaces.py +79 -0
  589. hydpy/interfaces/rconcinterfaces.py +30 -0
  590. hydpy/interfaces/routinginterfaces.py +324 -0
  591. hydpy/interfaces/soilinterfaces.py +96 -0
  592. hydpy/interfaces/stateinterfaces.py +98 -0
  593. hydpy/interfaces/tempinterfaces.py +46 -0
  594. hydpy/models/__init__.py +0 -0
  595. hydpy/models/arma/__init__.py +14 -0
  596. hydpy/models/arma/arma_control.py +383 -0
  597. hydpy/models/arma/arma_derived.py +204 -0
  598. hydpy/models/arma/arma_fluxes.py +41 -0
  599. hydpy/models/arma/arma_inlets.py +11 -0
  600. hydpy/models/arma/arma_logs.py +19 -0
  601. hydpy/models/arma/arma_model.py +461 -0
  602. hydpy/models/arma/arma_outlets.py +11 -0
  603. hydpy/models/arma_rimorido.py +381 -0
  604. hydpy/models/conv/__init__.py +12 -0
  605. hydpy/models/conv/conv_control.py +303 -0
  606. hydpy/models/conv/conv_derived.py +271 -0
  607. hydpy/models/conv/conv_fluxes.py +54 -0
  608. hydpy/models/conv/conv_inlets.py +11 -0
  609. hydpy/models/conv/conv_model.py +687 -0
  610. hydpy/models/conv/conv_outlets.py +11 -0
  611. hydpy/models/conv_idw.py +120 -0
  612. hydpy/models/conv_idw_ed.py +184 -0
  613. hydpy/models/conv_nn.py +112 -0
  614. hydpy/models/dam/__init__.py +16 -0
  615. hydpy/models/dam/dam_aides.py +17 -0
  616. hydpy/models/dam/dam_control.py +346 -0
  617. hydpy/models/dam/dam_derived.py +559 -0
  618. hydpy/models/dam/dam_factors.py +46 -0
  619. hydpy/models/dam/dam_fluxes.py +179 -0
  620. hydpy/models/dam/dam_inlets.py +29 -0
  621. hydpy/models/dam/dam_logs.py +52 -0
  622. hydpy/models/dam/dam_model.py +5011 -0
  623. hydpy/models/dam/dam_outlets.py +23 -0
  624. hydpy/models/dam/dam_receivers.py +41 -0
  625. hydpy/models/dam/dam_senders.py +23 -0
  626. hydpy/models/dam/dam_solver.py +75 -0
  627. hydpy/models/dam/dam_states.py +11 -0
  628. hydpy/models/dam_llake.py +499 -0
  629. hydpy/models/dam_lreservoir.py +548 -0
  630. hydpy/models/dam_lretention.py +343 -0
  631. hydpy/models/dam_pump.py +278 -0
  632. hydpy/models/dam_pump_sluice.py +339 -0
  633. hydpy/models/dam_sluice.py +319 -0
  634. hydpy/models/dam_v001.py +1127 -0
  635. hydpy/models/dam_v002.py +381 -0
  636. hydpy/models/dam_v003.py +422 -0
  637. hydpy/models/dam_v004.py +665 -0
  638. hydpy/models/dam_v005.py +479 -0
  639. hydpy/models/dummy/__init__.py +15 -0
  640. hydpy/models/dummy/dummy_control.py +22 -0
  641. hydpy/models/dummy/dummy_fluxes.py +11 -0
  642. hydpy/models/dummy/dummy_inlets.py +11 -0
  643. hydpy/models/dummy/dummy_inputs.py +41 -0
  644. hydpy/models/dummy/dummy_model.py +196 -0
  645. hydpy/models/dummy/dummy_outlets.py +11 -0
  646. hydpy/models/dummy_interceptedwater.py +85 -0
  647. hydpy/models/dummy_node2node.py +83 -0
  648. hydpy/models/dummy_snowalbedo.py +84 -0
  649. hydpy/models/dummy_snowcover.py +84 -0
  650. hydpy/models/dummy_snowycanopy.py +86 -0
  651. hydpy/models/dummy_soilwater.py +85 -0
  652. hydpy/models/evap/__init__.py +13 -0
  653. hydpy/models/evap/evap_control.py +354 -0
  654. hydpy/models/evap/evap_derived.py +236 -0
  655. hydpy/models/evap/evap_factors.py +188 -0
  656. hydpy/models/evap/evap_fixed.py +68 -0
  657. hydpy/models/evap/evap_fluxes.py +150 -0
  658. hydpy/models/evap/evap_inputs.py +54 -0
  659. hydpy/models/evap/evap_logs.py +91 -0
  660. hydpy/models/evap/evap_masks.py +48 -0
  661. hydpy/models/evap/evap_model.py +9170 -0
  662. hydpy/models/evap/evap_parameters.py +149 -0
  663. hydpy/models/evap/evap_sequences.py +32 -0
  664. hydpy/models/evap/evap_states.py +18 -0
  665. hydpy/models/evap_aet_hbv96.py +372 -0
  666. hydpy/models/evap_aet_minhas.py +331 -0
  667. hydpy/models/evap_aet_morsim.py +627 -0
  668. hydpy/models/evap_pet_ambav1.py +483 -0
  669. hydpy/models/evap_pet_hbv96.py +147 -0
  670. hydpy/models/evap_pet_m.py +94 -0
  671. hydpy/models/evap_pet_mlc.py +107 -0
  672. hydpy/models/evap_ret_fao56.py +265 -0
  673. hydpy/models/evap_ret_io.py +74 -0
  674. hydpy/models/evap_ret_tw2002.py +165 -0
  675. hydpy/models/exch/__init__.py +14 -0
  676. hydpy/models/exch/exch_control.py +262 -0
  677. hydpy/models/exch/exch_derived.py +36 -0
  678. hydpy/models/exch/exch_factors.py +26 -0
  679. hydpy/models/exch/exch_fluxes.py +48 -0
  680. hydpy/models/exch/exch_inlets.py +11 -0
  681. hydpy/models/exch/exch_logs.py +12 -0
  682. hydpy/models/exch/exch_model.py +451 -0
  683. hydpy/models/exch/exch_outlets.py +17 -0
  684. hydpy/models/exch/exch_receivers.py +17 -0
  685. hydpy/models/exch_branch_hbv96.py +186 -0
  686. hydpy/models/exch_waterlevel.py +73 -0
  687. hydpy/models/exch_weir_hbv96.py +609 -0
  688. hydpy/models/ga/__init__.py +14 -0
  689. hydpy/models/ga/ga_aides.py +17 -0
  690. hydpy/models/ga/ga_control.py +208 -0
  691. hydpy/models/ga/ga_derived.py +77 -0
  692. hydpy/models/ga/ga_fluxes.py +83 -0
  693. hydpy/models/ga/ga_inputs.py +26 -0
  694. hydpy/models/ga/ga_logs.py +17 -0
  695. hydpy/models/ga/ga_model.py +2952 -0
  696. hydpy/models/ga/ga_states.py +87 -0
  697. hydpy/models/ga_garto.py +1001 -0
  698. hydpy/models/ga_garto_submodel1.py +79 -0
  699. hydpy/models/gland/__init__.py +14 -0
  700. hydpy/models/gland/gland_control.py +90 -0
  701. hydpy/models/gland/gland_derived.py +113 -0
  702. hydpy/models/gland/gland_fluxes.py +137 -0
  703. hydpy/models/gland/gland_inputs.py +12 -0
  704. hydpy/models/gland/gland_model.py +1439 -0
  705. hydpy/models/gland/gland_outlets.py +11 -0
  706. hydpy/models/gland/gland_states.py +90 -0
  707. hydpy/models/gland_gr4.py +501 -0
  708. hydpy/models/gland_gr5.py +463 -0
  709. hydpy/models/gland_gr6.py +487 -0
  710. hydpy/models/hland/__init__.py +20 -0
  711. hydpy/models/hland/hland_aides.py +19 -0
  712. hydpy/models/hland/hland_constants.py +37 -0
  713. hydpy/models/hland/hland_control.py +1530 -0
  714. hydpy/models/hland/hland_derived.py +683 -0
  715. hydpy/models/hland/hland_factors.py +57 -0
  716. hydpy/models/hland/hland_fixed.py +42 -0
  717. hydpy/models/hland/hland_fluxes.py +279 -0
  718. hydpy/models/hland/hland_inputs.py +19 -0
  719. hydpy/models/hland/hland_masks.py +107 -0
  720. hydpy/models/hland/hland_model.py +4664 -0
  721. hydpy/models/hland/hland_outlets.py +11 -0
  722. hydpy/models/hland/hland_parameters.py +227 -0
  723. hydpy/models/hland/hland_sequences.py +382 -0
  724. hydpy/models/hland/hland_states.py +236 -0
  725. hydpy/models/hland_96.py +1812 -0
  726. hydpy/models/hland_96c.py +1196 -0
  727. hydpy/models/hland_96p.py +1204 -0
  728. hydpy/models/kinw/__init__.py +18 -0
  729. hydpy/models/kinw/kinw_aides.py +306 -0
  730. hydpy/models/kinw/kinw_control.py +270 -0
  731. hydpy/models/kinw/kinw_derived.py +197 -0
  732. hydpy/models/kinw/kinw_fixed.py +33 -0
  733. hydpy/models/kinw/kinw_fluxes.py +37 -0
  734. hydpy/models/kinw/kinw_inlets.py +11 -0
  735. hydpy/models/kinw/kinw_model.py +3026 -0
  736. hydpy/models/kinw/kinw_outlets.py +11 -0
  737. hydpy/models/kinw/kinw_solver.py +45 -0
  738. hydpy/models/kinw/kinw_states.py +17 -0
  739. hydpy/models/kinw_williams.py +1299 -0
  740. hydpy/models/kinw_williams_ext.py +768 -0
  741. hydpy/models/lland/__init__.py +42 -0
  742. hydpy/models/lland/lland_aides.py +38 -0
  743. hydpy/models/lland/lland_constants.py +88 -0
  744. hydpy/models/lland/lland_control.py +1329 -0
  745. hydpy/models/lland/lland_derived.py +380 -0
  746. hydpy/models/lland/lland_factors.py +18 -0
  747. hydpy/models/lland/lland_fixed.py +128 -0
  748. hydpy/models/lland/lland_fluxes.py +626 -0
  749. hydpy/models/lland/lland_inlets.py +12 -0
  750. hydpy/models/lland/lland_inputs.py +33 -0
  751. hydpy/models/lland/lland_logs.py +17 -0
  752. hydpy/models/lland/lland_masks.py +212 -0
  753. hydpy/models/lland/lland_model.py +7690 -0
  754. hydpy/models/lland/lland_outlets.py +12 -0
  755. hydpy/models/lland/lland_parameters.py +195 -0
  756. hydpy/models/lland/lland_sequences.py +67 -0
  757. hydpy/models/lland/lland_states.py +280 -0
  758. hydpy/models/lland_dd.py +2270 -0
  759. hydpy/models/lland_knauf.py +2156 -0
  760. hydpy/models/lland_knauf_ic.py +1920 -0
  761. hydpy/models/meteo/__init__.py +12 -0
  762. hydpy/models/meteo/meteo_control.py +154 -0
  763. hydpy/models/meteo/meteo_derived.py +159 -0
  764. hydpy/models/meteo/meteo_factors.py +88 -0
  765. hydpy/models/meteo/meteo_fixed.py +19 -0
  766. hydpy/models/meteo/meteo_fluxes.py +46 -0
  767. hydpy/models/meteo/meteo_inputs.py +47 -0
  768. hydpy/models/meteo/meteo_logs.py +30 -0
  769. hydpy/models/meteo/meteo_model.py +2904 -0
  770. hydpy/models/meteo/meteo_parameters.py +14 -0
  771. hydpy/models/meteo/meteo_sequences.py +22 -0
  772. hydpy/models/meteo_clear_glob_io.py +77 -0
  773. hydpy/models/meteo_glob_fao56.py +217 -0
  774. hydpy/models/meteo_glob_io.py +68 -0
  775. hydpy/models/meteo_glob_morsim.py +444 -0
  776. hydpy/models/meteo_precip_io.py +76 -0
  777. hydpy/models/meteo_psun_sun_glob_io.py +83 -0
  778. hydpy/models/meteo_sun_fao56.py +188 -0
  779. hydpy/models/meteo_sun_morsim.py +466 -0
  780. hydpy/models/meteo_temp_io.py +76 -0
  781. hydpy/models/musk/__init__.py +15 -0
  782. hydpy/models/musk/musk_control.py +328 -0
  783. hydpy/models/musk/musk_derived.py +32 -0
  784. hydpy/models/musk/musk_factors.py +53 -0
  785. hydpy/models/musk/musk_fluxes.py +24 -0
  786. hydpy/models/musk/musk_inlets.py +11 -0
  787. hydpy/models/musk/musk_masks.py +15 -0
  788. hydpy/models/musk/musk_model.py +838 -0
  789. hydpy/models/musk/musk_outlets.py +11 -0
  790. hydpy/models/musk/musk_sequences.py +88 -0
  791. hydpy/models/musk/musk_solver.py +68 -0
  792. hydpy/models/musk/musk_states.py +64 -0
  793. hydpy/models/musk_classic.py +228 -0
  794. hydpy/models/musk_mct.py +1247 -0
  795. hydpy/models/rconc/__init__.py +12 -0
  796. hydpy/models/rconc/rconc_control.py +473 -0
  797. hydpy/models/rconc/rconc_derived.py +76 -0
  798. hydpy/models/rconc/rconc_fluxes.py +19 -0
  799. hydpy/models/rconc/rconc_logs.py +74 -0
  800. hydpy/models/rconc/rconc_model.py +260 -0
  801. hydpy/models/rconc/rconc_states.py +11 -0
  802. hydpy/models/rconc_nash.py +48 -0
  803. hydpy/models/rconc_uh.py +53 -0
  804. hydpy/models/sw1d/__init__.py +17 -0
  805. hydpy/models/sw1d/sw1d_control.py +356 -0
  806. hydpy/models/sw1d/sw1d_derived.py +85 -0
  807. hydpy/models/sw1d/sw1d_factors.py +78 -0
  808. hydpy/models/sw1d/sw1d_fixed.py +12 -0
  809. hydpy/models/sw1d/sw1d_fluxes.py +55 -0
  810. hydpy/models/sw1d/sw1d_inlets.py +17 -0
  811. hydpy/models/sw1d/sw1d_model.py +3385 -0
  812. hydpy/models/sw1d/sw1d_outlets.py +11 -0
  813. hydpy/models/sw1d/sw1d_receivers.py +11 -0
  814. hydpy/models/sw1d/sw1d_senders.py +11 -0
  815. hydpy/models/sw1d/sw1d_states.py +23 -0
  816. hydpy/models/sw1d_channel.py +2051 -0
  817. hydpy/models/sw1d_gate_out.py +599 -0
  818. hydpy/models/sw1d_lias.py +105 -0
  819. hydpy/models/sw1d_lias_sluice.py +531 -0
  820. hydpy/models/sw1d_network.py +1219 -0
  821. hydpy/models/sw1d_pump.py +448 -0
  822. hydpy/models/sw1d_q_in.py +79 -0
  823. hydpy/models/sw1d_q_out.py +81 -0
  824. hydpy/models/sw1d_storage.py +78 -0
  825. hydpy/models/sw1d_weir_out.py +75 -0
  826. hydpy/models/test/__init__.py +14 -0
  827. hydpy/models/test/test_control.py +28 -0
  828. hydpy/models/test/test_fluxes.py +17 -0
  829. hydpy/models/test/test_model.py +201 -0
  830. hydpy/models/test/test_solver.py +48 -0
  831. hydpy/models/test/test_states.py +17 -0
  832. hydpy/models/test_discontinous.py +46 -0
  833. hydpy/models/test_stiff0d.py +47 -0
  834. hydpy/models/test_stiff1d.py +42 -0
  835. hydpy/models/whmod/__init__.py +21 -0
  836. hydpy/models/whmod/whmod_constants.py +77 -0
  837. hydpy/models/whmod/whmod_control.py +333 -0
  838. hydpy/models/whmod/whmod_derived.py +210 -0
  839. hydpy/models/whmod/whmod_factors.py +9 -0
  840. hydpy/models/whmod/whmod_fluxes.py +105 -0
  841. hydpy/models/whmod/whmod_inputs.py +15 -0
  842. hydpy/models/whmod/whmod_masks.py +178 -0
  843. hydpy/models/whmod/whmod_model.py +2091 -0
  844. hydpy/models/whmod/whmod_parameters.py +155 -0
  845. hydpy/models/whmod/whmod_sequences.py +193 -0
  846. hydpy/models/whmod/whmod_states.py +73 -0
  847. hydpy/models/whmod_rural.py +794 -0
  848. hydpy/models/whmod_urban.py +1011 -0
  849. hydpy/models/wland/__init__.py +43 -0
  850. hydpy/models/wland/wland_aides.py +55 -0
  851. hydpy/models/wland/wland_constants.py +103 -0
  852. hydpy/models/wland/wland_control.py +508 -0
  853. hydpy/models/wland/wland_derived.py +330 -0
  854. hydpy/models/wland/wland_factors.py +11 -0
  855. hydpy/models/wland/wland_fixed.py +12 -0
  856. hydpy/models/wland/wland_fluxes.py +166 -0
  857. hydpy/models/wland/wland_inputs.py +33 -0
  858. hydpy/models/wland/wland_masks.py +54 -0
  859. hydpy/models/wland/wland_model.py +3755 -0
  860. hydpy/models/wland/wland_outlets.py +11 -0
  861. hydpy/models/wland/wland_parameters.py +214 -0
  862. hydpy/models/wland/wland_sequences.py +108 -0
  863. hydpy/models/wland/wland_solver.py +45 -0
  864. hydpy/models/wland/wland_states.py +56 -0
  865. hydpy/models/wland_gd.py +888 -0
  866. hydpy/models/wland_wag.py +1244 -0
  867. hydpy/models/wq/__init__.py +14 -0
  868. hydpy/models/wq/wq_control.py +117 -0
  869. hydpy/models/wq/wq_derived.py +182 -0
  870. hydpy/models/wq/wq_factors.py +79 -0
  871. hydpy/models/wq/wq_fluxes.py +17 -0
  872. hydpy/models/wq/wq_model.py +1889 -0
  873. hydpy/models/wq_trapeze.py +168 -0
  874. hydpy/models/wq_trapeze_strickler.py +101 -0
  875. hydpy/models/wq_walrus.py +57 -0
  876. hydpy/py.typed +0 -0
  877. hydpy/tests/.coveragerc +22 -0
  878. hydpy/tests/__init__.py +0 -0
  879. hydpy/tests/check_consistency.py +32 -0
  880. hydpy/tests/hydpydoctestcustomize.pth +1 -0
  881. hydpy/tests/hydpydoctestcustomize.py +15 -0
  882. hydpy/tests/iotesting/__init__.py +0 -0
  883. hydpy/tests/run_doctests.py +233 -0
  884. hydpy-6.2.dev1.data/scripts/hyd.py +277 -0
  885. hydpy-6.2.dev1.dist-info/LICENSE +165 -0
  886. hydpy-6.2.dev1.dist-info/METADATA +163 -0
  887. hydpy-6.2.dev1.dist-info/RECORD +890 -0
  888. hydpy-6.2.dev1.dist-info/WHEEL +5 -0
  889. hydpy-6.2.dev1.dist-info/licenses_hydpy_installer.txt +42 -0
  890. hydpy-6.2.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,4868 @@
1
+ """This module provides features for applying and implementing hydrological models."""
2
+
3
+ # import...
4
+ # ...from standard library
5
+ from __future__ import annotations
6
+ import abc
7
+ import collections
8
+ import contextlib
9
+ import copy
10
+ import functools
11
+ import importlib
12
+ import inspect
13
+ import itertools
14
+ import os
15
+ import runpy
16
+ import types
17
+
18
+ # ...from site-packages
19
+ import numpy
20
+
21
+ # ...from HydPy
22
+ import hydpy
23
+ from hydpy import conf
24
+ from hydpy.core import auxfiletools
25
+ from hydpy.core import devicetools
26
+ from hydpy.core import exceptiontools
27
+ from hydpy.core import importtools
28
+ from hydpy.core import objecttools
29
+ from hydpy.core import parametertools
30
+ from hydpy.core import sequencetools
31
+ from hydpy.core import timetools
32
+ from hydpy.core import variabletools
33
+ from hydpy.core.typingtools import *
34
+ from hydpy.cythons import modelutils
35
+
36
+ if TYPE_CHECKING:
37
+ from hydpy.core import masktools
38
+ from hydpy.core import selectiontools
39
+ from hydpy.auxs import interptools
40
+ from hydpy.cythons import interfaceutils
41
+
42
+
43
+ TypeModel_co = TypeVar("TypeModel_co", bound="Model", covariant=True)
44
+ TypeModel_contra = TypeVar("TypeModel_contra", bound="Model", contravariant=True)
45
+ TypeSubmodelInterface = TypeVar("TypeSubmodelInterface", bound="SubmodelInterface")
46
+
47
+
48
+ class _ModelModule(types.ModuleType):
49
+ ControlParameters: type[parametertools.SubParameters]
50
+ DerivedParameters: type[parametertools.SubParameters]
51
+ FixedParameters: type[parametertools.SubParameters]
52
+ SolverParameters: type[parametertools.SubParameters]
53
+
54
+
55
+ class Method:
56
+ """Base class for defining (hydrological) calculation methods."""
57
+
58
+ SUBMODELINTERFACES: ClassVar[tuple[type[SubmodelInterface], ...]]
59
+ SUBMETHODS: tuple[type[Method], ...] = ()
60
+ CONTROLPARAMETERS: tuple[
61
+ type[parametertools.Parameter | interptools.BaseInterpolator], ...
62
+ ] = ()
63
+ DERIVEDPARAMETERS: tuple[type[parametertools.Parameter], ...] = ()
64
+ FIXEDPARAMETERS: tuple[type[parametertools.Parameter], ...] = ()
65
+ SOLVERPARAMETERS: tuple[type[parametertools.Parameter], ...] = ()
66
+ REQUIREDSEQUENCES: tuple[type[sequencetools.Sequence_], ...] = ()
67
+ UPDATEDSEQUENCES: tuple[type[sequencetools.Sequence_], ...] = ()
68
+ RESULTSEQUENCES: tuple[type[sequencetools.Sequence_], ...] = ()
69
+
70
+ __call__: Callable
71
+ __name__: str
72
+
73
+ def __init_subclass__(cls) -> None:
74
+ if isinstance(call := cls.__call__, types.FunctionType):
75
+ setattr(call, "__HYDPY_METHOD__", True)
76
+
77
+
78
+ class AutoMethod(Method):
79
+ """Base class for defining methods that only call their submethods in the specified
80
+ order without passing any arguments or other customisations."""
81
+
82
+ @classmethod
83
+ def __call__(cls, model: Model) -> None:
84
+ for method in cls.SUBMETHODS:
85
+ method.__call__(model)
86
+
87
+
88
+ class SetAutoMethod(Method):
89
+ """Base class for defining setter methods that also use the given data to calculate
90
+ other properties.
91
+
92
+ |SetAutoMethod| calls its submethods in the specified order. If, for example, the
93
+ first two submethods are setters, it requires precisely two parameter values. It
94
+ passes the first value to the first setter and the second value to the second
95
+ setter. After that, it executes the remaining methods without exchanging any data.
96
+ """
97
+
98
+ @classmethod
99
+ def __call__(cls, model: Model, *values) -> None:
100
+ for method, value in zip(cls.SUBMETHODS, values):
101
+ method.__call__(model, value)
102
+ for method in cls.SUBMETHODS[len(values) :]:
103
+ method.__call__(model)
104
+
105
+
106
+ class ReusableMethod(Method):
107
+ """Base class for defining methods that need not or must not be called multiple
108
+ times for the same simulation step.
109
+
110
+ |ReusableMethod| helps to implement "sharable" submodels, of which single instances
111
+ can be used by multiple main model instances. See |SharableSubmodelInterface| for
112
+ further information.
113
+ """
114
+
115
+ REUSEMARKER: str
116
+ """Name of an additional model attribute for marking if the respective method has
117
+ already been called and should not be called again for the same simulation step and
118
+ its results can be reused."""
119
+
120
+ def __init_subclass__(cls) -> None:
121
+ super().__init_subclass__()
122
+ cls.REUSEMARKER = f"__hydpy_reuse_{cls.__name__.lower()}__"
123
+
124
+ @classmethod
125
+ def call_reusablemethod(cls, model: Model, *args, **kwargs) -> None:
126
+ """Execute the "normal" model-specific `__call__` method only when indicated by
127
+ the |ReusableMethod.REUSEMARKER| attribute and update this attribute when
128
+ necessary."""
129
+ if not getattr(model, cls.REUSEMARKER):
130
+ cls.__call__(model, *args, **kwargs)
131
+ setattr(model, cls.REUSEMARKER, True)
132
+
133
+
134
+ abstractmodelmethods: set[Callable[..., Any]] = set()
135
+
136
+
137
+ def abstractmodelmethod(method: Callable[P, T]) -> Callable[P, T]:
138
+ """Alternative for Python's |abc.abstractmethod|.
139
+
140
+ We currently use it to mark abstract methods in submodel interfaces that are not
141
+ statically overridden by concrete implementations but dynamically added during
142
+ model initialisation (either in a pure Python or a Cython version).
143
+
144
+ So far, the only functionality of |abstractmodelmethod| is to collect all decorated
145
+ functions in the set `abstractmodelmethods` so that one can find out which methods
146
+ are "abstract model methods" and which are not. We might also use it later to
147
+ extend our model consistency checks.
148
+ """
149
+ abstractmodelmethods.add(method)
150
+ return method
151
+
152
+
153
+ class _SubmodelPropertyBase(Generic[TypeSubmodelInterface]):
154
+ interfaces: tuple[type[TypeSubmodelInterface], ...]
155
+
156
+ _CYTHON_PYTHON_SUBMODEL_ERROR_MESSAGE: Final = (
157
+ "The main model is initialised in Cython mode, but the submodel is "
158
+ "initialised in pure Python mode so that the main model's cythonized methods "
159
+ "could apply the submodel's methods."
160
+ )
161
+
162
+ def _check_submodel_follows_interface(
163
+ self, submodel: TypeSubmodelInterface
164
+ ) -> None:
165
+ if not isinstance(submodel, self.interfaces):
166
+ interfacenames = (i.__name__ for i in self.interfaces)
167
+ raise ValueError(
168
+ f"The given submodel is not an instance of any of the following "
169
+ f"supported interfaces: {objecttools.enumeration(interfacenames)}."
170
+ )
171
+
172
+ def _find_first_suitable_interface(
173
+ self, submodel: TypeSubmodelInterface
174
+ ) -> type[SubmodelInterface]:
175
+ for interface in self.interfaces:
176
+ if isinstance(submodel, interface):
177
+ return interface
178
+ interfacenames = (i.__name__ for i in self.interfaces)
179
+ raise ValueError(
180
+ f"The given submodel is not an instance of any of the following supported "
181
+ f"interfaces: {objecttools.enumeration(interfacenames)}."
182
+ )
183
+
184
+
185
+ class SubmodelProperty(_SubmodelPropertyBase[TypeSubmodelInterface]):
186
+ """Descriptor for submodel attributes.
187
+
188
+ |SubmodelProperty| instances link main models and their submodels. They follow the
189
+ attribute convention described in the documentation on class |SubmodelInterface|.
190
+ Behind the scenes, they build the required connections both on the Python and the
191
+ Cython level and perform some type-related tests (to avoid errors due to selecting
192
+ submodels following the wrong interfaces).
193
+
194
+ We prepare the main model and its submodel in Cython and pure Python mode to test
195
+ that |SubmodelProperty| works for all possible combinations:
196
+
197
+ >>> from hydpy import prepare_model, pub
198
+ >>> with pub.options.usecython(False):
199
+ ... mainmodel_python = prepare_model("lland")
200
+ ... submodel_python = prepare_model("ga_garto_submodel1")
201
+ >>> with pub.options.usecython(True):
202
+ ... mainmodel_cython = prepare_model("lland")
203
+ ... submodel_cython = prepare_model("ga_garto_submodel1")
204
+
205
+ By default, the main model handles no submodel:
206
+
207
+ >>> mainmodel_python.soilmodel
208
+ >>> mainmodel_cython.soilmodel
209
+
210
+ For pure Python main models, it makes no difference how the submodel is
211
+ initialised:
212
+
213
+ >>> mainmodel_python.soilmodel = submodel_python
214
+ >>> type(mainmodel_python.soilmodel)
215
+ <class 'hydpy.models.ga_garto_submodel1.Model'>
216
+ >>> mainmodel_python.cymodel
217
+
218
+ >>> mainmodel_python.soilmodel = submodel_cython
219
+ >>> type(mainmodel_python.soilmodel)
220
+ <class 'hydpy.models.ga_garto_submodel1.Model'>
221
+ >>> mainmodel_python.cymodel
222
+
223
+ If both models are initialised in Cython mode, |SubmodelProperty| connects the
224
+ instances of the Cython extension classes on the fly:
225
+
226
+ >>> mainmodel_cython.soilmodel = submodel_cython
227
+ >>> type(mainmodel_cython.soilmodel)
228
+ <class 'hydpy.models.ga_garto_submodel1.Model'>
229
+ >>> type(mainmodel_cython.cymodel.get_soilmodel())
230
+ <class 'hydpy.cythons.autogen.c_ga_garto_submodel1.Model'>
231
+
232
+ Combining a Cython main model with a pure Python submodel causes a |RuntimeError|,
233
+ as using such a mix could result in hard-to-find errors:
234
+
235
+ >>> mainmodel_cython.soilmodel = submodel_python
236
+ Traceback (most recent call last):
237
+ ...
238
+ RuntimeError: While trying to assign submodel `ga_garto_submodel1` to property \
239
+ `soilmodel` of the main model `lland`, the following error occurred: The main model \
240
+ is initialised in Cython mode, but the submodel is initialised in pure Python mode so \
241
+ that the main model's cythonized methods could apply the submodel's methods.
242
+
243
+ Disconnecting a submodel from its main model works by assigning |None| as well as
244
+ using the `del` statement:
245
+
246
+ >>> mainmodel_python.soilmodel = None
247
+ >>> mainmodel_python.soilmodel
248
+
249
+ >>> del mainmodel_cython.soilmodel
250
+ >>> mainmodel_cython.soilmodel
251
+ >>> mainmodel_cython.cymodel.get_soilmodel()
252
+
253
+ Trying to assign an unsuitable submodel results in the following error:
254
+
255
+ >>> mainmodel_python.soilmodel = mainmodel_python
256
+ Traceback (most recent call last):
257
+ ...
258
+ ValueError: While trying to assign submodel `lland` to property `soilmodel` of \
259
+ the main model `lland`, the following error occurred: The given submodel is not an \
260
+ instance of any of the following supported interfaces: SoilModel_V1.
261
+
262
+ The automatically generated docstrings list the supported interfaces:
263
+
264
+ >>> print(type(mainmodel_python).soilmodel.__doc__)
265
+ Optional submodel that complies with the following interface: SoilModel_V1.
266
+ """
267
+
268
+ name: str
269
+ """The addressed submodels' group name."""
270
+ interfaces: tuple[type[TypeSubmodelInterface], ...]
271
+ """The supported interfaces."""
272
+ optional: Final[bool]
273
+ """Flag indicating whether a submodel is optional or strictly required."""
274
+ sidemodel: Final[bool]
275
+ """Flag indicating whether the handled submodel is more a "side model" than a
276
+ submodel. Usually, two models consider each other as side models if they are
277
+ "real" submodels of a third model but need direct references."""
278
+
279
+ __hydpy_modeltype2instance__: ClassVar[
280
+ collections.defaultdict[type[Model], list[SubmodelProperty[Any]]]
281
+ ] = collections.defaultdict(list)
282
+
283
+ def __init__(
284
+ self,
285
+ *interfaces: type[TypeSubmodelInterface],
286
+ optional: bool = False,
287
+ sidemodel: bool = False,
288
+ ) -> None:
289
+ self.interfaces = tuple(interfaces)
290
+ self.optional = optional
291
+ self.sidemodel = sidemodel
292
+ interfacenames = (i.__name__ for i in self.interfaces)
293
+ prefix = "Optional submodel" if optional else "Required submodel"
294
+ suffix = (
295
+ "the following interface"
296
+ if len(interfaces) == 1
297
+ else "one of the following interfaces"
298
+ )
299
+ self.__doc__ = (
300
+ f"{prefix} that complies with {suffix}: "
301
+ f"{objecttools.enumeration(interfacenames, conjunction='or')}."
302
+ )
303
+
304
+ def __set_name__(self, owner: type[Model], name: str) -> None:
305
+ self.name = name
306
+ self.__hydpy_modeltype2instance__[owner].append(self)
307
+
308
+ @overload
309
+ def __get__(self, obj: None, objtype: type[Model] | None) -> Self: ...
310
+
311
+ @overload
312
+ def __get__(
313
+ self, obj: Model, objtype: type[Model] | None
314
+ ) -> TypeSubmodelInterface | None: ...
315
+
316
+ def __get__(
317
+ self, obj: Model | None, objtype: type[Model] | None = None
318
+ ) -> Self | TypeSubmodelInterface | None:
319
+ if obj is None:
320
+ return self
321
+ return vars(obj).get(self.name, None)
322
+
323
+ def __set__(self, obj: Model, value: TypeSubmodelInterface | None) -> None:
324
+ try:
325
+ if value is None:
326
+ self.__delete__(obj)
327
+ else:
328
+ self._check_submodel_follows_interface(value)
329
+ vars(obj)[self.name] = value
330
+ if obj.cymodel is not None:
331
+ if value.cymodel is None:
332
+ raise RuntimeError(self._CYTHON_PYTHON_SUBMODEL_ERROR_MESSAGE)
333
+ getattr(obj.cymodel, f"set_{self.name}")(value.cymodel)
334
+ except BaseException:
335
+ objecttools.augment_excmessage(
336
+ f"While trying to assign submodel `{value}` to property `{self.name}` "
337
+ f"of the main model `{obj.name}`"
338
+ )
339
+
340
+ def __delete__(self, obj: Model) -> None:
341
+ vars(obj)[self.name] = None
342
+ if obj.cymodel is not None:
343
+ getattr(obj.cymodel, f"set_{self.name}")(None)
344
+
345
+
346
+ class SubmodelsProperty(_SubmodelPropertyBase[TypeSubmodelInterface]):
347
+ """Descriptor for handling multiple submodels that follow defined interfaces.
348
+
349
+ |SubmodelsProperty| supports the `len` operator and is iterable and indexable:
350
+
351
+ >>> from hydpy import prepare_model
352
+ >>> main = prepare_model("sw1d_channel")
353
+ >>> sub1 = prepare_model("sw1d_q_in")
354
+ >>> sub2 = prepare_model("sw1d_lias")
355
+
356
+ >>> from hydpy.core.modeltools import SubmodelsProperty
357
+ >>> assert isinstance(type(main).routingmodels, SubmodelsProperty)
358
+
359
+ >>> main.routingmodels.append_submodel(submodel=sub1, typeid=1)
360
+ >>> main.routingmodels.append_submodel(submodel=sub2, typeid=1)
361
+ >>> len(main.routingmodels)
362
+ 2
363
+ >>> for submodel in main.routingmodels:
364
+ ... print(submodel.name)
365
+ sw1d_q_in
366
+ sw1d_lias
367
+ >>> main.routingmodels[0] is sub1
368
+ True
369
+ >>> main.routingmodels[1] is sub2
370
+ True
371
+ """
372
+
373
+ name: str
374
+ """The addressed submodels' group name."""
375
+ interfaces: tuple[type[TypeSubmodelInterface], ...]
376
+ """The supported interfaces."""
377
+ sidemodels: bool
378
+ """Flag indicating whether the handled submodel is more a "side model" than a
379
+ submodel. Usually, two models consider each other as side models if they are
380
+ "real" submodels of a third model but need direct references."""
381
+
382
+ __hydpy_modeltype2instance__: ClassVar[
383
+ collections.defaultdict[type[Model], list[SubmodelsProperty[Any]]]
384
+ ] = collections.defaultdict(list)
385
+ __hydpy_mainmodel2submodels__: collections.defaultdict[
386
+ Model, list[TypeSubmodelInterface | None]
387
+ ]
388
+
389
+ _mainmodel: Model | None
390
+ _mainmodel2numbersubmodels: collections.defaultdict[Model, int]
391
+ _mainmodel2submodeltypeids: collections.defaultdict[Model, list[int]]
392
+
393
+ def __set_name__(self, owner: type[Model], name: str) -> None:
394
+ self.name = name
395
+ self.__hydpy_modeltype2instance__[owner].append(self)
396
+
397
+ def __init__(
398
+ self, *interfaces: type[TypeSubmodelInterface], sidemodels: bool = False
399
+ ) -> None:
400
+ self.interfaces = tuple(interfaces)
401
+ self.sidemodels = sidemodels
402
+ self.__hydpy_mainmodel2submodels__ = collections.defaultdict(list)
403
+ self._mainmodel2numbersubmodels = collections.defaultdict(int)
404
+ self._mainmodel2submodeltypeids = collections.defaultdict(list)
405
+ interfacenames = (i.__name__ for i in self.interfaces)
406
+ suffix = "s" if len(interfaces) > 1 else ""
407
+ self.__doc__ = (
408
+ f"Vector of submodels that comply with the following interface{suffix}: "
409
+ f"{objecttools.enumeration(interfacenames, conjunction='or')}."
410
+ )
411
+
412
+ def __get__(self, obj: Model | None, objtype: type[Model] | None = None) -> Self:
413
+ if obj is None:
414
+ return self
415
+ try:
416
+ self._mainmodel = obj
417
+ return copy.copy(self)
418
+ finally:
419
+ self._mainmodel = None
420
+
421
+ @property
422
+ def number(self) -> int:
423
+ """The maximum number of handled submodels.
424
+
425
+ Initially, the maximum number of submodels is zero:
426
+
427
+ >>> from hydpy import prepare_model, pub
428
+ >>> with pub.options.usecython(False):
429
+ ... model = prepare_model("sw1d_channel")
430
+ >>> model.storagemodels.number
431
+ 0
432
+ >>> model.storagemodels.submodels
433
+ ()
434
+ >>> model.storagemodels.typeids
435
+ ()
436
+
437
+ Setting it to another value automatically prepares |SubmodelsProperty.typeids|
438
+ and |SubmodelsProperty.submodels|:
439
+
440
+ >>> model.storagemodels.number = 2
441
+ >>> model.storagemodels.number
442
+ 2
443
+ >>> model.storagemodels.typeids
444
+ (0, 0)
445
+ >>> model.storagemodels.submodels
446
+ (None, None)
447
+
448
+ When working in Cython mode, property |SubmodelsProperty.number| also prepares
449
+ the analogue vectors of the cythonized model:
450
+
451
+ >>> with pub.options.usecython(True):
452
+ ... model = prepare_model("sw1d_channel")
453
+ >>> model.storagemodels.number
454
+ 0
455
+ >>> model.storagemodels.submodels
456
+ ()
457
+ >>> model.storagemodels.typeids
458
+ ()
459
+
460
+ >>> model.storagemodels.number = 2
461
+ >>> model.storagemodels.number
462
+ 2
463
+ >>> model.storagemodels.submodels
464
+ (None, None)
465
+ >>> model.storagemodels.typeids
466
+ (0, 0)
467
+ >>> model.cymodel.storagemodels._get_number()
468
+ 2
469
+ >>> model.cymodel.storagemodels._get_typeid(0)
470
+ 0
471
+ >>> model.cymodel.storagemodels._get_submodel(0)
472
+ """
473
+ assert (model := self._mainmodel) is not None
474
+ return self._mainmodel2numbersubmodels[model]
475
+
476
+ @number.setter
477
+ def number(self, number: int) -> None:
478
+ if number != self.number:
479
+ assert (model := self._mainmodel) is not None
480
+ self._mainmodel2numbersubmodels[model] = number
481
+ self.__hydpy_mainmodel2submodels__[model] = [None for _ in range(number)]
482
+ self._mainmodel2submodeltypeids[model] = number * [0]
483
+ if (cymodel := model.cymodel) is not None:
484
+ cyprop: interfaceutils.SubmodelsProperty = getattr(cymodel, self.name)
485
+ cyprop.set_number(number)
486
+
487
+ def put_submodel(
488
+ self, submodel: TypeSubmodelInterface, typeid: int, position: int
489
+ ) -> None:
490
+ """Put a submodel and its relevant type ID to the given position.
491
+
492
+ We prepare the main model and its submodel in Cython and pure Python mode to
493
+ test that |SubmodelsProperty.put_submodel| works for all possible combinations:
494
+
495
+ >>> from hydpy import prepare_model, pub
496
+ >>> with pub.options.usecython(False):
497
+ ... main_py = prepare_model("sw1d_channel")
498
+ ... sub_py = prepare_model("sw1d_storage")
499
+ >>> with pub.options.usecython(True):
500
+ ... main_cy = prepare_model("sw1d_channel")
501
+ ... sub_cy = prepare_model("sw1d_storage")
502
+
503
+ For two pure Python models, there is no need to bother with synchronising
504
+ cythonized models:
505
+
506
+ >>> main_py.storagemodels.number = 2
507
+ >>> main_py.storagemodels.put_submodel(submodel=sub_py, typeid=1, position=0)
508
+ >>> assert main_py.storagemodels.typeids[0] == 1
509
+ >>> assert main_py.storagemodels.submodels[0] is sub_py
510
+ >>> assert main_py.storagemodels.typeids[1] == 0
511
+ >>> assert main_py.storagemodels.submodels[1] is None
512
+
513
+ If both models are initialised in Cython mode, |SubmodelsProperty.put_submodel|
514
+ updates |SubmodelsProperty.typeids| and |SubmodelsProperty.submodels| as well
515
+ as the corresponding vectors of the cythonized models:
516
+
517
+ >>> main_cy.storagemodels.number = 2
518
+ >>> main_cy.storagemodels.put_submodel(submodel=sub_cy, typeid=1, position=0)
519
+ >>> assert main_cy.storagemodels.typeids[0] == 1
520
+ >>> assert main_cy.cymodel.storagemodels._get_typeid(0) == 1
521
+ >>> assert main_cy.storagemodels.submodels[0] is sub_cy
522
+ >>> assert main_cy.cymodel.storagemodels._get_submodel(0) is sub_cy.cymodel
523
+ >>> assert main_cy.storagemodels.typeids[1] == 0
524
+ >>> assert main_cy.cymodel.storagemodels._get_typeid(1) == 0
525
+ >>> assert main_cy.storagemodels.submodels[1] is None
526
+ >>> assert main_cy.cymodel.storagemodels._get_submodel(1) is None
527
+
528
+ Connecting a pure Python mode main model with a Cython mode submodel causes no
529
+ harm:
530
+
531
+ >>> main_py.storagemodels.number = 0
532
+ >>> main_py.storagemodels.number = 2
533
+ >>> main_py.storagemodels.put_submodel(submodel=sub_cy, typeid=1, position=0)
534
+ >>> assert main_py.storagemodels.typeids[0] == 1
535
+ >>> assert main_py.storagemodels.submodels[0] is sub_cy
536
+ >>> assert main_py.storagemodels.typeids[1] == 0
537
+ >>> assert main_py.storagemodels.submodels[1] is None
538
+
539
+ However, connecting a Cython mode main model with a pure Python mode submodel
540
+ would result in erroneous calculations and thus raises the following error:
541
+
542
+ >>> main_cy.storagemodels.number = 0
543
+ >>> main_cy.storagemodels.number = 2
544
+ >>> main_cy.storagemodels.put_submodel(submodel=sub_py, typeid=1, position=0)
545
+ Traceback (most recent call last):
546
+ ...
547
+ RuntimeError: While trying to put submodel `sw1d_storage` to position `0` of \
548
+ property `storagemodels` of the main model `sw1d_channel`, the following error \
549
+ occurred: The main model is initialised in Cython mode, but the submodel is \
550
+ initialised in pure Python mode so that the main model's cythonized methods could \
551
+ apply the submodel's methods.
552
+ >>> assert main_cy.storagemodels.typeids[0] == 0
553
+ >>> assert main_cy.cymodel.storagemodels._get_typeid(0) == 0
554
+ >>> assert main_cy.storagemodels.submodels[0] is None
555
+ >>> assert main_cy.cymodel.storagemodels._get_submodel(0) is None
556
+ >>> assert main_cy.storagemodels.typeids[1] == 0
557
+ >>> assert main_cy.cymodel.storagemodels._get_typeid(1) == 0
558
+ >>> assert main_cy.storagemodels.submodels[1] is None
559
+ >>> assert main_cy.cymodel.storagemodels._get_submodel(1) is None
560
+
561
+ Method |SubmodelsProperty.put_submodel| checks if the given submodel follows
562
+ at least one supported interface:
563
+
564
+ >>> sub_py = prepare_model("sw1d_lias")
565
+ >>> main_py.storagemodels.number = 0
566
+ >>> main_py.storagemodels.number = 2
567
+ >>> main_py.storagemodels.put_submodel(submodel=sub_py, typeid=1, position=0)
568
+ Traceback (most recent call last):
569
+ ...
570
+ ValueError: While trying to put submodel `sw1d_lias` to position `0` of \
571
+ property `storagemodels` of the main model `sw1d_channel`, the following error \
572
+ occurred: The given submodel is not an instance of any of the following supported \
573
+ interfaces: StorageModel_V1.
574
+ >>> assert main_py.storagemodels.typeids[0] == 0
575
+ >>> assert main_py.storagemodels.submodels[0] is None
576
+ >>> assert main_py.storagemodels.typeids[1] == 0
577
+ >>> assert main_py.storagemodels.submodels[1] is None
578
+ """
579
+ assert (mainmodel := self._mainmodel) is not None
580
+ try:
581
+ self._check_submodel_follows_interface(submodel)
582
+ if (cymain := mainmodel.cymodel) is not None:
583
+ if (cysub := submodel.cymodel) is None:
584
+ raise RuntimeError(self._CYTHON_PYTHON_SUBMODEL_ERROR_MESSAGE)
585
+ cyprop: interfaceutils.SubmodelsProperty = getattr(cymain, self.name)
586
+ cyprop.put_submodel(submodel=cysub, typeid=typeid, position=position)
587
+ self.__hydpy_mainmodel2submodels__[mainmodel][position] = submodel
588
+ self._mainmodel2submodeltypeids[mainmodel][position] = typeid
589
+ except BaseException:
590
+ objecttools.augment_excmessage(
591
+ f"While trying to put submodel `{submodel}` to position `{position}` "
592
+ f"of property `{self.name}` of the main model `{mainmodel}`"
593
+ )
594
+
595
+ def delete_submodel(self, position: int) -> None:
596
+ """Delete the submodel at the given position.
597
+
598
+ We prepare the main model and its submodel in Cython and pure Python mode to
599
+ test that |SubmodelsProperty.delete_submodel| works both in Cython and pure
600
+ Python Cython mode:
601
+
602
+ >>> from hydpy import prepare_model, pub
603
+ >>> with pub.options.usecython(False):
604
+ ... main_py = prepare_model("sw1d_channel")
605
+ ... sub_py = prepare_model("sw1d_storage")
606
+ >>> with pub.options.usecython(True):
607
+ ... main_cy = prepare_model("sw1d_channel")
608
+ ... sub_cy = prepare_model("sw1d_storage")
609
+
610
+ In pure Python mode, |SubmodelsProperty.delete_submodel| resets the entry in
611
+ the submodel vector to |None| and the type ID to zero:
612
+
613
+ >>> main_py.storagemodels.number = 3
614
+ >>> main_py.storagemodels.put_submodel(submodel=sub_py, typeid=1, position=1)
615
+ >>> assert main_py.storagemodels.typeids[1] == 1
616
+ >>> assert main_py.storagemodels.submodels[1] is sub_py
617
+
618
+ >>> main_py.storagemodels.delete_submodel(position=1)
619
+ >>> assert main_py.storagemodels.typeids[1] == 0
620
+ >>> assert main_py.storagemodels.submodels[1] is None
621
+
622
+ In Cython mode, |SubmodelsProperty.delete_submodel| does the same for the
623
+ analogue C vectors:
624
+
625
+ >>> main_cy.storagemodels.number = 3
626
+ >>> main_cy.storagemodels.put_submodel(submodel=sub_cy, typeid=1, position=1)
627
+ >>> assert main_cy.storagemodels.typeids[1] == 1
628
+ >>> assert main_cy.cymodel.storagemodels._get_typeid(1) == 1
629
+ >>> assert main_cy.storagemodels.submodels[1] is sub_cy
630
+ >>> assert main_cy.cymodel.storagemodels._get_submodel(1) is sub_cy.cymodel
631
+
632
+ >>> main_cy.storagemodels.delete_submodel(position=1)
633
+ >>> assert main_cy.storagemodels.typeids[1] == 0
634
+ >>> assert main_cy.cymodel.storagemodels._get_typeid(1) == 0
635
+ >>> assert main_cy.storagemodels.submodels[1] is None
636
+ >>> assert main_cy.cymodel.storagemodels._get_submodel(1) is None
637
+
638
+ Calling |SubmodelsProperty.delete_submodel| for a position with an existing
639
+ submodel does not raise a warning or error:
640
+
641
+ >>> main_cy.storagemodels.delete_submodel(position=1)
642
+
643
+ Potential errors are reported like this:
644
+
645
+ >>> main_cy.storagemodels.delete_submodel(position=3)
646
+ Traceback (most recent call last):
647
+ ...
648
+ IndexError: While trying to delete a submodel at position `3` of property \
649
+ `storagemodels` of the main model `sw1d_channel`, the following error occurred: list \
650
+ assignment index out of range
651
+ """
652
+ assert (mainmodel := self._mainmodel) is not None
653
+ try:
654
+ self.__hydpy_mainmodel2submodels__[mainmodel][position] = None
655
+ self._mainmodel2submodeltypeids[mainmodel][position] = 0
656
+ if (cymain := mainmodel.cymodel) is not None:
657
+ cyprop: interfaceutils.SubmodelsProperty = getattr(cymain, self.name)
658
+ cyprop.put_submodel(submodel=None, typeid=0, position=position)
659
+ except BaseException:
660
+ objecttools.augment_excmessage(
661
+ f"While trying to delete a submodel at position `{position}` of "
662
+ f"property `{self.name}` of the main model `{mainmodel}`"
663
+ )
664
+
665
+ def append_submodel(
666
+ self, submodel: TypeSubmodelInterface, typeid: int | None = None
667
+ ) -> None:
668
+ """Append a submodel and its relevant type ID to the already available ones.
669
+
670
+ We prepare the main model and its submodel in Cython and pure Python mode to
671
+ test that |SubmodelsProperty.append_submodel| works for all possible
672
+ combinations:
673
+
674
+ >>> from hydpy import prepare_model, pub
675
+ >>> with pub.options.usecython(False):
676
+ ... main_py = prepare_model("sw1d_channel")
677
+ ... sub_py = prepare_model("sw1d_storage")
678
+ >>> with pub.options.usecython(True):
679
+ ... main_cy = prepare_model("sw1d_channel")
680
+ ... sub_cy = prepare_model("sw1d_storage")
681
+
682
+ For two pure Python models, there is no need to bother with synchronising
683
+ cythonized models:
684
+
685
+ >>> main_py.storagemodels.append_submodel(submodel=sub_py, typeid=1)
686
+ >>> assert main_py.storagemodels.number == 1
687
+ >>> assert main_py.storagemodels.typeids[0] == 1
688
+ >>> assert main_py.storagemodels.submodels[0] is sub_py
689
+
690
+ If both models are initialised in Cython mode,
691
+ |SubmodelsProperty.append_submodel| updates |SubmodelsProperty.typeids| and
692
+ |SubmodelsProperty.submodels| as well as the corresponding vectors of the
693
+ cythonized models:
694
+
695
+ >>> main_cy.storagemodels.append_submodel(submodel=sub_cy, typeid=1)
696
+ >>> assert main_cy.storagemodels.number == 1
697
+ >>> assert main_cy.storagemodels.typeids[0] == 1
698
+ >>> assert main_cy.cymodel.storagemodels._get_typeid(0) == 1
699
+ >>> assert main_cy.storagemodels.submodels[0] is sub_cy
700
+ >>> assert main_cy.cymodel.storagemodels._get_submodel(0) is sub_cy.cymodel
701
+
702
+ Connecting a pure Python mode main model with a Cython mode submodel causes no
703
+ harm:
704
+
705
+ >>> main_py.storagemodels.append_submodel(submodel=sub_cy, typeid=1)
706
+ >>> assert main_py.storagemodels.number == 2
707
+ >>> assert main_py.storagemodels.typeids[0] == 1
708
+ >>> assert main_py.storagemodels.submodels[0] is sub_py
709
+ >>> assert main_py.storagemodels.typeids[1] == 1
710
+ >>> assert main_py.storagemodels.submodels[1] is sub_cy
711
+
712
+ However, connecting a Cython mode main model with a pure Python mode submodel
713
+ would result in erroneous calculations and thus raises the following error:
714
+
715
+ >>> main_cy.storagemodels.append_submodel(submodel=sub_py, typeid=1)
716
+ Traceback (most recent call last):
717
+ ...
718
+ RuntimeError: While trying to append submodel `sw1d_storage` to property \
719
+ `storagemodels` of the main model `sw1d_channel`, the following error occurred: The \
720
+ main model is initialised in Cython mode, but the submodel is initialised in pure \
721
+ Python mode so that the main model's cythonized methods could apply the submodel's \
722
+ methods.
723
+
724
+ >>> assert main_cy.storagemodels.number == 1
725
+ >>> assert main_cy.storagemodels.typeids[0] == 1
726
+ >>> assert main_cy.cymodel.storagemodels._get_typeid(0) == 1
727
+ >>> assert main_cy.storagemodels.submodels[0] is sub_cy
728
+ >>> assert main_cy.cymodel.storagemodels._get_submodel(0) is sub_cy.cymodel
729
+
730
+ Method |SubmodelsProperty.append_submodel| checks if the given submodel follows
731
+ at least one supported interface:
732
+
733
+ >>> sub_wrong = prepare_model("sw1d_lias")
734
+ >>> main_py.storagemodels.append_submodel(submodel=sub_wrong, typeid=1)
735
+ Traceback (most recent call last):
736
+ ...
737
+ ValueError: While trying to append submodel `sw1d_lias` to property \
738
+ `storagemodels` of the main model `sw1d_channel`, the following error occurred: The \
739
+ given submodel is not an instance of any of the following supported interfaces: \
740
+ StorageModel_V1.
741
+
742
+ >>> assert main_py.storagemodels.number == 2
743
+ >>> assert main_py.storagemodels.typeids[0] == 1
744
+ >>> assert main_py.storagemodels.submodels[0] is sub_py
745
+ >>> assert main_py.storagemodels.typeids[1] == 1
746
+ >>> assert main_py.storagemodels.submodels[1] is sub_cy
747
+
748
+ For convenience, you can omit to pass the type ID.
749
+ |SubmodelsProperty.append_submodel| then detects the first suitable ID
750
+ automatically:
751
+
752
+ >>> main_py.routingmodels.append_submodel(prepare_model("sw1d_weir_out"))
753
+ >>> main_py.routingmodels.append_submodel(prepare_model("sw1d_q_in"))
754
+ >>> main_py.routingmodels.append_submodel(prepare_model("sw1d_lias"))
755
+ >>> assert main_py.routingmodels.number == 3
756
+ >>> assert main_py.routingmodels.typeids == (3, 1, 2)
757
+
758
+ Method |SubmodelsProperty.append_submodel| checks if the given submodel follows
759
+ at least one supported interface:
760
+
761
+ >>> main_py.routingmodels[0].routingmodelsupstream.append_submodel(
762
+ ... prepare_model("sw1d_q_out"))
763
+ Traceback (most recent call last):
764
+ ...
765
+ ValueError: While trying to append submodel `sw1d_q_out` to property \
766
+ `routingmodelsupstream` of the main model `sw1d_weir_out`, the following error \
767
+ occurred: The given submodel is not an instance of any of the following supported \
768
+ interfaces: RoutingModel_V1 and RoutingModel_V2.
769
+ """
770
+ assert (mainmodel := self._mainmodel) is not None
771
+ try:
772
+ if typeid is None:
773
+ typeid = self._find_first_suitable_interface(submodel).typeid
774
+ else:
775
+ self._check_submodel_follows_interface(submodel)
776
+ if (cymain := mainmodel.cymodel) is not None:
777
+ if (cysub := submodel.cymodel) is None:
778
+ raise RuntimeError(self._CYTHON_PYTHON_SUBMODEL_ERROR_MESSAGE)
779
+ cyprop: interfaceutils.SubmodelsProperty = getattr(cymain, self.name)
780
+ cyprop.append_submodel(submodel=cysub, typeid=typeid)
781
+ self._mainmodel2numbersubmodels[mainmodel] += 1
782
+ self.__hydpy_mainmodel2submodels__[mainmodel].append(submodel)
783
+ self._mainmodel2submodeltypeids[mainmodel].append(typeid)
784
+ except BaseException:
785
+ objecttools.augment_excmessage(
786
+ f"While trying to append submodel `{submodel}` to property "
787
+ f"`{self.name}` of the main model `{mainmodel}`"
788
+ )
789
+
790
+ @property
791
+ def submodels(self) -> tuple[TypeSubmodelInterface | None, ...]:
792
+ """The currently handled submodels.
793
+
794
+ >>> from hydpy import prepare_model
795
+ >>> main = prepare_model("sw1d_channel")
796
+ >>> sub1 = prepare_model("sw1d_q_in")
797
+ >>> sub2 = prepare_model("sw1d_lias")
798
+ >>> main.routingmodels.append_submodel(submodel=sub1, typeid=1)
799
+ >>> main.routingmodels.append_submodel(submodel=sub2, typeid=1)
800
+ >>> assert main.routingmodels.submodels == (sub1, sub2)
801
+ """
802
+ assert (mainmodel := self._mainmodel) is not None
803
+ return tuple(self.__hydpy_mainmodel2submodels__[mainmodel])
804
+
805
+ @property
806
+ def typeids(self) -> tuple[int, ...]:
807
+ """The interface-specific type IDs of the currently handled submodels.
808
+
809
+ >>> from hydpy import prepare_model
810
+ >>> main = prepare_model("sw1d_channel")
811
+ >>> sub1 = prepare_model("sw1d_q_in")
812
+ >>> sub2 = prepare_model("sw1d_lias")
813
+ >>> main.routingmodels.append_submodel(submodel=sub1, typeid=1)
814
+ >>> main.routingmodels.append_submodel(submodel=sub2, typeid=1)
815
+ >>> assert main.routingmodels.typeids == (1, 1)
816
+ """
817
+ assert (mainmodel := self._mainmodel) is not None
818
+ return tuple(self._mainmodel2submodeltypeids[mainmodel])
819
+
820
+ def __getitem__(self, value: int) -> TypeSubmodelInterface | None:
821
+ assert (mainmodel := self._mainmodel) is not None
822
+ return self.__hydpy_mainmodel2submodels__[mainmodel][value]
823
+
824
+ def __iter__(self) -> Iterator[TypeSubmodelInterface | None]:
825
+ assert (mainmodel := self._mainmodel) is not None
826
+ yield from self.__hydpy_mainmodel2submodels__[mainmodel]
827
+
828
+ def __len__(self) -> int:
829
+ return self.number
830
+
831
+
832
+ class SubmodelIsMainmodelProperty:
833
+ """Descriptor for boolean "submodel_is_mainmodel" attributes.
834
+
835
+ |SubmodelIsMainmodelProperty| instances work like simple boolean attributes but
836
+ silently synchronise the equally named boolean attributes of the corresponding
837
+ cython model, if available:
838
+
839
+ >>> from hydpy import prepare_model, pub
840
+ >>> with pub.options.usecython(True):
841
+ ... model = prepare_model("hland_96")
842
+ >>> type(model).aetmodel_is_mainmodel._name
843
+ 'aetmodel_is_mainmodel'
844
+ >>> model.aetmodel_is_mainmodel
845
+ False
846
+ >>> model.cymodel.aetmodel_is_mainmodel
847
+ 0
848
+ >>> model.aetmodel_is_mainmodel = True
849
+ >>> model.aetmodel_is_mainmodel
850
+ True
851
+ >>> model.cymodel.aetmodel_is_mainmodel
852
+ 1
853
+ """
854
+
855
+ _owner2value: dict[Model, bool]
856
+ _name: Final[str] # type: ignore[misc]
857
+
858
+ def __init__(self, doc: str | None = None) -> None:
859
+ self._owner2value = {}
860
+ self.__doc__ = doc
861
+
862
+ def __set_name__(self, owner: type[Model], name: str) -> None:
863
+ self._name = name # type: ignore[misc]
864
+
865
+ @overload
866
+ def __get__(self, obj: None, objtype: type[Model] | None) -> Self: ...
867
+
868
+ @overload
869
+ def __get__(self, obj: Model, objtype: type[Model] | None) -> bool: ...
870
+
871
+ def __get__(
872
+ self, obj: Model | None, objtype: type[Model] | None = None
873
+ ) -> Self | bool:
874
+ if obj is None:
875
+ return self
876
+ return self._owner2value.get(obj, False)
877
+
878
+ def __set__(self, obj: Model, value: bool) -> None:
879
+ self._owner2value[obj] = value
880
+ if (cymodel := obj.cymodel) is not None:
881
+ setattr(cymodel, self._name, value)
882
+
883
+
884
+ class SubmodelTypeIDProperty:
885
+ """Descriptor for integer "submodel_typeid" attributes.
886
+
887
+ |SubmodelTypeIDProperty| instances work like simple integer attributes but silently
888
+ synchronise the equally named integer attributes of the corresponding cython model,
889
+ if available:
890
+
891
+ >>> from hydpy import prepare_model, pub
892
+ >>> with pub.options.usecython(True):
893
+ ... model = prepare_model("hland_96")
894
+ >>> type(model).aetmodel_typeid._name
895
+ 'aetmodel_typeid'
896
+ >>> model.aetmodel_typeid
897
+ 0
898
+ >>> model.cymodel.aetmodel_typeid
899
+ 0
900
+ >>> model.aetmodel_typeid = 1
901
+ >>> model.aetmodel_typeid
902
+ 1
903
+ >>> model.cymodel.aetmodel_typeid
904
+ 1
905
+ """
906
+
907
+ _owner2value: dict[Model, int]
908
+ _name: Final[str] # type: ignore[misc]
909
+
910
+ def __init__(self, doc: str | None = None) -> None:
911
+ self._owner2value = {}
912
+ self.__doc__ = doc
913
+
914
+ def __set_name__(self, owner: type[Model], name: str) -> None:
915
+ self._name = name # type: ignore[misc]
916
+
917
+ @overload
918
+ def __get__(self, obj: None, objtype: type[Model] | None) -> Self: ...
919
+
920
+ @overload
921
+ def __get__(self, obj: Model, objtype: type[Model] | None) -> int: ...
922
+
923
+ def __get__(
924
+ self, obj: Model | None, objtype: type[Model] | None = None
925
+ ) -> Self | int:
926
+ if obj is None:
927
+ return self
928
+ return self._owner2value.get(obj, 0)
929
+
930
+ def __set__(self, obj: Model, value: int) -> None:
931
+ self._owner2value[obj] = value
932
+ if (cymodel := obj.cymodel) is not None:
933
+ setattr(cymodel, self._name, value)
934
+
935
+
936
+ class IndexProperty:
937
+ """Base class for index descriptors like |Idx_Sim|."""
938
+
939
+ name: str
940
+
941
+ def __set_name__(self, owner: Model, name: str) -> None:
942
+ self.name = name.lower()
943
+
944
+ @overload
945
+ def __get__(self, obj: Model, objtype: type[Model]) -> int: ...
946
+
947
+ @overload
948
+ def __get__(self, obj: None, objtype: type[Model]) -> Self: ...
949
+
950
+ def __get__(self, obj: Model | None, objtype: type[Model]) -> Self | int:
951
+ if obj is None:
952
+ return self
953
+ if obj.cymodel:
954
+ return getattr(obj.cymodel, self.name)
955
+ return vars(obj).get(self.name, 0)
956
+
957
+ def __set__(self, obj: Model, value: int) -> None:
958
+ if obj.cymodel:
959
+ setattr(obj.cymodel, self.name, value)
960
+ else:
961
+ vars(obj)[self.name] = value
962
+
963
+
964
+ class Idx_Sim(IndexProperty):
965
+ """The simulation step index.
966
+
967
+ Some model methods require knowing the index of the current simulation step (with
968
+ respect to the initialisation period), which one usually updates by passing it to
969
+ |Model.simulate|. However, you can change it manually via the |modeltools.Idx_Sim|
970
+ descriptor, which is often beneficial during testing:
971
+
972
+ >>> from hydpy.models.hland_96 import *
973
+ >>> parameterstep("1d")
974
+ >>> model.idx_sim
975
+ 0
976
+ >>> model.idx_sim = 1
977
+ >>> model.idx_sim
978
+ 1
979
+
980
+ Like other objects of |IndexProperty| subclasses, |Idx_Sim| objects are aware of
981
+ their name:
982
+
983
+ >>> Model.idx_sim.name
984
+ 'idx_sim'
985
+ """
986
+
987
+ def __init__(self) -> None:
988
+ self.__doc__ = "The simulation step index."
989
+
990
+
991
+ class Idx_HRU(IndexProperty):
992
+ """The hydrological response unit index.
993
+
994
+ The documentation on class |Idx_Sim| explains the general purpose and handling of
995
+ |IndexProperty| instances.
996
+ """
997
+
998
+ def __init__(self) -> None:
999
+ self.__doc__ = "The hydrological response unit index."
1000
+
1001
+
1002
+ class Idx_Segment(IndexProperty):
1003
+ """The segment index.
1004
+
1005
+ The documentation on class |Idx_Sim| explains the general purpose and handling of
1006
+ |IndexProperty| instances.
1007
+ """
1008
+
1009
+ def __init__(self) -> None:
1010
+ self.__doc__ = "The segment index."
1011
+
1012
+
1013
+ class Idx_Run(IndexProperty):
1014
+ """The run index.
1015
+
1016
+ The documentation on class |Idx_Sim| explains the general purpose and handling of
1017
+ |IndexProperty| instances.
1018
+ """
1019
+
1020
+ def __init__(self) -> None:
1021
+ self.__doc__ = "The run index."
1022
+
1023
+
1024
+ class DocName(NamedTuple):
1025
+ """Definitions for the documentation names of specific base or application
1026
+ models."""
1027
+
1028
+ short: str
1029
+ """Short name of a model, e.g. "W-Wag"."""
1030
+
1031
+ description: str = "base model"
1032
+ """Description of a model, e.g. "extended version of the original Wageningen WALRUS
1033
+ model"."""
1034
+
1035
+ @property
1036
+ def long(self):
1037
+ """Long name of a model.
1038
+
1039
+ >>> from hydpy.models.wland_wag import Model
1040
+ >>> Model.DOCNAME.long
1041
+ 'HydPy-W-Wag'
1042
+ """
1043
+ return f"HydPy-{self.short}"
1044
+
1045
+ @property
1046
+ def complete(self) -> str:
1047
+ """Complete presentation of a model.
1048
+
1049
+ >>> from hydpy.models.wland_wag import Model
1050
+ >>> Model.DOCNAME.complete
1051
+ 'HydPy-W-Wag (extended version of the original Wageningen WALRUS model)'
1052
+ """
1053
+ return f"{self.long} ({self.description})"
1054
+
1055
+ @property
1056
+ def family(self) -> str:
1057
+ """Family name of a model.
1058
+
1059
+ >>> from hydpy.models.wland_wag import Model
1060
+ >>> Model.DOCNAME.family
1061
+ 'HydPy-W'
1062
+ """
1063
+ return "-".join(self.long.split("-")[:2])
1064
+
1065
+
1066
+ class Model:
1067
+ """Base class for all hydrological models.
1068
+
1069
+ Class |Model| provides everything to create a usable application model, except
1070
+ method |Model.simulate|. See classes |AdHocModel| and |ELSModel|, which implement
1071
+ this method.
1072
+
1073
+ Class |Model| does not prepare the strongly required attributes `parameters` and
1074
+ `sequences` during initialisation. You need to add them manually whenever you want
1075
+ to prepare a workable |Model| object on your own (see the factory functions
1076
+ |prepare_model| and |parameterstep|, which do this regularly).
1077
+
1078
+ Similar to `parameters` and `sequences`, there is also the dynamic `masks`
1079
+ attribute, making all predefined masks of the actual model type available within a
1080
+ |Masks| object:
1081
+
1082
+ >>> from hydpy.models.hland_96 import *
1083
+ >>> parameterstep("1d")
1084
+ >>> model.masks
1085
+ complete of module hydpy.models.hland.hland_masks
1086
+ land of module hydpy.models.hland.hland_masks
1087
+ upperzone of module hydpy.models.hland.hland_masks
1088
+ snow of module hydpy.models.hland.hland_masks
1089
+ soil of module hydpy.models.hland.hland_masks
1090
+ field of module hydpy.models.hland.hland_masks
1091
+ forest of module hydpy.models.hland.hland_masks
1092
+ ilake of module hydpy.models.hland.hland_masks
1093
+ glacier of module hydpy.models.hland.hland_masks
1094
+ sealed of module hydpy.models.hland.hland_masks
1095
+ noglacier of module hydpy.models.hland.hland_masks
1096
+
1097
+ You can use these masks, for example, to average the zone-specific precipitation
1098
+ values handled by sequence |hland_fluxes.PC|. When passing no argument, method
1099
+ |Variable.average_values| applies the `complete` mask. For example, pass mask
1100
+ `land` to average the values of all zones except those of type
1101
+ |hland_constants.ILAKE|:
1102
+
1103
+ >>> nmbzones(4)
1104
+ >>> zonetype(FIELD, FOREST, GLACIER, ILAKE)
1105
+ >>> zonearea.values = 1.0
1106
+ >>> fluxes.pc = 1.0, 3.0, 5.0, 7.0
1107
+ >>> fluxes.pc.average_values()
1108
+ 4.0
1109
+ >>> fluxes.pc.average_values(model.masks.land)
1110
+ 3.0
1111
+ """
1112
+
1113
+ cymodel: CyModelProtocol | None
1114
+ parameters: parametertools.Parameters
1115
+ sequences: sequencetools.Sequences
1116
+ masks: masktools.Masks
1117
+ idx_sim = Idx_Sim()
1118
+
1119
+ __hydpy_element__: devicetools.Element | None
1120
+ __HYDPY_NAME__: ClassVar[str]
1121
+
1122
+ INLET_METHODS: ClassVar[tuple[type[Method], ...]]
1123
+ OUTLET_METHODS: ClassVar[tuple[type[Method], ...]]
1124
+ RECEIVER_METHODS: ClassVar[tuple[type[Method], ...]]
1125
+ SENDER_METHODS: ClassVar[tuple[type[Method], ...]]
1126
+ ADD_METHODS: ClassVar[tuple[Callable, ...]]
1127
+ METHOD_GROUPS: ClassVar[tuple[str, ...]]
1128
+ SUBMODELINTERFACES: ClassVar[tuple[type[SubmodelInterface], ...]]
1129
+ SUBMODELS: ClassVar[tuple[type[Submodel], ...]]
1130
+
1131
+ SOLVERPARAMETERS: tuple[type[parametertools.Parameter], ...] = ()
1132
+
1133
+ REUSABLE_METHODS: ClassVar[tuple[type[ReusableMethod], ...]]
1134
+
1135
+ COMPOSITE: bool = False
1136
+ """Flag for informing whether the respective |Model| subclass is usually not
1137
+ directly applied by model users but behind the scenes for compositing all models
1138
+ owned by elements belonging to the same |Element.collective| (see method
1139
+ |Elements.unite_collectives|)."""
1140
+
1141
+ DOCNAME: DocName
1142
+
1143
+ __HYDPY_ROOTMODEL__: bool | None
1144
+ """Flag telling whether a submodel should be considered as a submodel graph root.
1145
+
1146
+ `None` is reserved for base and special-purpose models likely irrelevant to users.
1147
+ """
1148
+
1149
+ def __init__(self) -> None:
1150
+ self.cymodel = None
1151
+ self.__hydpy_element__ = None
1152
+ self._init_methods()
1153
+
1154
+ def _init_methods(self) -> None:
1155
+ """Convert all pure Python calculation functions of the model class to methods
1156
+ and assign them to the model instance."""
1157
+ blacklist_shortnames: set[str] = set()
1158
+ shortname2method: dict[str, types.MethodType] = {}
1159
+ for cls_ in self.get_methods():
1160
+ longname = cls_.__name__.lower()
1161
+ if issubclass(cls_, ReusableMethod):
1162
+ setattr(self, cls_.REUSEMARKER, False)
1163
+ method = types.MethodType(cls_.call_reusablemethod, self)
1164
+ else:
1165
+ method = types.MethodType(cls_.__call__, self)
1166
+ setattr(self, longname, method)
1167
+ shortname = longname.rpartition("_")[0]
1168
+ if shortname in blacklist_shortnames:
1169
+ continue
1170
+ if shortname in shortname2method:
1171
+ del shortname2method[shortname]
1172
+ blacklist_shortnames.add(shortname)
1173
+ else:
1174
+ shortname2method[shortname] = method
1175
+ for shortname, method in shortname2method.items():
1176
+ setattr(self, shortname, method)
1177
+
1178
+ @property
1179
+ def element(self) -> devicetools.Element:
1180
+ """The model instance's master element.
1181
+
1182
+ Usually, one assigns a |Model| instance to an |Element| instance, but the other
1183
+ way round works as well (for more information, see the documentation on
1184
+ property |Element.model| of class |Element|):
1185
+
1186
+ >>> from hydpy import Element, prepare_model
1187
+ >>> from hydpy.core.modeltools import Model
1188
+ >>> model = prepare_model("musk_classic")
1189
+ >>> model.element
1190
+ Traceback (most recent call last):
1191
+ ...
1192
+ hydpy.core.exceptiontools.AttributeNotReady: Model `musk_classic` is not \
1193
+ connected to an `Element` so far.
1194
+
1195
+ >>> e = Element("e")
1196
+ >>> model.element = e
1197
+ Traceback (most recent call last):
1198
+ ...
1199
+ RuntimeError: While trying to build the node connection of the `outlet` \
1200
+ sequences of the model handled by element `e`, the following error occurred: Sequence \
1201
+ `q` of element `e` cannot be connected due to no available node handling variable `Q`.
1202
+ >>> model.element
1203
+ Element("e")
1204
+ >>> e.model.name
1205
+ 'musk_classic'
1206
+
1207
+ >>> del model.element
1208
+ >>> model.element
1209
+ Traceback (most recent call last):
1210
+ ...
1211
+ hydpy.core.exceptiontools.AttributeNotReady: Model `musk_classic` is not \
1212
+ connected to an `Element` so far.
1213
+ >>> e.model
1214
+ Traceback (most recent call last):
1215
+ ...
1216
+ hydpy.core.exceptiontools.AttributeNotReady: The model object of element `e` \
1217
+ has been requested but not been prepared so far.
1218
+ """
1219
+ if (element := self.__hydpy_element__) is None:
1220
+ raise exceptiontools.AttributeNotReady(
1221
+ f"Model `{self.name}` is not connected to an `Element` so far."
1222
+ )
1223
+ return element
1224
+
1225
+ @element.setter
1226
+ def element(self, element: devicetools.Element) -> None:
1227
+ self.__hydpy_element__ = element
1228
+ if not self.COMPOSITE:
1229
+ for model in self.find_submodels().values():
1230
+ model.__hydpy_element__ = element
1231
+ if exceptiontools.getattr_(element, "model", None) is not self:
1232
+ element.model = self
1233
+
1234
+ @element.deleter
1235
+ def element(self) -> None:
1236
+ if (element := self.__hydpy_element__) is not None:
1237
+ if exceptiontools.getattr_(element, "model", None) is self:
1238
+ del element.model
1239
+ for model in self.find_submodels(include_mainmodel=True).values():
1240
+ model.__hydpy_element__ = None
1241
+
1242
+ def connect(self) -> None:
1243
+ """Connect all |LinkSequence| objects and the selected |InputSequence| and
1244
+ |OutputSequence| objects of the actual model to the corresponding
1245
+ |NodeSequence| objects.
1246
+
1247
+ You cannot connect any sequences until the |Model| object itself is connected
1248
+ to an |Element| object referencing the required |Node| objects:
1249
+
1250
+ >>> from hydpy import prepare_model
1251
+ >>> prepare_model("musk_classic").connect()
1252
+ Traceback (most recent call last):
1253
+ ...
1254
+ hydpy.core.exceptiontools.AttributeNotReady: While trying to build the node \
1255
+ connection of the `input` sequences of the model handled by element `?`, the \
1256
+ following error occurred: Model `musk_classic` is not connected to an `Element` so far.
1257
+
1258
+ The application model |musk_classic| can receive inflow from an arbitrary
1259
+ number of upstream nodes and passes its outflow to a single downstream node
1260
+ (note that property |Element.model| of class |Element| calls method
1261
+ |Model.connect| automatically):
1262
+
1263
+ >>> from hydpy import Element, Node
1264
+ >>> in1 = Node("in1", variable="Q")
1265
+ >>> in2 = Node("in2", variable="Q")
1266
+ >>> out1 = Node("out1", variable="Q")
1267
+
1268
+ >>> element1 = Element("element1", inlets=(in1, in2), outlets=out1)
1269
+ >>> element1.model = prepare_model("musk_classic")
1270
+
1271
+ Now all connections work as expected:
1272
+
1273
+ >>> in1.sequences.sim = 1.0
1274
+ >>> in2.sequences.sim = 2.0
1275
+ >>> out1.sequences.sim = 3.0
1276
+ >>> element1.model.sequences.inlets.q
1277
+ q(1.0, 2.0)
1278
+ >>> element1.model.sequences.outlets.q
1279
+ q(3.0)
1280
+ >>> element1.model.sequences.inlets.q *= 2.0
1281
+ >>> element1.model.sequences.outlets.q *= 2.0
1282
+ >>> in1.sequences.sim
1283
+ sim(2.0)
1284
+ >>> in2.sequences.sim
1285
+ sim(4.0)
1286
+ >>> out1.sequences.sim
1287
+ sim(6.0)
1288
+
1289
+ To show some possible errors and related error messages, we define three
1290
+ additional nodes, two handling variables different from discharge (`Q`):
1291
+
1292
+ >>> in3 = Node("in3", variable="X")
1293
+ >>> out2 = Node("out2", variable="Q")
1294
+ >>> out3 = Node("out3", variable="X")
1295
+
1296
+ Link sequence names must match the `variable` a node is handling:
1297
+
1298
+ >>> element2 = Element("element2", inlets=(in1, in2), outlets=out3)
1299
+ >>> element2.model = prepare_model("musk_classic")
1300
+ Traceback (most recent call last):
1301
+ ...
1302
+ RuntimeError: While trying to build the node connection of the `outlet` \
1303
+ sequences of the model handled by element `element2`, the following error occurred: \
1304
+ Sequence `q` of element `element2` cannot be connected due to no available node \
1305
+ handling variable `Q`.
1306
+
1307
+ One can connect a 0-dimensional link sequence to a single node sequence only:
1308
+
1309
+ >>> element3 = Element("element3", inlets=(in1, in2), outlets=(out1, out2))
1310
+ >>> element3.model = prepare_model("musk_classic")
1311
+ Traceback (most recent call last):
1312
+ ...
1313
+ RuntimeError: While trying to build the node connection of the `outlet` \
1314
+ sequences of the model handled by element `element3`, the following error occurred: \
1315
+ Sequence `q` cannot be connected as it is 0-dimensional but multiple nodes are \
1316
+ available which are handling variable `Q`.
1317
+
1318
+ Method |Model.connect| generally reports about unusable node sequences:
1319
+
1320
+ >>> element4 = Element("element4", inlets=(in1, in2), outlets=(out1, out3))
1321
+ >>> element4.model = prepare_model("musk_classic")
1322
+ Traceback (most recent call last):
1323
+ ...
1324
+ RuntimeError: While trying to build the node connection of the `outlet` \
1325
+ sequences of the model handled by element `element4`, the following error occurred: \
1326
+ The following nodes have not been connected to any sequences: out3.
1327
+
1328
+ >>> element5 = Element("element5", inlets=(in1, in2, in3), outlets=out1)
1329
+ >>> element5.model = prepare_model("musk_classic")
1330
+ Traceback (most recent call last):
1331
+ ...
1332
+ RuntimeError: While trying to build the node connection of the `inlet` \
1333
+ sequences of the model handled by element `element5`, the following error occurred: \
1334
+ The following nodes have not been connected to any sequences: in3.
1335
+
1336
+ >>> element6 = Element("element6", inlets=in1, outlets=out1, receivers=in2)
1337
+ >>> element6.model = prepare_model("musk_classic")
1338
+ Traceback (most recent call last):
1339
+ ...
1340
+ RuntimeError: While trying to build the node connection of the `receiver` \
1341
+ sequences of the model handled by element `element6`, the following error occurred: \
1342
+ The following nodes have not been connected to any sequences: in2.
1343
+
1344
+ >>> element7 = Element("element7", inlets=in1, outlets=out1, senders=in2)
1345
+ >>> element7.model = prepare_model("musk_classic")
1346
+ Traceback (most recent call last):
1347
+ ...
1348
+ RuntimeError: While trying to build the node connection of the `sender` \
1349
+ sequences of the model handled by element `element7`, the following error occurred: \
1350
+ The following nodes have not been connected to any sequences: in2.
1351
+
1352
+ The above examples explain how to connect link sequences to their nodes. Such
1353
+ connections are relatively hard requirements (|musk_classic| definitively needs
1354
+ inflow provided from a node, which the node itself typically receives from
1355
+ another model). In contrast, connections between input or output sequences and
1356
+ nodes are optional. If one defines such a connection for an input sequence, it
1357
+ receives data from the related node; otherwise, it uses its individually
1358
+ managed data, usually read from a file. If one defines such a connection for
1359
+ an output sequence, it passes its internal data to the related node; otherwise,
1360
+ nothing happens.
1361
+
1362
+ We demonstrate this functionality by focussing on the input sequences
1363
+ |hland_inputs.T| and |hland_inputs.P| and the output sequences |hland_fluxes.Q0|
1364
+ and |hland_states.UZ| of application model |hland_96|. |hland_inputs.T| uses
1365
+ its own data (which we define manually, but we could read it from a file as
1366
+ well), whereas |hland_inputs.P| gets its data from node `inp1`. Flux sequence
1367
+ |hland_fluxes.Q0| and state sequence |hland_states.UZ| pass their data to two
1368
+ separate output nodes, whereas all other fluxes and states do not. This
1369
+ functionality requires telling each node which sequence it should connect to,
1370
+ which we do by passing the sequence types (or the globally available aliases
1371
+ `hland_inputs_P`, `hland_fluxes_Q0`, and `hland_states_UZ`) to the `variable`
1372
+ keyword of different node objects:
1373
+
1374
+ >>> from hydpy import pub
1375
+ >>> from hydpy.aliases import hland_inputs_P, hland_fluxes_Q0, hland_states_UZ
1376
+ >>> pub.timegrids = "2000-01-01", "2000-01-06", "1d"
1377
+
1378
+ >>> inp1 = Node("inp1", variable=hland_inputs_P)
1379
+ >>> outp1 = Node("outp1", variable=hland_fluxes_Q0)
1380
+ >>> outp2 = Node("outp2", variable=hland_states_UZ)
1381
+ >>> element8 = Element("element8", outlets=out1, inputs=inp1,
1382
+ ... outputs=[outp1, outp2])
1383
+ >>> element8.model = prepare_model("hland_96")
1384
+ >>> element8.prepare_inputseries()
1385
+ >>> element8.model.sequences.inputs.t.series = 1.0, 2.0, 3.0, 4.0, 5.0
1386
+ >>> inp1.sequences.sim(9.0)
1387
+ >>> element8.model.load_data(2)
1388
+ >>> element8.model.sequences.inputs.t
1389
+ t(3.0)
1390
+ >>> element8.model.sequences.inputs.p
1391
+ p(9.0)
1392
+ >>> element8.model.sequences.fluxes.q0 = 99.0
1393
+ >>> element8.model.sequences.states.uz = 999.0
1394
+ >>> element8.model.update_outputs()
1395
+ >>> outp1.sequences.sim
1396
+ sim(99.0)
1397
+ >>> outp2.sequences.sim
1398
+ sim(999.0)
1399
+
1400
+ Instead of using single |InputSequence| and |OutputSequence| subclasses, one
1401
+ can create and apply fused variables, combining multiple subclasses (see the
1402
+ documentation on class |FusedVariable| for more information and a more
1403
+ realistic example):
1404
+
1405
+ >>> from hydpy import FusedVariable
1406
+ >>> from hydpy.aliases import lland_inputs_Nied, lland_fluxes_QDGZ
1407
+ >>> Precip = FusedVariable("Precip", hland_inputs_P, lland_inputs_Nied)
1408
+ >>> inp2 = Node("inp2", variable=Precip)
1409
+ >>> FastRunoff = FusedVariable("FastRunoff", hland_fluxes_Q0, lland_fluxes_QDGZ)
1410
+ >>> outp3 = Node("outp3", variable=FastRunoff)
1411
+ >>> element9 = Element("element9", outlets=out1, inputs=inp2, outputs=outp3)
1412
+ >>> element9.model = prepare_model("hland_96")
1413
+ >>> inp2.sequences.sim(9.0)
1414
+ >>> element9.model.load_data(0)
1415
+ >>> element9.model.sequences.inputs.p
1416
+ p(9.0)
1417
+ >>> element9.model.sequences.fluxes.q0 = 99.0
1418
+ >>> element9.model.update_outputs()
1419
+ >>> outp3.sequences.sim
1420
+ sim(99.0)
1421
+
1422
+ Method |Model.connect| reports if one of the given fused variables does not
1423
+ find a fitting sequence:
1424
+
1425
+ >>> from hydpy.aliases import lland_inputs_TemL
1426
+ >>> Wrong = FusedVariable("Wrong", lland_inputs_Nied, lland_inputs_TemL)
1427
+ >>> inp3 = Node("inp3", variable=Wrong)
1428
+ >>> element10 = Element("element10", outlets=out1, inputs=inp3)
1429
+ >>> element10.model = prepare_model("hland_96")
1430
+ Traceback (most recent call last):
1431
+ ...
1432
+ RuntimeError: While trying to build the node connection of the `input` \
1433
+ sequences of the model handled by element `element10`, the following error occurred: \
1434
+ The following nodes have not been connected to any sequences: inp3.
1435
+
1436
+ >>> outp4 = Node("outp4", variable=Wrong)
1437
+ >>> element11 = Element("element11", outlets=out1, outputs=outp4)
1438
+ >>> element11.model = prepare_model("hland_96")
1439
+ Traceback (most recent call last):
1440
+ ...
1441
+ TypeError: While trying to build the node connection of the `output` \
1442
+ sequences of the model handled by element `element11`, the following error occurred: \
1443
+ None of the output sequences of model `hland_96` is among the sequences of the fused \
1444
+ variable `Wrong` of node `outp4`.
1445
+
1446
+ Selecting the wrong sequences results in the following error messages:
1447
+
1448
+ >>> outp5 = Node("outp5", variable=hland_fluxes_Q0)
1449
+ >>> element12 = Element("element12", outlets=out1, inputs=outp5)
1450
+ >>> element12.model = prepare_model("hland_96")
1451
+ Traceback (most recent call last):
1452
+ ...
1453
+ RuntimeError: While trying to build the node connection of the `input` \
1454
+ sequences of the model handled by element `element12`, the following error occurred: \
1455
+ The following nodes have not been connected to any sequences: outp5.
1456
+
1457
+ >>> inp5 = Node("inp5", variable="P")
1458
+ >>> element13 = Element("element13", outlets=out1, outputs=inp5)
1459
+ >>> element13.model = prepare_model("hland_96")
1460
+ Traceback (most recent call last):
1461
+ ...
1462
+ TypeError: While trying to build the node connection of the `output` sequences \
1463
+ of the model handled by element `element13`, the following error occurred: No factor, \
1464
+ flux, or state sequence of model `hland_96` is named `p`.
1465
+
1466
+ So far, you can build connections to 0-dimensional output sequences only:
1467
+
1468
+ >>> from hydpy.models.hland.hland_fluxes import PC
1469
+ >>> outp6 = Node("outp6", variable=PC)
1470
+ >>> element14 = Element("element14", outlets=out1, outputs=outp6)
1471
+ >>> element14.model = prepare_model("hland_96")
1472
+ Traceback (most recent call last):
1473
+ ...
1474
+ TypeError: While trying to build the node connection of the `output` sequences \
1475
+ of the model handled by element `element14`, the following error occurred: Only \
1476
+ connections with 0-dimensional output sequences are supported, but sequence `pc` is \
1477
+ 1-dimensional.
1478
+
1479
+ |FusedVariable| also supports |ReceiverSequence| for passing information from
1480
+ output nodes to receiver sequences (instead of input sequences, which we
1481
+ demonstrated in the above examples). We take the receiver sequences
1482
+ |dam_receivers.OWL| (outer water level) and |dam_receivers.RWL| (remote water
1483
+ level) used by the application model |dam_pump| as an example:
1484
+
1485
+ >>> from hydpy.aliases import dam_receivers_OWL, dam_receivers_RWL
1486
+
1487
+ One |dam_pump| instance (handled by element `dam1`) shall receive the water
1488
+ level (|dam_factors.WaterLevel|) of two independent |dam_pump| instances.
1489
+ `dam1` interprets the water level of `dam2` as its outer water level and the
1490
+ water level of `dam3` as its remote water level:
1491
+
1492
+ >>> from hydpy.aliases import dam_factors_WaterLevel
1493
+ >>> owl = FusedVariable("OWL", dam_receivers_OWL, dam_factors_WaterLevel)
1494
+ >>> rwl = FusedVariable("RWL", dam_receivers_RWL, dam_factors_WaterLevel)
1495
+ >>> n21, n31 = Node("n21", variable=owl), Node("n31", variable=rwl)
1496
+ >>> x, y = Node("x", variable=owl), Node("y", variable=rwl)
1497
+ >>> dam1 = Element("dam1", inlets="n01", outlets="n12",
1498
+ ... receivers=(n21, n31))
1499
+ >>> dam2 = Element("dam2", inlets="n12", outlets="n23",
1500
+ ... receivers=(x,y), outputs=n21)
1501
+ >>> dam3 = Element("dam3", inlets="n23", outlets="n34",
1502
+ ... receivers=(x, y), outputs=n31)
1503
+ >>> dam1.model = prepare_model("dam_pump")
1504
+ >>> dam2.model = prepare_model("dam_pump")
1505
+ >>> dam3.model = prepare_model("dam_pump")
1506
+
1507
+ We confirm that all connections are correctly built by letting `dam2` and
1508
+ `dam3` send different water levels:
1509
+
1510
+ >>> dam2.model.sequences.factors.waterlevel = 2.0
1511
+ >>> dam2.model.update_outputs()
1512
+ >>> dam3.model.sequences.factors.waterlevel = 3.0
1513
+ >>> dam3.model.update_outputs()
1514
+ >>> dam1.model.sequences.receivers.owl
1515
+ owl(2.0)
1516
+ >>> dam1.model.sequences.receivers.rwl
1517
+ rwl(3.0)
1518
+
1519
+ .. testsetup::
1520
+
1521
+ >>> Node.clear_all()
1522
+ >>> Element.clear_all()
1523
+ >>> FusedVariable.clear_registry()
1524
+ """
1525
+ group = "inputs"
1526
+ try:
1527
+ self._connect_inputs()
1528
+ group = "outputs"
1529
+ self._connect_outputs()
1530
+ group = "inlets"
1531
+ self._connect_inlets()
1532
+ group = "receivers"
1533
+ self._connect_receivers()
1534
+ group = "outlets"
1535
+ self._connect_outlets()
1536
+ group = "senders"
1537
+ self._connect_senders()
1538
+ except BaseException:
1539
+ objecttools.augment_excmessage(
1540
+ f"While trying to build the node connection of the `{group[:-1]}` "
1541
+ f"sequences of the model handled by element "
1542
+ f"`{objecttools.devicename(self)}`"
1543
+ )
1544
+
1545
+ def _connect_inputs(self, report_noconnect: bool = True) -> None:
1546
+ self._connect_subgroup("inputs", report_noconnect)
1547
+
1548
+ def _connect_outputs(self) -> None:
1549
+ def _set_pointer(
1550
+ seq: sequencetools.OutputSequence, node_: devicetools.Node
1551
+ ) -> None:
1552
+ if seq.NDIM > 0:
1553
+ raise TypeError(
1554
+ f"Only connections with 0-dimensional output sequences are "
1555
+ f"supported, but sequence `{seq.name}` is {seq.NDIM}-dimensional."
1556
+ )
1557
+ seq.set_pointer(node_.get_double("outputs"))
1558
+
1559
+ for node in self.element.outputs:
1560
+ if isinstance(node.variable, devicetools.FusedVariable):
1561
+ connected = False
1562
+ for submodel in self.find_submodels(include_mainmodel=True).values():
1563
+ for sequence in itertools.chain(
1564
+ submodel.sequences.factors,
1565
+ submodel.sequences.fluxes,
1566
+ submodel.sequences.states,
1567
+ ):
1568
+ if sequence in node.variable:
1569
+ _set_pointer(sequence, node)
1570
+ connected = True
1571
+ break
1572
+ if not connected:
1573
+ submodelphrase = objecttools.submodelphrase(self)
1574
+ raise TypeError(
1575
+ f"None of the output sequences of {submodelphrase} is among "
1576
+ f"the sequences of the fused variable `{node.variable}` of "
1577
+ f"node `{node.name}`."
1578
+ )
1579
+ else:
1580
+ name = self._determine_name(node.variable)
1581
+ sequence_ = getattr(self.sequences.factors, name, None)
1582
+ if sequence_ is None:
1583
+ sequence_ = getattr(self.sequences.fluxes, name, None)
1584
+ if sequence_ is None:
1585
+ sequence_ = getattr(self.sequences.states, name, None)
1586
+ if sequence_ is None:
1587
+ raise TypeError(
1588
+ f"No factor, flux, or state sequence of model `{self}` is "
1589
+ f"named `{name}`."
1590
+ )
1591
+ _set_pointer(sequence_, node)
1592
+
1593
+ def _determine_name(self, var: str | sequencetools.InOutSequenceTypes) -> str:
1594
+ if isinstance(var, str):
1595
+ return var.lower()
1596
+ return var.__name__.lower()
1597
+
1598
+ def _connect_inlets(self, report_noconnect: bool = True) -> None:
1599
+ self._connect_subgroup("inlets", report_noconnect, 0)
1600
+
1601
+ def _connect_receivers(self, report_noconnect: bool = True) -> None:
1602
+ self._connect_subgroup("receivers", report_noconnect, -1)
1603
+
1604
+ def _connect_outlets(self, report_noconnect: bool = True) -> None:
1605
+ self._connect_subgroup("outlets", report_noconnect, -1)
1606
+
1607
+ def _connect_senders(self, report_noconnect: bool = True) -> None:
1608
+ self._connect_subgroup("senders", report_noconnect, 0)
1609
+
1610
+ def _connect_subgroup(
1611
+ self, group: str, report_noconnect: bool, position: Literal[0, -1] | None = None
1612
+ ) -> None:
1613
+ st = sequencetools
1614
+ available_nodes = getattr(self.element, group)
1615
+ applied_nodes = []
1616
+ for submodel in self.find_submodels(
1617
+ include_mainmodel=True, position=position
1618
+ ).values():
1619
+ sequences = submodel.sequences[group]
1620
+ for sequence in sequences:
1621
+ selected_nodes = []
1622
+ for node in available_nodes:
1623
+ if isinstance(var := node.variable, devicetools.FusedVariable):
1624
+ if sequence in var:
1625
+ selected_nodes.append(node)
1626
+ else:
1627
+ name = var.lower() if isinstance(var, str) else var.name
1628
+ if name == sequence.name:
1629
+ selected_nodes.append(node)
1630
+ if sequence.NDIM == 0:
1631
+ if not selected_nodes:
1632
+ if (group == "inputs") or not report_noconnect:
1633
+ # see https://github.com/nedbat/coveragepy/issues/198:
1634
+ continue # pragma: no cover
1635
+ raise RuntimeError(
1636
+ f"Sequence {objecttools.elementphrase(sequence)} cannot "
1637
+ f"be connected due to no available node handling variable "
1638
+ f"`{sequence.name.upper()}`."
1639
+ )
1640
+ if len(selected_nodes) > 1:
1641
+ raise RuntimeError(
1642
+ f"Sequence `{sequence.name}` cannot be connected as it is "
1643
+ f"0-dimensional but multiple nodes are available which "
1644
+ f"are handling variable `{type(sequence).__name__}`."
1645
+ )
1646
+ applied_nodes.append(selected_nodes[0])
1647
+ assert isinstance(sequence, (st.InputSequence, st.LinkSequence))
1648
+ sequence.set_pointer(selected_nodes[0].get_double(group))
1649
+ elif sequence.NDIM == 1:
1650
+ sequence.shape = len(selected_nodes)
1651
+ for idx, node in enumerate(selected_nodes):
1652
+ applied_nodes.append(node)
1653
+ assert isinstance(sequence, st.LinkSequence)
1654
+ sequence.set_pointer(node.get_double(group), idx)
1655
+ if report_noconnect and (len(applied_nodes) < len(available_nodes)):
1656
+ remaining_nodes = [
1657
+ node.name for node in available_nodes if node not in applied_nodes
1658
+ ]
1659
+ raise RuntimeError(
1660
+ f"The following nodes have not been connected to any sequences: "
1661
+ f"{objecttools.enumeration(remaining_nodes)}."
1662
+ )
1663
+
1664
+ @property
1665
+ def name(self) -> str:
1666
+ """Name of the model type.
1667
+
1668
+ For base models, |Model.name| corresponds to the package name:
1669
+
1670
+ >>> from hydpy import prepare_model
1671
+ >>> hland = prepare_model("hland")
1672
+ >>> hland.name
1673
+ 'hland'
1674
+
1675
+ For application models, |Model.name| to corresponds the module name:
1676
+
1677
+ >>> hland_96 = prepare_model("hland_96")
1678
+ >>> hland_96.name
1679
+ 'hland_96'
1680
+
1681
+ This last example has only technical reasons:
1682
+
1683
+ >>> hland.name
1684
+ 'hland'
1685
+ """
1686
+ return self.__HYDPY_NAME__
1687
+
1688
+ def prepare_allseries(self, allocate_ram: bool = True, jit: bool = False) -> None:
1689
+ """Call method |Model.prepare_inputseries| with `read_jit=jit` and methods
1690
+ |Model.prepare_factorseries|, |Model.prepare_fluxseries|, and
1691
+ |Model.prepare_stateseries| with `write_jit=jit`."""
1692
+ self.prepare_inputseries(allocate_ram=allocate_ram, read_jit=jit)
1693
+ self.prepare_factorseries(allocate_ram=allocate_ram, write_jit=jit)
1694
+ self.prepare_fluxseries(allocate_ram=allocate_ram, write_jit=jit)
1695
+ self.prepare_stateseries(allocate_ram=allocate_ram, write_jit=jit)
1696
+
1697
+ def prepare_inputseries(
1698
+ self, allocate_ram: bool = True, read_jit: bool = False, write_jit: bool = False
1699
+ ) -> None:
1700
+ """Call method |IOSequence.prepare_series| of all directly handled
1701
+ |InputSequence| objects."""
1702
+ self.sequences.inputs.prepare_series(
1703
+ allocate_ram=allocate_ram, read_jit=read_jit, write_jit=write_jit
1704
+ )
1705
+
1706
+ def prepare_factorseries(
1707
+ self, allocate_ram: bool = True, read_jit: bool = False, write_jit: bool = False
1708
+ ) -> None:
1709
+ """Call method |IOSequence.prepare_series| of all directly handled
1710
+ |FactorSequence| objects."""
1711
+ self.sequences.factors.prepare_series(
1712
+ allocate_ram=allocate_ram, read_jit=read_jit, write_jit=write_jit
1713
+ )
1714
+
1715
+ def prepare_fluxseries(
1716
+ self, allocate_ram: bool = True, read_jit: bool = False, write_jit: bool = False
1717
+ ) -> None:
1718
+ """Call method |IOSequence.prepare_series| of all directly handled
1719
+ |FluxSequence|."""
1720
+ self.sequences.fluxes.prepare_series(
1721
+ allocate_ram=allocate_ram, read_jit=read_jit, write_jit=write_jit
1722
+ )
1723
+
1724
+ def prepare_stateseries(
1725
+ self, allocate_ram: bool = True, read_jit: bool = False, write_jit: bool = False
1726
+ ) -> None:
1727
+ """Call method |IOSequence.prepare_series| of all directly handled
1728
+ |StateSequence| objects and."""
1729
+ self.sequences.states.prepare_series(
1730
+ allocate_ram=allocate_ram, read_jit=read_jit, write_jit=write_jit
1731
+ )
1732
+
1733
+ def load_allseries(self) -> None:
1734
+ """Call method |Model.load_inputseries|, |Model.load_factorseries|,
1735
+ |Model.load_fluxseries|, and |Model.load_stateseries|."""
1736
+ self.load_inputseries()
1737
+ self.load_factorseries()
1738
+ self.load_fluxseries()
1739
+ self.load_stateseries()
1740
+
1741
+ def load_inputseries(self) -> None:
1742
+ """Call method |IOSequence.load_series| of all directly handled |InputSequence|
1743
+ objects."""
1744
+ self.sequences.inputs.load_series()
1745
+
1746
+ def load_factorseries(self) -> None:
1747
+ """Call method |IOSequence.load_series| of all directly handled
1748
+ |FactorSequence| objects."""
1749
+ self.sequences.factors.load_series()
1750
+
1751
+ def load_fluxseries(self) -> None:
1752
+ """Call method |IOSequence.load_series| of all directly handled |FluxSequence|
1753
+ objects."""
1754
+ self.sequences.fluxes.load_series()
1755
+
1756
+ def load_stateseries(self) -> None:
1757
+ """Call method |IOSequence.load_series| of all directly handled |StateSequence|
1758
+ objects."""
1759
+ self.sequences.states.load_series()
1760
+
1761
+ def save_allseries(self) -> None:
1762
+ """Call method |Model.save_inputseries|, |Model.save_factorseries|,
1763
+ |Model.save_fluxseries|, and |Model.save_stateseries|."""
1764
+ self.save_inputseries()
1765
+ self.save_factorseries()
1766
+ self.save_fluxseries()
1767
+ self.save_stateseries()
1768
+
1769
+ def save_inputseries(self) -> None:
1770
+ """Call method |IOSequence.save_series| of all directly handled |InputSequence|
1771
+ objects."""
1772
+ self.sequences.inputs.save_series()
1773
+
1774
+ def save_factorseries(self) -> None:
1775
+ """Call method |IOSequence.save_series| of all directly handled
1776
+ |FactorSequence| objects."""
1777
+ self.sequences.factors.save_series()
1778
+
1779
+ def save_fluxseries(self) -> None:
1780
+ """Call method |IOSequence.save_series| of all directly handled |FluxSequence|
1781
+ objects."""
1782
+ self.sequences.fluxes.save_series()
1783
+
1784
+ def save_stateseries(self) -> None:
1785
+ """Call method |IOSequence.save_series| of all directly handled |StateSequence|
1786
+ objects."""
1787
+ self.sequences.states.save_series()
1788
+
1789
+ def get_controlfileheader(
1790
+ self,
1791
+ import_submodels: bool = True,
1792
+ parameterstep: timetools.PeriodConstrArg | None = None,
1793
+ simulationstep: timetools.PeriodConstrArg | None = None,
1794
+ ) -> str:
1795
+ """Return the header of a parameter control file.
1796
+
1797
+ The header contains the default coding information, the model import commands
1798
+ and the actual parameter and simulation step sizes:
1799
+
1800
+ >>> from hydpy import prepare_model, pub
1801
+ >>> model = prepare_model("hland_96")
1802
+ >>> model.aetmodel = prepare_model("evap_aet_hbv96")
1803
+ >>> pub.timegrids = "2000.01.01", "2001.01.01", "1h"
1804
+ >>> print(model.get_controlfileheader())
1805
+ from hydpy.models.hland_96 import *
1806
+ from hydpy.models import evap_aet_hbv96
1807
+ <BLANKLINE>
1808
+ simulationstep("1h")
1809
+ parameterstep("1d")
1810
+ <BLANKLINE>
1811
+ <BLANKLINE>
1812
+
1813
+ Optionally, you can omit the submodel import lines and define alternative
1814
+ parameter step and simulation step sizes:
1815
+
1816
+ >>> print(model.get_controlfileheader(
1817
+ ... import_submodels=False, parameterstep="2d", simulationstep="3d"))
1818
+ from hydpy.models.hland_96 import *
1819
+ <BLANKLINE>
1820
+ simulationstep("3d")
1821
+ parameterstep("2d")
1822
+ <BLANKLINE>
1823
+ <BLANKLINE>
1824
+
1825
+ .. testsetup::
1826
+
1827
+ >>> del pub.timegrids
1828
+ """
1829
+ lines = [f"from hydpy.models.{self} import *"]
1830
+ if import_submodels:
1831
+ names = []
1832
+ for submodel in self.find_submodels().values():
1833
+ if (name := submodel.name) not in names:
1834
+ names.append(name)
1835
+ for name in sorted(names):
1836
+ lines.append(f"from hydpy.models import {name}")
1837
+ options = hydpy.pub.options
1838
+ with options.parameterstep(parameterstep):
1839
+ if simulationstep is None:
1840
+ simulationstep = options.simulationstep
1841
+ else:
1842
+ simulationstep = timetools.Period(simulationstep)
1843
+ lines.append(f'\nsimulationstep("{simulationstep}")')
1844
+ lines.append(f'parameterstep("{options.parameterstep}")\n\n')
1845
+ return "\n".join(lines)
1846
+
1847
+ def _get_controllines(
1848
+ self,
1849
+ *,
1850
+ parameterstep: timetools.PeriodConstrArg | None = None,
1851
+ simulationstep: timetools.PeriodConstrArg | None = None,
1852
+ auxfiler: auxfiletools.Auxfiler | None = None,
1853
+ sublevel: int = 0,
1854
+ ignore: tuple[type[parametertools.Parameter], ...] | None = None,
1855
+ ) -> list[str]:
1856
+ parameter2auxfile = None if auxfiler is None else auxfiler.get(self)
1857
+ lines = []
1858
+ opts = hydpy.pub.options
1859
+ with opts.parameterstep(parameterstep), opts.simulationstep(simulationstep):
1860
+ for par in self.parameters.control:
1861
+ if (ignore is None) or not isinstance(par, ignore):
1862
+ if parameter2auxfile is not None:
1863
+ auxfilename = parameter2auxfile.get_filename(par)
1864
+ if auxfilename:
1865
+ lines.append(f'{par.name}(auxfile="{auxfilename}")')
1866
+ continue
1867
+ lines.extend(repr(par).split("\n"))
1868
+ solver_lines = tuple(
1869
+ f"solver.{repr(par)}"
1870
+ for par in self.parameters.solver
1871
+ if exceptiontools.attrready(par, "alternative_initvalue")
1872
+ )
1873
+ if solver_lines:
1874
+ lines.append("")
1875
+ lines.extend(solver_lines)
1876
+ indent = sublevel * " "
1877
+ return [f"{indent}{line}\n" for line in lines]
1878
+
1879
+ def save_controls(
1880
+ self,
1881
+ parameterstep: timetools.PeriodConstrArg | None = None,
1882
+ simulationstep: timetools.PeriodConstrArg | None = None,
1883
+ auxfiler: auxfiletools.Auxfiler | None = None,
1884
+ filepath: str | None = None,
1885
+ ) -> None:
1886
+ """Write the control parameters (and eventually some solver parameters) to a
1887
+ control file.
1888
+
1889
+ Usually, a control file consists of a header (see the documentation on the
1890
+ method |Model.get_controlfileheader|) and the string representations of the
1891
+ individual |Parameter| objects handled by the `control` |SubParameters| object.
1892
+
1893
+ The main functionality of method |Model.save_controls| is demonstrated in the
1894
+ documentation on method |HydPy.save_controls| of class |HydPy|, which one
1895
+ should apply to write the parameter information of complete *HydPy* projects.
1896
+ However, calling |Model.save_controls| on individual |Model| objects offers the
1897
+ advantage of choosing an arbitrary file path, as shown in the following
1898
+ example:
1899
+
1900
+ >>> from hydpy.models.test_stiff1d import *
1901
+ >>> parameterstep("1d")
1902
+ >>> simulationstep("1h")
1903
+ >>> k(0.1)
1904
+ >>> n(3)
1905
+
1906
+ >>> from hydpy import Open
1907
+ >>> with Open():
1908
+ ... model.save_controls(filepath="otherdir/otherfile.py")
1909
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1910
+ otherdir/otherfile.py
1911
+ ---------------------------------------
1912
+ from hydpy.models.test_stiff1d import *
1913
+ <BLANKLINE>
1914
+ simulationstep("1h")
1915
+ parameterstep("1d")
1916
+ <BLANKLINE>
1917
+ k(0.1)
1918
+ n(3)
1919
+ <BLANKLINE>
1920
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1921
+
1922
+ Method |Model.save_controls| also writes the string representations of all
1923
+ |SolverParameter| objects with non-default values into the control file:
1924
+
1925
+ >>> solver.abserrormax(1e-6)
1926
+ >>> with Open():
1927
+ ... model.save_controls(filepath="otherdir/otherfile.py")
1928
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1929
+ otherdir/otherfile.py
1930
+ ---------------------------------------
1931
+ from hydpy.models.test_stiff1d import *
1932
+ <BLANKLINE>
1933
+ simulationstep("1h")
1934
+ parameterstep("1d")
1935
+ <BLANKLINE>
1936
+ k(0.1)
1937
+ n(3)
1938
+ <BLANKLINE>
1939
+ solver.abserrormax(0.000001)
1940
+ <BLANKLINE>
1941
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1942
+
1943
+ Without a given file path and a proper project configuration, method
1944
+ |Model.save_controls| raises the following error:
1945
+
1946
+ >>> model.save_controls()
1947
+ Traceback (most recent call last):
1948
+ ...
1949
+ RuntimeError: To save the control parameters of a model to a file, its \
1950
+ filename must be known. This can be done, by passing a filename to function \
1951
+ `save_controls` directly. But in complete HydPy applications, it is usally assumed \
1952
+ to be consistent with the name of the element handling the model.
1953
+
1954
+ Submodels like |meteo_glob_fao56| allow using their instances by multiple main
1955
+ models. We prepare such a case by selecting such an instance as the submodel
1956
+ of the absolute main model |lland_knauf| and the the relative submodel
1957
+ |evap_aet_morsim|:
1958
+
1959
+ >>> from hydpy.core.importtools import reverse_model_wildcard_import
1960
+ >>> reverse_model_wildcard_import()
1961
+
1962
+ >>> from hydpy import pub
1963
+ >>> pub.timegrids = "2000-01-01", "2001-01-02", "1d"
1964
+ >>> from hydpy.models.lland_knauf import *
1965
+ >>> parameterstep()
1966
+ >>> nhru(1)
1967
+ >>> ft(1.0)
1968
+ >>> fhru(1.0)
1969
+ >>> lnk(ACKER)
1970
+ >>> measuringheightwindspeed(10.0)
1971
+ >>> lai(3.0)
1972
+ >>> wmax(300.0)
1973
+ >>> with model.add_radiationmodel_v1("meteo_glob_fao56") as meteo_glob_fao56:
1974
+ ... latitude(50.0)
1975
+ >>> with model.add_aetmodel_v1("evap_aet_morsim"):
1976
+ ... measuringheightwindspeed(2.0)
1977
+ ... model.add_radiationmodel_v1(meteo_glob_fao56)
1978
+
1979
+ To avoid name collisions, |Model.save_controls| prefixes the string `submodel_`
1980
+ to the submodel name (which is identical to the submodel module's name) to
1981
+ create the name of the variable that references the shared model's instance:
1982
+
1983
+ >>> with Open(): # doctest: +ELLIPSIS
1984
+ ... model.save_controls(filepath="otherdir/otherfile.py")
1985
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~...
1986
+ otherdir/otherfile.py
1987
+ ----------------------------------------------------------------------------...
1988
+ from hydpy.models.lland_knauf import *
1989
+ from hydpy.models import evap_aet_morsim
1990
+ from hydpy.models import meteo_glob_fao56
1991
+ ...
1992
+ simulationstep("1d")
1993
+ parameterstep("1d")
1994
+ ...
1995
+ ft(1.0)
1996
+ ...
1997
+ measuringheightwindspeed(10.0)
1998
+ ...
1999
+ with model.add_aetmodel_v1(evap_aet_morsim):
2000
+ measuringheightwindspeed(2.0)
2001
+ ...
2002
+ with model.add_radiationmodel_v1(meteo_glob_fao56) as \
2003
+ submodel_meteo_glob_fao56:
2004
+ latitude(50.0)
2005
+ ...
2006
+ model.add_radiationmodel_v1(submodel_meteo_glob_fao56)
2007
+ ...
2008
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~...
2009
+ """
2010
+
2011
+ def _extend_lines_submodel(
2012
+ model: Model, sublevel: int, preparemethods: set[str]
2013
+ ) -> None:
2014
+ def _find_adder_and_position() -> (
2015
+ tuple[importtools.SubmodelAdder, str | None]
2016
+ ):
2017
+ mt2sn2as = importtools.SubmodelAdder.__hydpy_maintype2subname2adders__
2018
+ subname, position = name.rpartition(".")[2], None
2019
+ for modeltype in inspect.getmro(type(model)):
2020
+ if (name2adders := mt2sn2as.get(modeltype)) is not None:
2021
+ if subname.rsplit("_")[-1].isnumeric():
2022
+ subname, position = subname.rsplit("_")
2023
+ for adder in name2adders[subname]:
2024
+ if isinstance(submodel, adder.submodelinterface):
2025
+ return adder, position
2026
+ assert False
2027
+
2028
+ sublevel += 1
2029
+ for name, submodel in model.find_submodels(
2030
+ include_subsubmodels=False, repeat_sharedmodels=True
2031
+ ).items():
2032
+ adder, position = _find_adder_and_position()
2033
+ importtools.TargetParameterUpdater.testmode = True
2034
+ try:
2035
+ if position is None:
2036
+ adder.update(model, submodel, refresh=False)
2037
+ else:
2038
+ adder.update(
2039
+ model, submodel, position=int(position), refresh=False
2040
+ )
2041
+ finally:
2042
+ importtools.TargetParameterUpdater.testmode = False
2043
+ position = "" if position is None else f", position={position}"
2044
+ addername = adder.get_wrapped().__name__
2045
+ indent = (sublevel - 1) * " "
2046
+ if submodel in visited_shared_submodels:
2047
+ lines.append(
2048
+ f"{indent}model.{addername}(submodel_{submodel}{position})\n"
2049
+ )
2050
+ else:
2051
+ line = f"{indent}with model.{addername}({submodel}{position})"
2052
+ if submodel in shared_submodels:
2053
+ assert isinstance(submodel, SharableSubmodelInterface)
2054
+ visited_shared_submodels.add(submodel)
2055
+ line = f"{line} as submodel_{submodel}:\n"
2056
+ else:
2057
+ line = f"{line}:\n"
2058
+ lines.append(line)
2059
+ preparemethods_ = preparemethods.copy()
2060
+ for method in adder.methods:
2061
+ preparemethods_.add(method.__name__)
2062
+ targetparameters = set()
2063
+ for methodname in preparemethods_:
2064
+ updater = getattr(submodel, methodname, None)
2065
+ if (
2066
+ isinstance(updater, importtools.TargetParameterUpdater)
2067
+ and ((old := updater.values_orig.get(submodel)) is not None)
2068
+ and ((new := updater.values_test.get(submodel)) is not None)
2069
+ and objecttools.is_equal(old, new)
2070
+ ):
2071
+ targetparameters.add(updater.targetparameter)
2072
+ submodellines = (
2073
+ submodel._get_controllines( # pylint: disable=protected-access
2074
+ parameterstep=parameterstep,
2075
+ simulationstep=simulationstep,
2076
+ auxfiler=auxfiler,
2077
+ sublevel=sublevel,
2078
+ ignore=tuple(targetparameters),
2079
+ )
2080
+ )
2081
+ if submodellines:
2082
+ lines.extend(submodellines)
2083
+ else:
2084
+ lines.append(f"{sublevel * ' '}pass\n") # pragma: no cover
2085
+ _extend_lines_submodel(
2086
+ model=submodel,
2087
+ sublevel=sublevel,
2088
+ preparemethods=preparemethods_,
2089
+ )
2090
+
2091
+ header = self.get_controlfileheader(
2092
+ import_submodels=True,
2093
+ parameterstep=parameterstep,
2094
+ simulationstep=simulationstep,
2095
+ )
2096
+ lines = [header]
2097
+ lines.extend(
2098
+ self._get_controllines(
2099
+ parameterstep=parameterstep,
2100
+ simulationstep=simulationstep,
2101
+ auxfiler=auxfiler,
2102
+ sublevel=0,
2103
+ )
2104
+ )
2105
+
2106
+ submodels = tuple(self.find_submodels(repeat_sharedmodels=True).values())
2107
+ sharable_submodels = {
2108
+ m for m in submodels if isinstance(m, SharableSubmodelInterface)
2109
+ }
2110
+ shared_submodels = {m for m in sharable_submodels if submodels.count(m) > 1}
2111
+ visited_shared_submodels: set[SharableSubmodelInterface] = set()
2112
+
2113
+ # ToDo: needs refactoring
2114
+ for submodel in self.find_submodels().values():
2115
+ submodel.preparemethod2arguments.clear()
2116
+ try:
2117
+ _extend_lines_submodel(model=self, sublevel=0, preparemethods=set())
2118
+ finally:
2119
+ for submodel in self.find_submodels().values():
2120
+ submodel.preparemethod2arguments.clear()
2121
+
2122
+ text = "".join(lines)
2123
+
2124
+ if filepath:
2125
+ with open(filepath, mode="w", encoding="utf-8") as controlfile:
2126
+ controlfile.write(text)
2127
+ else:
2128
+ filename = objecttools.devicename(self)
2129
+ if filename == "?":
2130
+ raise RuntimeError(
2131
+ "To save the control parameters of a model to a file, its "
2132
+ "filename must be known. This can be done, by passing a filename "
2133
+ "to function `save_controls` directly. But in complete HydPy "
2134
+ "applications, it is usally assumed to be consistent with the "
2135
+ "name of the element handling the model."
2136
+ )
2137
+ hydpy.pub.controlmanager.save_file(filename, text)
2138
+
2139
+ @contextlib.contextmanager
2140
+ def define_conditions(
2141
+ self, module: types.ModuleType | str | None = None
2142
+ ) -> Generator[None, None, None]:
2143
+ """Allow defining the values of condition sequences in condition files
2144
+ conveniently.
2145
+
2146
+ |Model.define_conditions| works similar to the "add_submodel" methods wrapped
2147
+ by instances of class |SubmodelAdder| but is much simpler. In combination with
2148
+ the `with` statement, it makes the all relevant state and log sequences
2149
+ temporarily directly available:
2150
+
2151
+ >>> from hydpy import pub
2152
+ >>> pub.timegrids = "2000-01-01", "2001-01-01", "6h"
2153
+ >>> from hydpy.models.lland_knauf import *
2154
+ >>> parameterstep()
2155
+ >>> nhru(2)
2156
+ >>> ft(10.0)
2157
+ >>> fhru(0.2, 0.8)
2158
+ >>> lnk(ACKER, MISCHW)
2159
+ >>> wmax(acker=100.0, mischw=200.0)
2160
+ >>> measuringheightwindspeed(10.0)
2161
+ >>> with model.add_aetmodel_v1("evap_aet_morsim"):
2162
+ ... pass
2163
+ >>> with model.aetmodel.define_conditions():
2164
+ ... loggedwindspeed2m(1.0, 3.0, 2.0, 4.0)
2165
+ >>> loggedwindspeed2m
2166
+ Traceback (most recent call last):
2167
+ ...
2168
+ NameError: name 'loggedwindspeed2m' is not defined
2169
+ >>> model.aetmodel.sequences.logs.loggedwindspeed2m
2170
+ loggedwindspeed2m(1.0, 3.0, 2.0, 4.0)
2171
+
2172
+ One can pass the submodel's module or name for documentation purposes:
2173
+
2174
+ >>> with model.aetmodel.define_conditions("evap_aet_morsim"):
2175
+ ... loggedwindspeed2m(4.0, 2.0, 3.0, 1.0)
2176
+ >>> loggedwindspeed2m
2177
+ Traceback (most recent call last):
2178
+ ...
2179
+ NameError: name 'loggedwindspeed2m' is not defined
2180
+ >>> model.aetmodel.sequences.logs.loggedwindspeed2m
2181
+ loggedwindspeed2m(4.0, 2.0, 3.0, 1.0)
2182
+
2183
+ For misleading input, |Model.define_conditions| raises the following error:
2184
+
2185
+ >>> from hydpy.models import evap_aet_hbv96
2186
+ >>> with model.aetmodel.define_conditions(evap_aet_hbv96):
2187
+ ... loggedwindspeed2m(1.0, 3.0, 2.0, 4.0)
2188
+ Traceback (most recent call last):
2189
+ ...
2190
+ TypeError: While trying to define the conditions of (sub)model \
2191
+ `evap_aet_morsim`, the following error occurred: (Sub)model `evap_aet_morsim` is not \
2192
+ of type `evap_aet_hbv96`.
2193
+ >>> loggedwindspeed2m
2194
+ Traceback (most recent call last):
2195
+ ...
2196
+ NameError: name 'loggedwindspeed2m' is not defined
2197
+ >>> model.aetmodel.sequences.logs.loggedwindspeed2m
2198
+ loggedwindspeed2m(4.0, 2.0, 3.0, 1.0)
2199
+
2200
+ .. testsetup::
2201
+
2202
+ >>> del pub.timegrids
2203
+ """
2204
+ try:
2205
+ if module is not None:
2206
+ module = importtools.load_modelmodule(module)
2207
+ if self.__module__ != module.__name__:
2208
+ raise TypeError(
2209
+ f"(Sub)model `{self.name}` is not of type "
2210
+ f"`{module.__name__.rpartition('.')[2]}`."
2211
+ )
2212
+ assert (
2213
+ ((frame1 := inspect.currentframe()) is not None)
2214
+ and ((frame2 := frame1.f_back) is not None)
2215
+ and ((frame3 := frame2.f_back) is not None)
2216
+ )
2217
+ namespace = frame3.f_locals
2218
+ old_locals = namespace.get(importtools.__HYDPY_MODEL_LOCALS__, {})
2219
+ new_locals = {}
2220
+ for seq in self.sequences.conditionsequences:
2221
+ new_locals[seq.name] = seq
2222
+ try:
2223
+ namespace[importtools.__HYDPY_MODEL_LOCALS__] = new_locals
2224
+ namespace.update(new_locals)
2225
+ yield
2226
+ finally:
2227
+ for name in new_locals:
2228
+ namespace.pop(name, None)
2229
+ namespace.update(old_locals)
2230
+ namespace[importtools.__HYDPY_MODEL_LOCALS__] = old_locals
2231
+ except BaseException:
2232
+ objecttools.augment_excmessage(
2233
+ f"While trying to define the conditions of (sub)model `{self.name}`"
2234
+ )
2235
+
2236
+ def __prepare_conditionfilename(self, filename: str | None) -> str:
2237
+ if filename is None:
2238
+ filename = objecttools.devicename(self)
2239
+ if filename == "?":
2240
+ raise RuntimeError(
2241
+ "To load or save the conditions of a model from or to a file, its "
2242
+ "filename must be known. This can be done, by passing filename "
2243
+ "to method `load_conditions` or `save_conditions` directly. But "
2244
+ "in complete HydPy applications, it is usally assumed to be "
2245
+ "consistent with the name of the element handling the model. "
2246
+ "Actually, neither a filename is given nor does the model know "
2247
+ "its master element."
2248
+ )
2249
+ if not filename.endswith(".py"):
2250
+ filename += ".py"
2251
+ return filename
2252
+
2253
+ def load_conditions(self, filename: str | None = None) -> None:
2254
+ """Read the initial conditions from a file and assign them to the respective
2255
+ |StateSequence| and |LogSequence| objects.
2256
+
2257
+ The documentation on method |HydPy.load_conditions| of class |HydPy| explains
2258
+ how to read and write condition values for complete *HydPy* projects in the
2259
+ most convenient manner. However, using the underlying methods
2260
+ |Model.load_conditions| and |Model.save_conditions| directly offers the
2261
+ advantage of specifying alternative filenames. We demonstrate this by using
2262
+ the state sequence |hland_states.SM| if the `land_dill_assl` |Element| object
2263
+ of the `HydPy-H-Lahn` example project:
2264
+
2265
+ >>> from hydpy.core.testtools import prepare_full_example_2
2266
+ >>> hp, pub, TestIO = prepare_full_example_2()
2267
+ >>> dill_assl = hp.elements.land_dill_assl.model
2268
+ >>> dill_assl.sequences.states.sm
2269
+ sm(185.13164, 181.18755, 199.80432, 196.55888, 212.04018, 209.48859,
2270
+ 222.12115, 220.12671, 230.30756, 228.70779, 236.91943, 235.64427)
2271
+
2272
+ We work in the freshly created condition directory `test`:
2273
+
2274
+ >>> with TestIO():
2275
+ ... pub.conditionmanager.currentdir = "test"
2276
+
2277
+ We set all soil moisture values to zero and write the updated values to file
2278
+ `cold_start.py`:
2279
+
2280
+ >>> dill_assl.sequences.states.sm(0.0)
2281
+ >>> with TestIO():
2282
+ ... dill_assl.save_conditions("cold_start.py")
2283
+
2284
+ Trying to reload from the written file (after changing the soil moisture values
2285
+ again) without passing the file name fails due to the wrong assumption that the
2286
+ element's name serves as the file name base:
2287
+
2288
+ >>> dill_assl.sequences.states.sm(100.0)
2289
+ >>> with TestIO(): # doctest: +ELLIPSIS
2290
+ ... dill_assl.load_conditions()
2291
+ Traceback (most recent call last):
2292
+ ...
2293
+ FileNotFoundError: While trying to load the initial conditions of element \
2294
+ `land_dill_assl`, the following error occurred: [Errno 2] No such file or directory: \
2295
+ '...land_dill_assl.py'
2296
+
2297
+ One does not need to explicitly state the file extensions (`.py`):
2298
+
2299
+ >>> with TestIO():
2300
+ ... dill_assl.load_conditions("cold_start")
2301
+ >>> dill_assl.sequences.states.sm
2302
+ sm(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
2303
+
2304
+ Automatically determining the file name requires a proper reference to the
2305
+ related |Element| object:
2306
+
2307
+ >>> del dill_assl.element
2308
+ >>> with TestIO():
2309
+ ... dill_assl.save_conditions()
2310
+ Traceback (most recent call last):
2311
+ ...
2312
+ RuntimeError: While trying to save the actual conditions of element `?`, the \
2313
+ following error occurred: To load or save the conditions of a model from or to a file, \
2314
+ its filename must be known. This can be done, by passing filename to method \
2315
+ `load_conditions` or `save_conditions` directly. But in complete HydPy applications, \
2316
+ it is usally assumed to be consistent with the name of the element handling the \
2317
+ model. Actually, neither a filename is given nor does the model know its master \
2318
+ element.
2319
+
2320
+ The submodels selected in the `HydPy-H-Lahn` example project do not require any
2321
+ condition sequences. Hence, we replace the combination of |evap_aet_hbv96| and
2322
+ |evap_pet_hbv96| with a plain |evap_aet_morsim| instance, which relies on some
2323
+ log sequences:
2324
+
2325
+ >>> with dill_assl.add_aetmodel_v1("evap_aet_morsim"):
2326
+ ... pass
2327
+
2328
+ The following code demonstrates that reading and writing of condition sequences
2329
+ also works for submodels:
2330
+
2331
+ >>> logs = dill_assl.aetmodel.sequences.logs
2332
+ >>> logs.loggedairtemperature = 20.0
2333
+ >>> logs.loggedwindspeed2m = 2.0
2334
+ >>> with TestIO(): # doctest: +ELLIPSIS
2335
+ ... dill_assl.save_conditions("submodel_conditions.py")
2336
+ >>> logs.loggedairtemperature = 10.0
2337
+ >>> logs.loggedwindspeed2m = 1.0
2338
+ >>> with TestIO(): # doctest: +ELLIPSIS
2339
+ ... dill_assl.load_conditions("submodel_conditions.py")
2340
+ >>> logs.loggedairtemperature
2341
+ loggedairtemperature(20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0,
2342
+ 20.0, 20.0, 20.0, 20.0)
2343
+ >>> logs.loggedwindspeed2m
2344
+ loggedwindspeed2m(2.0)
2345
+
2346
+ Method |Model.save_conditions| writes lines that use function |controlcheck|.
2347
+ It, therefore, must know the control directory related to the written
2348
+ conditions, for which it relies on the |FileManager.currentdir| property of the
2349
+ control manager instance of module |pub|. So, make sure this property points
2350
+ to the correct directory. Otherwise, errors like the following might occur:
2351
+
2352
+ >>> with TestIO(): # doctest: +ELLIPSIS
2353
+ ... del pub.controlmanager.currentdir
2354
+ ... pub.controlmanager.currentdir = "calib_1"
2355
+ ... pub.controlmanager.currentdir = "calib_2"
2356
+ ... pub.controlmanager.currentdir = None
2357
+ ... dill_assl.save_conditions("submodel_conditions.py")
2358
+ Traceback (most recent call last):
2359
+ ...
2360
+ RuntimeError: While trying to save the actual conditions of element `?`, the \
2361
+ following error occurred: While trying to determine the related control file \
2362
+ directory for configuring the `controlcheck` function, the following error occurred: \
2363
+ The current working directory of the control manager has not been defined manually \
2364
+ and cannot be determined automatically: The default directory (default) is not among \
2365
+ the available directories (calib_1 and calib_2).
2366
+
2367
+ .. testsetup::
2368
+
2369
+ >>> from hydpy import Element, Node, pub
2370
+ >>> Element.clear_all()
2371
+ >>> Node.clear_all()
2372
+ >>> del pub.timegrids
2373
+ """
2374
+ hasconditions = any(
2375
+ model.sequences.states or model.sequences.logs
2376
+ for model in self.find_submodels(include_mainmodel=True).values()
2377
+ )
2378
+ if hasconditions:
2379
+ try:
2380
+ dict_ = locals()
2381
+ for seq in self.sequences.conditionsequences:
2382
+ dict_[seq.name] = seq
2383
+ dict_["model"] = self
2384
+ filepath = os.path.join(
2385
+ hydpy.pub.conditionmanager.inputpath,
2386
+ self.__prepare_conditionfilename(filename),
2387
+ )
2388
+ with hydpy.pub.options.trimvariables(False):
2389
+ runpy.run_path(filepath, init_globals=dict_)
2390
+ for model in self.find_submodels(include_mainmodel=True).values():
2391
+ for seq in reversed(tuple(model.sequences.conditionsequences)):
2392
+ seq.trim()
2393
+ except BaseException:
2394
+ objecttools.augment_excmessage(
2395
+ f"While trying to load the initial conditions of element "
2396
+ f"`{objecttools.devicename(self)}`"
2397
+ )
2398
+
2399
+ def save_conditions(self, filename: str | None = None) -> None:
2400
+ """Query the actual conditions of the |StateSequence| and |LogSequence| objects
2401
+ and write them into an initial condition file.
2402
+
2403
+ See the documentation on method |Model.load_conditions| for further
2404
+ information.
2405
+ """
2406
+ try:
2407
+ model2hasconditions = {}
2408
+ for model in self.find_submodels(include_mainmodel=True).values():
2409
+ seqs = model.sequences
2410
+ model2hasconditions[model] = seqs.states or seqs.logs
2411
+ if any(model2hasconditions.values()):
2412
+ con = hydpy.pub.controlmanager
2413
+ lines = [f"from hydpy.models.{self} import *\n"]
2414
+ submodelnames = set()
2415
+ for model, hasconditions in model2hasconditions.items():
2416
+ if hasconditions and (model is not self):
2417
+ submodelnames.add(model.name)
2418
+ for submodelname in sorted(submodelnames):
2419
+ lines.append(f"from hydpy.models import {submodelname}\n")
2420
+ try:
2421
+ controldir = con.currentdir
2422
+ except BaseException:
2423
+ objecttools.augment_excmessage(
2424
+ "While trying to determine the related control file directory "
2425
+ "for configuring the `controlcheck` function"
2426
+ )
2427
+ lines.append(
2428
+ f'\ncontrolcheck(projectdir=r"{con.projectdir}", '
2429
+ f'controldir="{controldir}", '
2430
+ f'stepsize="{hydpy.pub.timegrids.stepsize}")\n\n'
2431
+ )
2432
+ for seq in self.sequences.conditionsequences:
2433
+ lines.append(f"{repr(seq)}\n")
2434
+ for fullname, model in self.find_submodels().items():
2435
+ if model2hasconditions[model]:
2436
+ if fullname.rsplit("_")[-1].isnumeric():
2437
+ prefix, _, position = fullname.rpartition("_")
2438
+ fullname = f"{prefix}[{position}]"
2439
+ lines.append(f"with {fullname}.define_conditions({model}):\n")
2440
+ for seq in model.sequences.conditionsequences:
2441
+ lines.append(f" {repr(seq)}\n")
2442
+ filepath = os.path.join(
2443
+ hydpy.pub.conditionmanager.outputpath,
2444
+ self.__prepare_conditionfilename(filename),
2445
+ )
2446
+ with open(filepath, "w", encoding="utf-8") as file_:
2447
+ file_.writelines(lines)
2448
+ except BaseException:
2449
+ objecttools.augment_excmessage(
2450
+ f"While trying to save the actual conditions of element "
2451
+ f"`{objecttools.devicename(self)}`"
2452
+ )
2453
+
2454
+ def trim_conditions(self) -> None:
2455
+ """Call method |Sequences.trim_conditions| of the handled |Sequences| object."""
2456
+ for model in self.find_submodels(include_mainmodel=True).values():
2457
+ model.sequences.trim_conditions()
2458
+
2459
+ def reset_conditions(self) -> None:
2460
+ """Call method |Sequences.reset| of the handled |Sequences| object."""
2461
+ for model in self.find_submodels(include_mainmodel=True).values():
2462
+ model.sequences.reset()
2463
+
2464
+ @abc.abstractmethod
2465
+ def simulate(self, idx: int) -> None:
2466
+ """Perform a simulation run over a single simulation time step."""
2467
+
2468
+ def reset_reuseflags(self) -> None:
2469
+ """Reset all |ReusableMethod.REUSEMARKER| attributes of the current model
2470
+ instance and its submodels (usually at the beginning of a simulation step).
2471
+
2472
+ When working in Cython mode, the standard model import overrides this generic
2473
+ Python version with a model-specific Cython version.
2474
+ """
2475
+ for method in self.REUSABLE_METHODS:
2476
+ setattr(self, method.REUSEMARKER, False)
2477
+ for submodel in self.find_submodels(include_subsubmodels=False).values():
2478
+ submodel.reset_reuseflags()
2479
+
2480
+ def load_data(self, idx: int) -> None:
2481
+ """Call method |Sequences.load_data| of the attribute `sequences` of the
2482
+ current model instance and its submodels.
2483
+
2484
+ When working in Cython mode, the standard model import overrides this generic
2485
+ Python version with a model-specific Cython version.
2486
+ """
2487
+ self.idx_sim = idx
2488
+ if self.sequences:
2489
+ self.sequences.load_data(idx)
2490
+ for submodel in self.find_submodels(include_subsubmodels=False).values():
2491
+ submodel.load_data(idx)
2492
+
2493
+ def save_data(self, idx: int) -> None:
2494
+ """Call method |Sequences.save_data| of the attribute `sequences` of the
2495
+ current model instance and its submodels.
2496
+
2497
+ When working in Cython mode, the standard model import overrides this generic
2498
+ Python version with a model-specific Cython version.
2499
+ """
2500
+ self.idx_sim = idx
2501
+ if self.sequences:
2502
+ self.sequences.save_data(idx)
2503
+ for submodel in self.find_submodels(include_subsubmodels=False).values():
2504
+ submodel.save_data(idx)
2505
+
2506
+ def update_inlets(self) -> None:
2507
+ """Call all methods defined as "INLET_METHODS" in the defined order.
2508
+
2509
+ >>> from hydpy.core.modeltools import AdHocModel, Method
2510
+ >>> class print_1(Method):
2511
+ ... @staticmethod
2512
+ ... def __call__(self):
2513
+ ... print(1)
2514
+ >>> class print_2(Method):
2515
+ ... @staticmethod
2516
+ ... def __call__(self):
2517
+ ... print(2)
2518
+ >>> class Test(AdHocModel):
2519
+ ... INLET_METHODS = print_1, print_2
2520
+ >>> Test().update_inlets()
2521
+ 1
2522
+ 2
2523
+
2524
+ When working in Cython mode, the standard model import overrides this generic
2525
+ Python version with a model-specific Cython version.
2526
+ """
2527
+ for method in self.INLET_METHODS:
2528
+ method.__call__(self) # pylint: disable=unnecessary-dunder-call
2529
+
2530
+ def update_outlets(self) -> None:
2531
+ """Call all methods defined as "OUTLET_METHODS" in the defined order.
2532
+
2533
+ >>> from hydpy.core.modeltools import AdHocModel, Method
2534
+ >>> class print_1(Method):
2535
+ ... @staticmethod
2536
+ ... def __call__(self):
2537
+ ... print(1)
2538
+ >>> class print_2(Method):
2539
+ ... @staticmethod
2540
+ ... def __call__(self):
2541
+ ... print(2)
2542
+ >>> class Test(AdHocModel):
2543
+ ... OUTLET_METHODS = print_1, print_2
2544
+ >>> Test().update_outlets()
2545
+ 1
2546
+ 2
2547
+
2548
+ When working in Cython mode, the standard model import overrides this generic
2549
+ Python version with a model-specific Cython version.
2550
+ """
2551
+ for method in self.OUTLET_METHODS:
2552
+ method.__call__(self) # pylint: disable=unnecessary-dunder-call
2553
+
2554
+ def update_receivers(self, idx: int) -> None:
2555
+ """Call all methods defined as "RECEIVER_METHODS" in the defined order.
2556
+
2557
+ >>> from hydpy.core.modeltools import AdHocModel, Method
2558
+ >>> class print_1(Method):
2559
+ ... @staticmethod
2560
+ ... def __call__(self):
2561
+ ... print(test.idx_sim+1)
2562
+ >>> class print_2(Method):
2563
+ ... @staticmethod
2564
+ ... def __call__(self):
2565
+ ... print(test.idx_sim+2)
2566
+ >>> class Test(AdHocModel):
2567
+ ... RECEIVER_METHODS = print_1, print_2
2568
+ >>> test = Test()
2569
+ >>> test.update_receivers(1)
2570
+ 2
2571
+ 3
2572
+
2573
+ When working in Cython mode, the standard model import overrides this generic
2574
+ Python version with a model-specific Cython version.
2575
+ """
2576
+ self.idx_sim = idx
2577
+ for method in self.RECEIVER_METHODS:
2578
+ method.__call__(self) # pylint: disable=unnecessary-dunder-call
2579
+
2580
+ def update_senders(self, idx: int) -> None:
2581
+ """Call all methods defined as "SENDER_METHODS" in the defined order.
2582
+
2583
+ >>> from hydpy.core.modeltools import AdHocModel, Method
2584
+ >>> class print_1(Method):
2585
+ ... @staticmethod
2586
+ ... def __call__(self):
2587
+ ... print(test.idx_sim+1)
2588
+ >>> class print_2(Method):
2589
+ ... @staticmethod
2590
+ ... def __call__(self):
2591
+ ... print(test.idx_sim+2)
2592
+ >>> class Test(AdHocModel):
2593
+ ... SENDER_METHODS = print_1, print_2
2594
+ >>> test = Test()
2595
+ >>> test.update_senders(1)
2596
+ 2
2597
+ 3
2598
+
2599
+ When working in Cython mode, the standard model import overrides this generic
2600
+ Python version with a model-specific Cython version.
2601
+ """
2602
+ self.idx_sim = idx
2603
+ for method in self.SENDER_METHODS:
2604
+ method.__call__(self) # pylint: disable=unnecessary-dunder-call
2605
+
2606
+ def new2old(self) -> None:
2607
+ """Call method |StateSequences.new2old| of subattribute `sequences.states`.
2608
+
2609
+ When working in Cython mode, the standard model import overrides this generic
2610
+ Python version with a model-specific Cython version.
2611
+ """
2612
+ if self.sequences:
2613
+ self.sequences.states.new2old()
2614
+ for submodel in self.find_submodels(include_subsubmodels=False).values():
2615
+ if submodel.sequences:
2616
+ submodel.new2old()
2617
+
2618
+ def update_outputs(self) -> None:
2619
+ """Call method |Sequences.update_outputs| of attribute |Model.sequences|.
2620
+
2621
+ When working in Cython mode, the standard model import overrides this generic
2622
+ Python version with a model-specific Cython version.
2623
+ """
2624
+ self.sequences.update_outputs()
2625
+
2626
+ @classmethod
2627
+ def get_methods(cls, skip: tuple[MethodGroup, ...] = ()) -> Iterator[type[Method]]:
2628
+ """Convenience method for iterating through all methods selected by a |Model|
2629
+ subclass.
2630
+
2631
+ >>> from hydpy.models import hland_96, ga_garto_submodel1
2632
+ >>> for method in hland_96.Model.get_methods():
2633
+ ... print(method.__name__) # doctest: +ELLIPSIS
2634
+ Calc_TC_V1
2635
+ ...
2636
+ Pass_Q_V1
2637
+
2638
+ >>> for method in ga_garto_submodel1.Model.get_methods():
2639
+ ... print(method.__name__) # doctest: +ELLIPSIS
2640
+ Set_InitialSurfaceWater_V1
2641
+ ...
2642
+ Get_SoilWaterContent_V1
2643
+ Return_RelativeMoisture_V1
2644
+ ...
2645
+ Withdraw_AllBins_V1
2646
+
2647
+ One can skip all methods that belong to specific groups:
2648
+
2649
+ >>> for method in hland_96.Model.get_methods(skip=("OUTLET_METHODS",)):
2650
+ ... print(method.__name__) # doctest: +ELLIPSIS
2651
+ Calc_TC_V1
2652
+ ...
2653
+ Calc_OutRC_RConcModel_V1
2654
+
2655
+ >>> for method in hland_96.Model.get_methods(("OUTLET_METHODS", "ADD_METHODS")):
2656
+ ... print(method.__name__) # doctest: +ELLIPSIS
2657
+ Calc_TC_V1
2658
+ ...
2659
+ Calc_QT_V1
2660
+
2661
+ Note that function |Model.get_methods| returns the "raw" |Method| objects
2662
+ instead of the modified Python or Cython functions used for performing
2663
+ calculations.
2664
+ """
2665
+ methods = set()
2666
+ if hasattr(cls, "METHOD_GROUPS"):
2667
+ for groupname in cls.METHOD_GROUPS:
2668
+ if groupname in skip:
2669
+ continue
2670
+ if (groupname == "ADD_METHODS") and hasattr(cls, "INTERFACE_METHODS"):
2671
+ for method in cls.INTERFACE_METHODS:
2672
+ if method not in methods:
2673
+ methods.add(method)
2674
+ yield method
2675
+ for method in getattr(cls, groupname, ()):
2676
+ if method not in methods:
2677
+ methods.add(method)
2678
+ yield method
2679
+
2680
+ @overload
2681
+ def find_submodels(
2682
+ self,
2683
+ *,
2684
+ include_subsubmodels: bool = True,
2685
+ include_mainmodel: bool = False,
2686
+ include_sidemodels: Literal[False] = ...,
2687
+ include_optional: Literal[False] = ...,
2688
+ include_feedbacks: bool = False,
2689
+ aggregate_vectors: Literal[False] = ...,
2690
+ repeat_sharedmodels: bool = False,
2691
+ position: Literal[0, -1] | None = None,
2692
+ ) -> dict[str, Model]: ...
2693
+
2694
+ @overload
2695
+ def find_submodels(
2696
+ self,
2697
+ *,
2698
+ include_subsubmodels: bool = True,
2699
+ include_mainmodel: bool = False,
2700
+ include_sidemodels: Literal[False] = ...,
2701
+ include_optional: Literal[True],
2702
+ include_feedbacks: bool = False,
2703
+ aggregate_vectors: Literal[False] = ...,
2704
+ repeat_sharedmodels: bool = False,
2705
+ position: Literal[0, -1] | None = None,
2706
+ ) -> dict[str, Model | None]: ...
2707
+
2708
+ @overload
2709
+ def find_submodels(
2710
+ self,
2711
+ *,
2712
+ include_subsubmodels: bool = True,
2713
+ include_mainmodel: bool = False,
2714
+ include_sidemodels: Literal[False] = ...,
2715
+ include_optional: Literal[False] = ...,
2716
+ include_feedbacks: bool = False,
2717
+ aggregate_vectors: Literal[True],
2718
+ repeat_sharedmodels: bool = False,
2719
+ ) -> dict[str, Model | None]: ...
2720
+
2721
+ @overload
2722
+ def find_submodels(
2723
+ self,
2724
+ *,
2725
+ include_subsubmodels: bool = True,
2726
+ include_mainmodel: bool = False,
2727
+ include_sidemodels: Literal[False] = ...,
2728
+ include_optional: Literal[True],
2729
+ include_feedbacks: bool = False,
2730
+ aggregate_vectors: Literal[True],
2731
+ repeat_sharedmodels: bool = False,
2732
+ ) -> dict[str, Model | None]: ...
2733
+
2734
+ @overload
2735
+ def find_submodels(
2736
+ self,
2737
+ *,
2738
+ include_subsubmodels: Literal[False],
2739
+ include_mainmodel: bool = False,
2740
+ include_sidemodels: Literal[True],
2741
+ include_optional: Literal[False] = ...,
2742
+ include_feedbacks: bool = False,
2743
+ aggregate_vectors: Literal[False] = ...,
2744
+ repeat_sharedmodels: bool = False,
2745
+ position: Literal[0, -1] | None = None,
2746
+ ) -> dict[str, Model]: ...
2747
+
2748
+ @overload
2749
+ def find_submodels(
2750
+ self,
2751
+ *,
2752
+ include_subsubmodels: Literal[False],
2753
+ include_mainmodel: bool = False,
2754
+ include_sidemodels: Literal[True],
2755
+ include_optional: Literal[True],
2756
+ include_feedbacks: bool = False,
2757
+ aggregate_vectors: Literal[False] = ...,
2758
+ repeat_sharedmodels: bool = False,
2759
+ position: Literal[0, -1] | None = None,
2760
+ ) -> dict[str, Model | None]: ...
2761
+
2762
+ @overload
2763
+ def find_submodels(
2764
+ self,
2765
+ *,
2766
+ include_subsubmodels: Literal[False],
2767
+ include_mainmodel: bool = False,
2768
+ include_sidemodels: Literal[True],
2769
+ include_optional: Literal[False] = ...,
2770
+ include_feedbacks: bool = False,
2771
+ aggregate_vectors: Literal[True],
2772
+ repeat_sharedmodels: bool = False,
2773
+ ) -> dict[str, Model | None]: ...
2774
+
2775
+ @overload
2776
+ def find_submodels(
2777
+ self,
2778
+ *,
2779
+ include_subsubmodels: Literal[False],
2780
+ include_mainmodel: bool = False,
2781
+ include_sidemodels: Literal[True],
2782
+ include_optional: Literal[True],
2783
+ include_feedbacks: bool = False,
2784
+ aggregate_vectors: Literal[True],
2785
+ repeat_sharedmodels: bool = False,
2786
+ ) -> dict[str, Model | None]: ...
2787
+
2788
+ def find_submodels(
2789
+ self,
2790
+ *,
2791
+ include_subsubmodels: bool = True,
2792
+ include_mainmodel: bool = False,
2793
+ include_sidemodels: bool = False,
2794
+ include_optional: bool = False,
2795
+ include_feedbacks: bool = False,
2796
+ aggregate_vectors: bool = False,
2797
+ repeat_sharedmodels: bool = False,
2798
+ position: Literal[0, -1] | None = None,
2799
+ ) -> dict[str, Model] | dict[str, Model | None]:
2800
+ """Find the (sub)submodel instances of the current main model instance.
2801
+
2802
+ Method |Model.find_submodels| returns an empty dictionary by default if no
2803
+ submodel is available:
2804
+
2805
+ >>> from hydpy import prepare_model
2806
+ >>> model = prepare_model("lland_knauf")
2807
+ >>> model.find_submodels()
2808
+ {}
2809
+
2810
+ The `include_mainmodel` parameter allows the addition of the main model:
2811
+
2812
+ >>> model.find_submodels(include_mainmodel=True)
2813
+ {'model': lland_knauf}
2814
+
2815
+ The `include_optional` parameter allows considering prepared and unprepared
2816
+ submodels:
2817
+
2818
+ >>> model.find_submodels(include_optional=True)
2819
+ {'model.aetmodel': None, 'model.radiationmodel': None, 'model.soilmodel': None}
2820
+ >>> model.aetmodel = prepare_model("evap_aet_minhas")
2821
+ >>> model.aetmodel.petmodel = prepare_model("evap_pet_mlc")
2822
+ >>> model.aetmodel.petmodel.retmodel = prepare_model("evap_ret_tw2002")
2823
+ >>> from pprint import pprint
2824
+ >>> pprint(model.find_submodels(include_optional=True)) # doctest: +ELLIPSIS
2825
+ {'model.aetmodel': evap_aet_minhas...,
2826
+ 'model.aetmodel.intercmodel': None,
2827
+ 'model.aetmodel.petmodel': evap_pet_mlc...,
2828
+ 'model.aetmodel.petmodel.retmodel': evap_ret_tw2002,
2829
+ 'model.aetmodel.petmodel.retmodel.radiationmodel': None,
2830
+ 'model.aetmodel.petmodel.retmodel.tempmodel': None,
2831
+ 'model.aetmodel.soilwatermodel': None,
2832
+ 'model.radiationmodel': None,
2833
+ 'model.soilmodel': None}
2834
+
2835
+ By default, |Model.find_submodels| does not return an additional entry when a
2836
+ main model serves as a sub-submodel:
2837
+
2838
+ >>> model.aetmodel.soilwatermodel = model
2839
+ >>> model.aetmodel.soilwatermodel_is_mainmodel = True
2840
+ >>> pprint(model.find_submodels(include_optional=True)) # doctest: +ELLIPSIS
2841
+ {'model.aetmodel': evap_aet_minhas...,
2842
+ 'model.aetmodel.intercmodel': None,
2843
+ 'model.aetmodel.petmodel': evap_pet_mlc...,
2844
+ 'model.aetmodel.petmodel.retmodel': evap_ret_tw2002,
2845
+ 'model.aetmodel.petmodel.retmodel.radiationmodel': None,
2846
+ 'model.aetmodel.petmodel.retmodel.tempmodel': None,
2847
+ 'model.radiationmodel': None,
2848
+ 'model.soilmodel': None}
2849
+
2850
+ Use the `include_feedbacks` parameter to make such feedback connections
2851
+ transparent:
2852
+
2853
+ >>> pprint(model.find_submodels(include_mainmodel=True,
2854
+ ... include_optional=True, include_feedbacks=True)) # doctest: +ELLIPSIS
2855
+ {'model': lland_knauf...,
2856
+ 'model.aetmodel': evap_aet_minhas...,
2857
+ 'model.aetmodel.intercmodel': None,
2858
+ 'model.aetmodel.petmodel': evap_pet_mlc...,
2859
+ 'model.aetmodel.petmodel.retmodel': evap_ret_tw2002,
2860
+ 'model.aetmodel.petmodel.retmodel.radiationmodel': None,
2861
+ 'model.aetmodel.petmodel.retmodel.tempmodel': None,
2862
+ 'model.aetmodel.soilwatermodel': lland_knauf...,
2863
+ 'model.radiationmodel': None,
2864
+ 'model.soilmodel': None}
2865
+
2866
+ |Model.find_submodels| includes only one reference to shared model instances by
2867
+ default:
2868
+
2869
+ >>> model.radiationmodel = prepare_model("meteo_glob_fao56")
2870
+ >>> model.aetmodel = prepare_model("evap_aet_morsim")
2871
+ >>> model.aetmodel.radiationmodel = model.radiationmodel
2872
+ >>> pprint(model.find_submodels(include_optional=True)) # doctest: +ELLIPSIS
2873
+ {'model.aetmodel': evap_aet_morsim...,
2874
+ 'model.aetmodel.intercmodel': None,
2875
+ 'model.aetmodel.snowalbedomodel': None,
2876
+ 'model.aetmodel.snowcovermodel': None,
2877
+ 'model.aetmodel.snowycanopymodel': None,
2878
+ 'model.aetmodel.soilwatermodel': None,
2879
+ 'model.aetmodel.tempmodel': None,
2880
+ 'model.radiationmodel': meteo_glob_fao56,
2881
+ 'model.soilmodel': None}
2882
+
2883
+ Use the `repeat_sharedmodels` parameter to change this behaviour:
2884
+
2885
+ >>> pprint(model.find_submodels(
2886
+ ... repeat_sharedmodels=True, include_optional=True)) # doctest: +ELLIPSIS
2887
+ {'model.aetmodel': evap_aet_morsim...,
2888
+ 'model.aetmodel.intercmodel': None,
2889
+ 'model.aetmodel.radiationmodel': meteo_glob_fao56,
2890
+ 'model.aetmodel.snowalbedomodel': None,
2891
+ 'model.aetmodel.snowcovermodel': None,
2892
+ 'model.aetmodel.snowycanopymodel': None,
2893
+ 'model.aetmodel.soilwatermodel': None,
2894
+ 'model.aetmodel.tempmodel': None,
2895
+ 'model.radiationmodel': meteo_glob_fao56,
2896
+ 'model.soilmodel': None}
2897
+
2898
+ All previous examples dealt with scalar submodel references handled by
2899
+ |SubmodelProperty|. Now we will focus on vectors of submodel references
2900
+ handled by |SubmodelsProperty| and take |sw1d_channel| as an example:
2901
+
2902
+ >>> channel = prepare_model("sw1d_channel")
2903
+ >>> channel.parameters.control.nmbsegments(2)
2904
+
2905
+ Again, method |Model.find_submodels| returns by default an empty dictionary if
2906
+ no submodel is available:
2907
+
2908
+ >>> channel.find_submodels()
2909
+ {}
2910
+
2911
+ The `include_optional` parameter works as shown for the scalar case. But for
2912
+ scalar cases, the names contain an additional suffix to indicate the position
2913
+ of the respective submodel:
2914
+
2915
+ >>> pprint(channel.find_submodels(include_optional=True))
2916
+ {'model.routingmodels_0': None,
2917
+ 'model.routingmodels_1': None,
2918
+ 'model.routingmodels_2': None,
2919
+ 'model.storagemodels_0': None,
2920
+ 'model.storagemodels_1': None}
2921
+
2922
+ We now add some possible submodels to the |sw1d_channel| main model:
2923
+
2924
+ >>> with channel.add_routingmodel_v1("sw1d_q_in", position=0, update=False):
2925
+ ... pass
2926
+ >>> with channel.add_storagemodel_v1("sw1d_storage", position=0, update=False):
2927
+ ... pass
2928
+ >>> with channel.add_routingmodel_v2("sw1d_lias", position=1, update=False):
2929
+ ... pass
2930
+ >>> with channel.add_storagemodel_v1("sw1d_storage", position=1, update=False):
2931
+ ... pass
2932
+ >>> with channel.add_routingmodel_v3("sw1d_weir_out", position=2, update=False):
2933
+ ... pass
2934
+
2935
+ Method |Model.find_submodels| associates them with the correct positions:
2936
+
2937
+ >>> pprint(channel.find_submodels())
2938
+ {'model.routingmodels_0': sw1d_q_in,
2939
+ 'model.routingmodels_1': sw1d_lias,
2940
+ 'model.routingmodels_2': sw1d_weir_out,
2941
+ 'model.storagemodels_0': sw1d_storage,
2942
+ 'model.storagemodels_1': sw1d_storage}
2943
+
2944
+ One can use the `aggregate_vectors` parameter to gain a better overview.
2945
+ Then, |Model.find_submodels| reports only the names of the respective
2946
+ |SubmodelsProperty| instances with a suffixed wildcard to distinguish them
2947
+ from |SubmodelProperty| instances:
2948
+
2949
+ >>> channel.find_submodels(aggregate_vectors=True)
2950
+ {'model.routingmodels_*': None, 'model.storagemodels_*': None}
2951
+
2952
+ Another option is to include side models. However, this does not work in
2953
+ combination with including sub-submodels and thus cannot give further insight
2954
+ into the configuration of a |sw1d_channel| model:
2955
+
2956
+ >>> pprint(channel.find_submodels(include_sidemodels=True))
2957
+ Traceback (most recent call last):
2958
+ ...
2959
+ ValueError: Including sub-submodels and side-models leads to ambiguous results.
2960
+
2961
+ So, one needs to apply it to the respective submodels directly:
2962
+
2963
+ >>> pprint(channel.storagemodels[0].find_submodels(
2964
+ ... include_subsubmodels=False, include_sidemodels=True))
2965
+ {'model.routingmodelsdownstream_0': sw1d_lias,
2966
+ 'model.routingmodelsupstream_0': sw1d_q_in}
2967
+
2968
+ >>> pprint(channel.routingmodels[1].find_submodels(
2969
+ ... include_subsubmodels=False, include_sidemodels=True))
2970
+ {'model.routingmodelsdownstream_0': sw1d_weir_out,
2971
+ 'model.routingmodelsupstream_0': sw1d_q_in,
2972
+ 'model.storagemodeldownstream': sw1d_storage,
2973
+ 'model.storagemodelupstream': sw1d_storage}
2974
+
2975
+ When dealing with submodel arrays handled by |SubmodelsProperty| instances, one
2976
+ might be interested in only querying the first or the last model, which is
2977
+ supported by the `position` parameter:
2978
+
2979
+ >>> pprint(channel.find_submodels(position=0))
2980
+ {'model.routingmodels_0': sw1d_q_in, 'model.storagemodels_0': sw1d_storage}
2981
+ >>> pprint(channel.find_submodels(position=-1))
2982
+ {'model.routingmodels_2': sw1d_weir_out, 'model.storagemodels_1': sw1d_storage}
2983
+ >>> pprint(channel.find_submodels(position=1))
2984
+ Traceback (most recent call last):
2985
+ ...
2986
+ ValueError: The `position` argument requires the integer value `0´ or `-1`, \
2987
+ but the value `1` of type `int` is given.
2988
+ """
2989
+
2990
+ if include_subsubmodels and include_sidemodels:
2991
+ raise ValueError(
2992
+ "Including sub-submodels and side-models leads to ambiguous results."
2993
+ )
2994
+ if position not in (None, 0, -1):
2995
+ raise ValueError(
2996
+ "The `position` argument requires the integer value `0´ or `-1`, but "
2997
+ f"the {objecttools.value_of_type(position)} is given."
2998
+ )
2999
+
3000
+ def _find_submodels(name: str, model: Model) -> None:
3001
+ name2submodel_new = {}
3002
+
3003
+ if isinstance(model, SharableSubmodelInterface):
3004
+ sharables.add(model)
3005
+
3006
+ for subprop in SubmodelProperty.__hydpy_modeltype2instance__[type(model)]:
3007
+ sub_is_main = getattr(model, f"{subprop.name}_is_mainmodel")
3008
+ if (include_sidemodels or not subprop.sidemodel) and (
3009
+ include_feedbacks or not sub_is_main
3010
+ ):
3011
+ submodel = getattr(model, subprop.name)
3012
+ if (include_optional or (submodel is not None)) and (
3013
+ repeat_sharedmodels or (submodel not in sharables)
3014
+ ):
3015
+ name2submodel_new[f"{name}.{subprop.name}"] = submodel
3016
+
3017
+ for subsprop in SubmodelsProperty.__hydpy_modeltype2instance__[type(model)]:
3018
+ if include_sidemodels or not subsprop.sidemodels:
3019
+ submodelsname = f"{name}.{subsprop.name}"
3020
+ if aggregate_vectors:
3021
+ name2submodel_new[f"{submodelsname}_*"] = None
3022
+ elif submodels := subsprop.__hydpy_mainmodel2submodels__[model]:
3023
+ i_last = len(submodels) - 1
3024
+ if position is not None:
3025
+ submodels = [submodels[position]]
3026
+ for i, submodel in enumerate(submodels):
3027
+ # implement when required:
3028
+ assert not isinstance(submodel, SharableSubmodelInterface)
3029
+ if include_optional or (submodel is not None):
3030
+ j = i_last if position == -1 else i
3031
+ name2submodel_new[f"{submodelsname}_{j}"] = submodel
3032
+
3033
+ name2submodel.update(name2submodel_new)
3034
+ if include_subsubmodels:
3035
+ for subname, submodel in name2submodel_new.items():
3036
+ if submodel not in seen:
3037
+ seen.add(submodel)
3038
+ _find_submodels(subname, submodel)
3039
+
3040
+ seen: set[Model] = {self}
3041
+ sharables: set[SharableSubmodelInterface] = set()
3042
+ name2submodel = {"model": self} if include_mainmodel else {}
3043
+ _find_submodels("model", self)
3044
+ return dict(sorted(name2submodel.items()))
3045
+
3046
+ def query_submodels(self, name: types.ModuleType | str, /) -> list[Model]:
3047
+ """Use |Model.find_submodels| to query all (sub)models of the given type.
3048
+
3049
+ >>> from hydpy import prepare_model
3050
+ >>> model = prepare_model("lland_knauf")
3051
+ >>> model.query_submodels("meteo_glob_fao56")
3052
+ []
3053
+
3054
+ >>> model.radiationmodel = prepare_model("meteo_glob_fao56")
3055
+ >>> model.query_submodels("meteo_glob_fao56")
3056
+ [meteo_glob_fao56]
3057
+
3058
+ >>> model.aetmodel = prepare_model("evap_aet_morsim")
3059
+ >>> model.aetmodel.radiationmodel = model.radiationmodel
3060
+ >>> model.query_submodels("meteo_glob_fao56")
3061
+ [meteo_glob_fao56]
3062
+
3063
+ >>> from hydpy.models import meteo_glob_fao56
3064
+ >>> model.aetmodel.radiationmodel = prepare_model(meteo_glob_fao56)
3065
+ >>> model.query_submodels(meteo_glob_fao56)
3066
+ [meteo_glob_fao56, meteo_glob_fao56]
3067
+ """
3068
+ if isinstance(name, types.ModuleType):
3069
+ name = importtools.load_modelmodule(name).Model.__HYDPY_NAME__
3070
+ submodels = self.find_submodels(include_mainmodel=True)
3071
+ return [s for s in submodels.values() if s.name == name]
3072
+
3073
+ def update_parameters(self, ignore_errors: bool = False) -> None:
3074
+ """Use the control parameter values of the current model for updating its
3075
+ derived parameters and the control and derived parameters of all its submodels.
3076
+
3077
+ We use the combination of |hland_96|, |evap_aet_hbv96|, and |evap_pet_hbv96|
3078
+ used by the `HydPy-H-Lahn` project for modelling the Dill catchment as an
3079
+ example:
3080
+
3081
+ >>> from hydpy.core.testtools import prepare_full_example_2
3082
+ >>> hp = prepare_full_example_2()[0]
3083
+ >>> model = hp.elements.land_dill_assl.model
3084
+
3085
+ First, all zones of the Dill catchment are either of type
3086
+ |hland_constants.FIELD| or |hland_constants.FOREST|:
3087
+
3088
+ >>> model.parameters.control.zonetype
3089
+ zonetype(FIELD, FOREST, FIELD, FOREST, FIELD, FOREST, FIELD, FOREST,
3090
+ FIELD, FOREST, FIELD, FOREST)
3091
+
3092
+ Hence, the |evap_control.Soil| parameter of |evap_aet_hbv96| must be |True| for
3093
+ the entire basin, as both zone types possess a soil module which
3094
+ requires soil evapotranspiration estimates:
3095
+
3096
+ >>> model.aetmodel.parameters.control.soil
3097
+ soil(True)
3098
+
3099
+ Second, |hland_96| requires definitions for the zones' altitude
3100
+ (|hland_control.ZoneZ|) and determines the average basin altitude
3101
+ (|hland_derived.Z|) automatically:
3102
+
3103
+ >>> model.parameters.control.zonez
3104
+ zonez(2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 6.0, 6.0, 7.0, 7.0)
3105
+ >>> model.parameters.derived.z
3106
+ z(4.205345)
3107
+
3108
+ |evap_aet_hbv96| handles its altitude data similarly but relies on the unit 1 m
3109
+ instead of 100 m:
3110
+
3111
+ >>> model.aetmodel.petmodel.parameters.control.hrualtitude
3112
+ hrualtitude(200.0, 200.0, 300.0, 300.0, 400.0, 400.0, 500.0, 500.0,
3113
+ 600.0, 600.0, 700.0, 700.0)
3114
+ >>> model.aetmodel.petmodel.parameters.derived.altitude
3115
+ altitude(420.53445)
3116
+
3117
+ We now set the first zone to type |hland_constants.ILAKE| and the altitude of
3118
+ all zones to 400 m:
3119
+
3120
+ >>> from hydpy.models.hland_96 import ILAKE
3121
+ >>> model.parameters.control.zonetype[0] = ILAKE
3122
+ >>> model.parameters.control.zonez(4.0)
3123
+
3124
+ |Model.update_parameters| uses the appropriate interface methods to transfer
3125
+ the updated control parameter values from the main model to all its submodels.
3126
+ So, parameter |evap_control.Soil| parameter of |evap_aet_hbv96| becomes aware
3127
+ of the introduced internal lake zone, which does not include a soil module and
3128
+ hence needs no soil evapotranspiration estimates:
3129
+
3130
+ >>> model.update_parameters()
3131
+ >>> model.aetmodel.parameters.control.soil
3132
+ soil(field=True, forest=True, ilake=False)
3133
+
3134
+ Additionally, |Model.update_parameters| uses method |Parameters.update| of
3135
+ class |Parameters| for updating the derived parameters |hland_derived.Z| of the
3136
+ |hland_96| main model and |evap_derived.Altitude| of the |evap_pet_hbv96|
3137
+ submodel:
3138
+
3139
+ >>> model.parameters.derived.z
3140
+ z(4.0)
3141
+ >>> model.aetmodel.petmodel.parameters.control.hrualtitude
3142
+ hrualtitude(400.0)
3143
+ >>> model.aetmodel.petmodel.parameters.derived.altitude
3144
+ altitude(400.0)
3145
+ """
3146
+ self.parameters.update(ignore_errors=ignore_errors)
3147
+ for name, submodel in self.find_submodels(include_subsubmodels=False).items():
3148
+ if isinstance(submodel, SubmodelInterface):
3149
+ adder = submodel._submodeladder # pylint: disable=protected-access
3150
+ if adder is not None:
3151
+ if adder.dimensionality == 0:
3152
+ adder.update(self, submodel, refresh=True)
3153
+ elif adder.dimensionality == 1:
3154
+ position = int(name.rpartition("_")[2])
3155
+ adder.update(self, submodel, position=position, refresh=True)
3156
+ else:
3157
+ assert_never(adder.dimensionality)
3158
+ submodel.update_parameters(ignore_errors=ignore_errors)
3159
+
3160
+ @property
3161
+ def conditions(self) -> ConditionsModel:
3162
+ """A nested dictionary that contains the values of all condition sequences of
3163
+ a model and its submodels.
3164
+
3165
+ See the documentation on property |HydPy.conditions| for further information.
3166
+ """
3167
+ conditions = {}
3168
+ for name, model in self.find_submodels(include_mainmodel=True).items():
3169
+ conditions[name] = model.sequences.conditions
3170
+ return conditions
3171
+
3172
+ @conditions.setter
3173
+ def conditions(self, conditions: ConditionsModel) -> None:
3174
+ for name, model in self.find_submodels(include_mainmodel=True).items():
3175
+ model.sequences.conditions = conditions[name]
3176
+
3177
+ @property
3178
+ def couple_models(self) -> ModelCoupler | None:
3179
+ """If available, return a function object for coupling models to a composite
3180
+ model suitable at least for the actual model subclass (see method
3181
+ |Elements.unite_collectives|)."""
3182
+ return None
3183
+
3184
+ # ToDo: Replace this hack with a Mypy plugin?
3185
+ def __getattr__(self, item: str) -> Any:
3186
+ assert False
3187
+
3188
+ del __getattr__
3189
+
3190
+ def __setattr__(self, key: str, value: Any) -> None:
3191
+ assert False
3192
+
3193
+ del __setattr__
3194
+
3195
+ def __str__(self) -> str:
3196
+ return self.name
3197
+
3198
+ def __init_subclass__(cls) -> None:
3199
+ modulename = cls.__module__
3200
+ if modulename.startswith("hydpy.interfaces."):
3201
+ cls.__HYDPY_NAME__ = cls.__name__
3202
+ if not modulename.startswith("hydpy.models."):
3203
+ return
3204
+ if modulename.count(".") > 2:
3205
+ modulename = modulename.rpartition(".")[0]
3206
+ module = cast(_ModelModule, importlib.import_module(modulename))
3207
+ modelname = modulename.split(".")[-1]
3208
+ cls.__HYDPY_NAME__ = modelname
3209
+
3210
+ allsequences = set()
3211
+ st = sequencetools
3212
+ infos: tuple[tuple[type[Any], type[Any], set[Any]], ...] = (
3213
+ (st.InletSequences, st.InletSequence, set()),
3214
+ (st.ReceiverSequences, st.ReceiverSequence, set()),
3215
+ (st.InputSequences, st.InputSequence, set()),
3216
+ (st.FluxSequences, st.FluxSequence, set()),
3217
+ (st.FactorSequences, st.FactorSequence, set()),
3218
+ (st.StateSequences, st.StateSequence, set()),
3219
+ (st.LogSequences, st.LogSequence, set()),
3220
+ (st.AideSequences, st.AideSequence, set()),
3221
+ (st.OutletSequences, st.OutletSequence, set()),
3222
+ (st.SenderSequences, st.SenderSequence, set()),
3223
+ )
3224
+ for method in cls.get_methods():
3225
+ for sequence in itertools.chain(
3226
+ method.REQUIREDSEQUENCES,
3227
+ method.UPDATEDSEQUENCES,
3228
+ method.RESULTSEQUENCES,
3229
+ ):
3230
+ for _, typesequence, sequences in infos:
3231
+ if issubclass(sequence, typesequence):
3232
+ sequences.add(sequence)
3233
+ for typesequences, _, sequences in infos:
3234
+ allsequences.update(sequences)
3235
+ classname = typesequences.__name__
3236
+ if not hasattr(module, classname):
3237
+ members = {
3238
+ "CLASSES": variabletools.sort_variables(sequences),
3239
+ "__doc__": f"{classname[:-9]} sequences of model {modelname}.",
3240
+ "__module__": modulename,
3241
+ }
3242
+ setattr(module, classname, type(classname, (typesequences,), members))
3243
+
3244
+ fixedparameters = set(getattr(module, "ADDITIONAL_FIXEDPARAMETERS", ()))
3245
+ controlparameters = set(getattr(module, "ADDITIONAL_CONTROLPARAMETERS", ()))
3246
+ derivedparameters = set(getattr(module, "ADDITIONAL_DERIVEDPARAMETERS", ()))
3247
+ for host in itertools.chain(cls.get_methods(), allsequences):
3248
+ fixedparameters.update(getattr(host, "FIXEDPARAMETERS", ()))
3249
+ controlparameters.update(getattr(host, "CONTROLPARAMETERS", ()))
3250
+ derivedparameters.update(getattr(host, "DERIVEDPARAMETERS", ()))
3251
+ for par in itertools.chain(
3252
+ controlparameters.copy(), derivedparameters.copy(), cls.SOLVERPARAMETERS
3253
+ ):
3254
+ fixedparameters.update(getattr(par, "FIXEDPARAMETERS", ()))
3255
+ controlparameters.update(getattr(par, "CONTROLPARAMETERS", ()))
3256
+ derivedparameters.update(getattr(par, "DERIVEDPARAMETERS", ()))
3257
+ if controlparameters and not hasattr(module, "ControlParameters"):
3258
+ module.ControlParameters = type(
3259
+ "ControlParameters",
3260
+ (parametertools.SubParameters,),
3261
+ {
3262
+ "CLASSES": variabletools.sort_variables(controlparameters),
3263
+ "__doc__": f"Control parameters of model {modelname}.",
3264
+ "__module__": modulename,
3265
+ },
3266
+ )
3267
+ if derivedparameters and not hasattr(module, "DerivedParameters"):
3268
+ module.DerivedParameters = type(
3269
+ "DerivedParameters",
3270
+ (parametertools.SubParameters,),
3271
+ {
3272
+ "CLASSES": variabletools.sort_variables(derivedparameters),
3273
+ "__doc__": f"Derived parameters of model {modelname}.",
3274
+ "__module__": modulename,
3275
+ },
3276
+ )
3277
+ if fixedparameters and not hasattr(module, "FixedParameters"):
3278
+ module.FixedParameters = type(
3279
+ "FixedParameters",
3280
+ (parametertools.SubParameters,),
3281
+ {
3282
+ "CLASSES": variabletools.sort_variables(fixedparameters),
3283
+ "__doc__": f"Fixed parameters of model {modelname}.",
3284
+ "__module__": modulename,
3285
+ },
3286
+ )
3287
+ if cls.SOLVERPARAMETERS and not hasattr(module, "SolverParameters"):
3288
+ module.SolverParameters = type(
3289
+ "SolverParameters",
3290
+ (parametertools.SubParameters,),
3291
+ {
3292
+ "CLASSES": variabletools.sort_variables(cls.SOLVERPARAMETERS),
3293
+ "__doc__": f"Solver parameters of model {modelname}.",
3294
+ "__module__": modulename,
3295
+ },
3296
+ )
3297
+
3298
+ cls.REUSABLE_METHODS = tuple(
3299
+ method for method in cls.get_methods() if issubclass(method, ReusableMethod)
3300
+ )
3301
+
3302
+ def __repr__(self) -> str:
3303
+ lines = [self.name]
3304
+ for port, model in self.find_submodels().items():
3305
+ prefix = port.count(".") * " "
3306
+ lines.append(f"{prefix}{port.rsplit('.')[-1]}: {model.name}")
3307
+ return "\n".join(lines)
3308
+
3309
+
3310
+ class RunModel(Model):
3311
+ """Base class for |AdHocModel| and |SegmentModel| that introduces so-called "run
3312
+ methods", which need to be executed in the order of their positions in the
3313
+ |RunModel.RUN_METHODS| tuple."""
3314
+
3315
+ RUN_METHODS: ClassVar[tuple[type[Method], ...]]
3316
+ METHOD_GROUPS = (
3317
+ "RECEIVER_METHODS",
3318
+ "INLET_METHODS",
3319
+ "RUN_METHODS",
3320
+ "ADD_METHODS",
3321
+ "OUTLET_METHODS",
3322
+ "SENDER_METHODS",
3323
+ )
3324
+
3325
+ @abc.abstractmethod
3326
+ def run(self) -> None:
3327
+ """Call all methods defined as "run methods" in the defined order."""
3328
+
3329
+ def simulate(self, idx: int) -> None:
3330
+ """Perform a simulation run over a single simulation time step.
3331
+
3332
+ The required argument `idx` corresponds to property `idx_sim`
3333
+ (see the main documentation on class |Model|).
3334
+
3335
+ You can integrate method |Model.simulate| into your workflows for
3336
+ tailor-made simulation runs. Method |Model.simulate| is complete
3337
+ enough to allow for consecutive calls. However, note that it
3338
+ does neither call |Model.save_data|, |Model.update_receivers|,
3339
+ nor |Model.update_senders|. Also, one would have to reset the
3340
+ related node sequences, as done in the following example:
3341
+
3342
+ >>> from hydpy.core.testtools import prepare_full_example_2
3343
+ >>> hp, pub, TestIO = prepare_full_example_2()
3344
+ >>> model = hp.elements.land_dill_assl.model
3345
+ >>> for idx in range(4):
3346
+ ... model.simulate(idx)
3347
+ ... print(hp.nodes.dill_assl.sequences.sim)
3348
+ ... hp.nodes.dill_assl.sequences.sim = 0.0
3349
+ sim(11.757526)
3350
+ sim(8.865079)
3351
+ sim(7.101815)
3352
+ sim(5.994195)
3353
+ >>> hp.nodes.dill_assl.sequences.sim.series
3354
+ InfoArray([nan, nan, nan, nan])
3355
+
3356
+ The results above are identical to those of method |HydPy.simulate|
3357
+ of class |HydPy|, which is the standard method to perform simulation
3358
+ runs (except that method |HydPy.simulate| of class |HydPy| also
3359
+ performs the steps neglected by method |Model.simulate| of class
3360
+ |Model| mentioned above):
3361
+
3362
+ >>> from hydpy import round_
3363
+ >>> hp.reset_conditions()
3364
+ >>> hp.simulate()
3365
+ >>> round_(hp.nodes.dill_assl.sequences.sim.series)
3366
+ 11.757526, 8.865079, 7.101815, 5.994195
3367
+
3368
+ When working in Cython mode, the standard model import overrides
3369
+ this generic Python version with a model-specific Cython version.
3370
+
3371
+ .. testsetup::
3372
+
3373
+ >>> from hydpy import Node, Element
3374
+ >>> Node.clear_all()
3375
+ >>> Element.clear_all()
3376
+ """
3377
+ self.reset_reuseflags()
3378
+ self.load_data(idx)
3379
+ self.update_inlets()
3380
+ self.run()
3381
+ self.new2old()
3382
+ self.update_outlets()
3383
+ self.update_outputs()
3384
+
3385
+
3386
+ class AdHocModel(RunModel):
3387
+ """Base class for models solving the underlying differential equations in an "ad
3388
+ hoc manner".
3389
+
3390
+ "Ad hoc" stands for the classical approaches in hydrology to calculate individual
3391
+ fluxes separately (often sequentially) and without error control
3392
+ :cite:p:`ref-Clark2010`.
3393
+ """
3394
+
3395
+ def run(self) -> None:
3396
+ """Call all methods defined as "run methods" in the defined order.
3397
+
3398
+ >>> from hydpy.core.modeltools import AdHocModel, Method
3399
+ >>> class print_1(Method):
3400
+ ... @staticmethod
3401
+ ... def __call__(self):
3402
+ ... print(1)
3403
+ >>> class print_2(Method):
3404
+ ... @staticmethod
3405
+ ... def __call__(self):
3406
+ ... print(2)
3407
+ >>> class Test(AdHocModel):
3408
+ ... RUN_METHODS = print_1, print_2
3409
+ >>> Test().run()
3410
+ 1
3411
+ 2
3412
+
3413
+ When working in Cython mode, the standard model import overrides this generic
3414
+ Python version with a model-specific Cython version.
3415
+ """
3416
+ for method in self.RUN_METHODS:
3417
+ method.__call__(self) # pylint: disable=unnecessary-dunder-call
3418
+
3419
+
3420
+ class SegmentModel(RunModel):
3421
+ """Base class for (routing) models that solve the underlying differential equations
3422
+ "segment-wise".
3423
+
3424
+ "segment-wise" means that |SegmentModel| first runs the "run methods" for the
3425
+ first segment (by setting |SegmentModel.idx_segment| to zero), then for the
3426
+ second segment (by setting |SegmentModel.idx_segment| to one), and so on.
3427
+ Therefore, it requires the concrete model subclass to provide a control
3428
+ parameter named "NmbSegments". Additionally, it requires the concrete
3429
+ model to implement a solver parameter named "NmbRuns" that defines how many
3430
+ times the "run methods" need to be (repeatedly) executed for each segment.
3431
+ See |musk_classic| and |musk_mct| as examples.
3432
+ """
3433
+
3434
+ idx_segment = Idx_Segment()
3435
+ idx_run = Idx_Run()
3436
+ nmb_segments: int = 0
3437
+
3438
+ def run(self) -> None:
3439
+ """Call all methods defined as "run methods" "segment-wise".
3440
+
3441
+ When working in Cython mode, the standard model import overrides this generic
3442
+ Python version with a model-specific Cython version.
3443
+ """
3444
+
3445
+ for idx_segment in range(self.parameters.control.nmbsegments.value):
3446
+ self.idx_segment = idx_segment
3447
+ for idx_run in range(self.parameters.solver.nmbruns.value):
3448
+ self.idx_run = idx_run
3449
+ for method in self.RUN_METHODS:
3450
+ method.__call__(self) # pylint: disable=unnecessary-dunder-call
3451
+
3452
+ def run_segments(self, method: Method) -> None:
3453
+ """Run the given methods for all segments.
3454
+
3455
+ Method |SegmentModel.run_segments| is mainly thought for testing purposes.
3456
+ See the documentation on method |musk_model.Calc_Discharge_V1| on how to apply
3457
+ it.
3458
+ """
3459
+ try:
3460
+ for idx in range(self.nmb_segments):
3461
+ self.idx_segment = idx
3462
+ method()
3463
+ finally:
3464
+ self.idx_segment = 0
3465
+
3466
+
3467
+ class SubstepModel(RunModel):
3468
+ """Base class for (routing) models that solve the underlying differential equations
3469
+ "substep-wise".
3470
+
3471
+ "substep-wise" means method |SubstepModel.run| repeatedly calls all "run methods"
3472
+ in the usual order within each simulation step until the |SubstepModel.timeleft|
3473
+ attribute is not larger than zero anymore. The concrete model subclass is up to
3474
+ reduce |SubstepModel.timeleft|. This mechanism allows the concrete model to
3475
+ adjust the internal calculation time step depending on its current accuracy and
3476
+ stability requirements.
3477
+ """
3478
+
3479
+ cymodel: CySubstepModelProtocol | None
3480
+
3481
+ _timeleft: float = 0.0
3482
+
3483
+ @property
3484
+ def timeleft(self) -> float:
3485
+ """The time left within the current simulation step [s]."""
3486
+ if (cymodel := self.cymodel) is None:
3487
+ return self._timeleft
3488
+ return cymodel.timeleft
3489
+
3490
+ @timeleft.setter
3491
+ def timeleft(self, value: float) -> None:
3492
+ if (cymodel := self.cymodel) is None:
3493
+ self._timeleft = value
3494
+ else:
3495
+ cymodel.timeleft = value
3496
+
3497
+ def run(self) -> None:
3498
+ """Call all methods defined as "run methods" repeatedly.
3499
+
3500
+ When working in Cython mode, the standard model import overrides this generic
3501
+ Python version with a model-specific Cython version.
3502
+ """
3503
+ self.timeleft = self.parameters.derived.seconds.value
3504
+ while True:
3505
+ for method in self.RUN_METHODS:
3506
+ method.__call__(self) # pylint: disable=unnecessary-dunder-call
3507
+ if self.timeleft <= 0.0:
3508
+ break
3509
+ self.new2old()
3510
+
3511
+
3512
+ class SolverModel(Model):
3513
+ """Base class for hydrological models, which solve ordinary differential equations
3514
+ with numerical integration algorithms."""
3515
+
3516
+ PART_ODE_METHODS: ClassVar[tuple[type[Method], ...]]
3517
+ FULL_ODE_METHODS: ClassVar[tuple[type[Method], ...]]
3518
+
3519
+ @abc.abstractmethod
3520
+ def solve(self) -> None:
3521
+ """Solve all `FULL_ODE_METHODS` in parallel."""
3522
+
3523
+
3524
+ class NumConstsELS:
3525
+ """Configuration options for using the "Explicit Lobatto Sequence" implemented by
3526
+ class |ELSModel|.
3527
+
3528
+ You can change the following solver options at your own risk.
3529
+
3530
+ >>> from hydpy.core.modeltools import NumConstsELS
3531
+ >>> consts = NumConstsELS()
3532
+
3533
+ The maximum number of Runge Kutta submethods to be applied (the higher, the better
3534
+ the theoretical accuracy, but also the worse the time spent unsuccessful when the
3535
+ theory does not apply):
3536
+
3537
+ >>> consts.nmb_methods
3538
+ 10
3539
+
3540
+ The number of entries to handle the stages of the highest order method (must agree
3541
+ with the maximum number of methods):
3542
+
3543
+ >>> consts.nmb_stages
3544
+ 11
3545
+
3546
+ The maximum increase of the integration step size in case of success:
3547
+
3548
+ >>> consts.dt_increase
3549
+ 2.0
3550
+
3551
+ The maximum decrease of the integration step size in case of failure:
3552
+
3553
+ >>> consts.dt_decrease
3554
+ 10.0
3555
+
3556
+ The Runge Kutta coefficients, one matrix for each submethod:
3557
+
3558
+ >>> consts.a_coefs.shape
3559
+ (11, 12, 11)
3560
+ """
3561
+
3562
+ nmb_methods: int
3563
+ nmb_stages: int
3564
+ dt_increase: float
3565
+ dt_decrease: float
3566
+ a_coeffs: numpy.ndarray
3567
+
3568
+ def __init__(self):
3569
+ self.nmb_methods = 10
3570
+ self.nmb_stages = 11
3571
+ self.dt_increase = 2.0
3572
+ self.dt_decrease = 10.0
3573
+ path = os.path.join(
3574
+ conf.__path__[0], "a_coefficients_explicit_lobatto_sequence.npy"
3575
+ )
3576
+ self.a_coefs = numpy.load(path)
3577
+
3578
+
3579
+ class NumVarsELS:
3580
+ """Intermediate results of the "Explicit Lobatto Sequence" implemented by class
3581
+ |ELSModel|.
3582
+
3583
+ Class |NumVarsELS| should be of relevance for model developers, as it helps to
3584
+ evaluate how efficient newly implemented models are solved (see the documentation
3585
+ on method |ELSModel.solve| of class |ELSModel| as an example).
3586
+ """
3587
+
3588
+ use_relerror: bool
3589
+ nmb_calls: int
3590
+ t0: float
3591
+ t1: float
3592
+ dt_est: float
3593
+ dt: float
3594
+ idx_method: int
3595
+ idx_stage: int
3596
+ abserror: float
3597
+ relerror: float
3598
+ last_abserror: float
3599
+ last_relerror: float
3600
+ extrapolated_abserror: float
3601
+ extrapolated_relerror: float
3602
+ f0_ready: bool
3603
+
3604
+ def __init__(self):
3605
+ self.use_relerror = False
3606
+ self.nmb_calls = 0
3607
+ self.t0 = 0.0
3608
+ self.t1 = 0.0
3609
+ self.dt_est = 1.0
3610
+ self.dt = 1.0
3611
+ self.idx_method = 0
3612
+ self.idx_stage = 0
3613
+ self.abserror = 0.0
3614
+ self.relerror = 0.0
3615
+ self.last_abserror = 0.0
3616
+ self.last_relerror = 0.0
3617
+ self.extrapolated_abserror = 0.0
3618
+ self.extrapolated_relerror = 0.0
3619
+ self.f0_ready = False
3620
+
3621
+
3622
+ class ELSModel(SolverModel):
3623
+ """Base class for hydrological models using the "Explicit Lobatto Sequence" for
3624
+ solving ordinary differential equations.
3625
+
3626
+ The "Explicit Lobatto Sequence" is a variable order Runge Kutta method combining
3627
+ different Lobatto methods. Its main idea is to first calculate a solution with a
3628
+ lower order method, then use these results to apply the next higher-order method,
3629
+ and to compare both results. If they are close enough, the latter results are
3630
+ accepted. If not, the next higher-order method is applied (or, if no higher-order
3631
+ method is available, the step size is decreased, and the algorithm restarts with
3632
+ the method of the lowest order). So far, a thorough description of the algorithm
3633
+ is available in German only :cite:p:`ref-Tyralla2016`.
3634
+
3635
+ Note the strengths and weaknesses of class |ELSModel| discussed in the
3636
+ documentation on method |ELSModel.solve|. Model developers should not derive from
3637
+ class |ELSModel| when trying to implement models with a high potential for stiff
3638
+ parameterisations. Discontinuities should be regularised, for example, by the
3639
+ "smoothing functions" provided by module |smoothtools|. Model users should be
3640
+ careful not to define two small smoothing factors, to avoid needlessly long
3641
+ simulation times.
3642
+ """
3643
+
3644
+ SOLVERSEQUENCES: ClassVar[tuple[type[sequencetools.DependentSequence], ...]]
3645
+ PART_ODE_METHODS: ClassVar[tuple[type[Method], ...]]
3646
+ FULL_ODE_METHODS: ClassVar[tuple[type[Method], ...]]
3647
+ METHOD_GROUPS = (
3648
+ "RECEIVER_METHODS",
3649
+ "INLET_METHODS",
3650
+ "PART_ODE_METHODS",
3651
+ "FULL_ODE_METHODS",
3652
+ "ADD_METHODS",
3653
+ "OUTLET_METHODS",
3654
+ "SENDER_METHODS",
3655
+ )
3656
+ numconsts: NumConstsELS
3657
+ numvars: NumVarsELS
3658
+
3659
+ def __init__(self) -> None:
3660
+ super().__init__()
3661
+ self.numconsts = NumConstsELS()
3662
+ self.numvars = NumVarsELS()
3663
+
3664
+ def simulate(self, idx: int) -> None:
3665
+ """Similar to method |Model.simulate| of class |AdHocModel| but calls method
3666
+ |ELSModel.solve| instead of |AdHocModel.run|.
3667
+
3668
+ When working in Cython mode, the standard model import overrides this generic
3669
+ Python version with a model-specific Cython version.
3670
+ """
3671
+ self.reset_reuseflags()
3672
+ self.load_data(idx)
3673
+ self.update_inlets()
3674
+ self.solve()
3675
+ self.update_outlets()
3676
+ self.update_outputs()
3677
+
3678
+ def solve(self) -> None:
3679
+ """Solve all `FULL_ODE_METHODS` in parallel.
3680
+
3681
+ Implementing numerical integration algorithms that (hopefully) always work well
3682
+ in practice is a tricky task. The following exhaustive examples show how well
3683
+ our "Explicit Lobatto Sequence" algorithm performs for the numerical test
3684
+ models |test_stiff0d| and |test_discontinous|. We hope to cover all possible
3685
+ corner cases. Please tell us if you find one we missed.
3686
+
3687
+ First, we set the value of parameter |test_control.K| to zero, resulting in no
3688
+ changes at all and thus defining the simplest test case possible:
3689
+
3690
+ >>> from hydpy.models.test_stiff0d import *
3691
+ >>> parameterstep()
3692
+ >>> k(0.0)
3693
+
3694
+ Second, we assign values to the solver parameters |test_solver.AbsErrorMax|,
3695
+ |test_solver.RelDTMin|, and |test_solver.RelDTMax| to specify the required
3696
+ numerical accuracy and the smallest and largest internal integration step size
3697
+ allowed:
3698
+
3699
+ >>> solver.abserrormax(0.1)
3700
+ >>> solver.reldtmin(0.001)
3701
+ >>> solver.reldtmax(1.0)
3702
+
3703
+ Additionally, we set |test_solver.RelErrorMax| to |numpy.nan|, which disables
3704
+ taking relative errors into account:
3705
+
3706
+ >>> solver.relerrormax(nan)
3707
+
3708
+ Calling method |ELSModel.solve| correctly calculates zero discharge
3709
+ (|test_fluxes.Q|) and thus does not change the water storage (|test_states.S|):
3710
+
3711
+ >>> states.s(1.0)
3712
+ >>> model.numvars.nmb_calls = 0
3713
+ >>> model.solve()
3714
+ >>> states.s
3715
+ s(1.0)
3716
+ >>> fluxes.q
3717
+ q(0.0)
3718
+
3719
+ The achieve the above result, |ELSModel| requires two function calls, one for
3720
+ the initial guess (using the Explicit Euler Method) and the other one
3721
+ (extending the Explicit Euler method to the Explicit Heun method) to confirm
3722
+ the first guess meets the required accuracy:
3723
+
3724
+ >>> model.numvars.idx_method
3725
+ 2
3726
+ >>> model.numvars.dt
3727
+ 1.0
3728
+ >>> model.numvars.nmb_calls
3729
+ 2
3730
+
3731
+ With moderate changes due to setting the value of parameter |test_control.K|
3732
+ to 0.1, two method calls are still sufficient:
3733
+
3734
+ >>> k(0.1)
3735
+ >>> states.s(1.0)
3736
+ >>> model.numvars.nmb_calls = 0
3737
+ >>> model.solve()
3738
+ >>> states.s
3739
+ s(0.905)
3740
+ >>> fluxes.q
3741
+ q(0.095)
3742
+ >>> model.numvars.idx_method
3743
+ 2
3744
+ >>> model.numvars.nmb_calls
3745
+ 2
3746
+
3747
+ Calculating the analytical solution shows |ELSModel| did not exceed the given
3748
+ tolerance value:
3749
+
3750
+ >>> import numpy
3751
+ >>> from hydpy import round_
3752
+ >>> round_(numpy.exp(-k))
3753
+ 0.904837
3754
+
3755
+ After decreasing the allowed error by one order of magnitude, |ELSModel|
3756
+ requires four method calls (again, one for the first order and one for the
3757
+ second-order method, and two additional calls for the third-order method):
3758
+
3759
+ >>> solver.abserrormax(0.001)
3760
+ >>> states.s(1.0)
3761
+ >>> model.numvars.nmb_calls = 0
3762
+ >>> model.solve()
3763
+ >>> states.s
3764
+ s(0.904833)
3765
+ >>> fluxes.q
3766
+ q(0.095167)
3767
+ >>> model.numvars.idx_method
3768
+ 3
3769
+ >>> model.numvars.nmb_calls
3770
+ 4
3771
+
3772
+ After decreasing |test_solver.AbsErrorMax| by ten again, |ELSModel| needs one
3773
+ further higher-order method, which requires three additional calls, making a
3774
+ sum of seven:
3775
+
3776
+ >>> solver.abserrormax(0.0001)
3777
+ >>> states.s(1.0)
3778
+ >>> model.numvars.nmb_calls = 0
3779
+ >>> model.solve()
3780
+ >>> states.s
3781
+ s(0.904837)
3782
+ >>> fluxes.q
3783
+ q(0.095163)
3784
+ >>> model.numvars.idx_method
3785
+ 4
3786
+ >>> model.numvars.nmb_calls
3787
+ 7
3788
+
3789
+ |ELSModel| achieves even a very extreme numerical precision (just for testing,
3790
+ way beyond hydrological requirements) in one single step but now requires 29
3791
+ method calls:
3792
+
3793
+ >>> solver.abserrormax(1e-12)
3794
+ >>> states.s(1.0)
3795
+ >>> model.numvars.nmb_calls = 0
3796
+ >>> model.solve()
3797
+ >>> states.s
3798
+ s(0.904837)
3799
+ >>> fluxes.q
3800
+ q(0.095163)
3801
+ >>> model.numvars.dt
3802
+ 1.0
3803
+ >>> model.numvars.idx_method
3804
+ 8
3805
+ >>> model.numvars.nmb_calls
3806
+ 29
3807
+
3808
+ With a more dynamical parameterisation, where the storage decreases by about
3809
+ 40 % per time step, |ELSModel| needs seven method calls to meet a "normal"
3810
+ error tolerance:
3811
+
3812
+ >>> solver.abserrormax(0.01)
3813
+ >>> k(0.5)
3814
+ >>> states.s(1.0)
3815
+ >>> model.numvars.nmb_calls = 0
3816
+ >>> model.solve()
3817
+ >>> states.s
3818
+ s(0.606771)
3819
+ >>> fluxes.q
3820
+ q(0.393229)
3821
+ >>> model.numvars.idx_method
3822
+ 4
3823
+ >>> model.numvars.nmb_calls
3824
+ 7
3825
+ >>> round_(numpy.exp(-k))
3826
+ 0.606531
3827
+
3828
+ Being an explicit integration method, the "Explicit Lobatto Sequence" can be
3829
+ inefficient for solving stiff initial value problems. Setting |test_control.K|
3830
+ to 2.0 forces |ELSModel| to solve the problem in two substeps, requiring a
3831
+ total of 22 method calls:
3832
+
3833
+ >>> k(2.0)
3834
+ >>> round_(numpy.exp(-k))
3835
+ 0.135335
3836
+ >>> states.s(1.0)
3837
+ >>> model.numvars.nmb_calls = 0
3838
+ >>> model.solve()
3839
+ >>> states.s
3840
+ s(0.134658)
3841
+ >>> fluxes.q
3842
+ q(0.865342)
3843
+ >>> round_(model.numvars.dt)
3844
+ 0.3
3845
+ >>> model.numvars.nmb_calls
3846
+ 22
3847
+
3848
+ Increasing the stiffness of the initial value problem further can increase
3849
+ computation times rapidly:
3850
+
3851
+ >>> k(4.0)
3852
+ >>> round_(numpy.exp(-k))
3853
+ 0.018316
3854
+ >>> states.s(1.0)
3855
+ >>> model.numvars.nmb_calls = 0
3856
+ >>> model.solve()
3857
+ >>> states.s
3858
+ s(0.019774)
3859
+ >>> fluxes.q
3860
+ q(0.980226)
3861
+ >>> round_(model.numvars.dt)
3862
+ 0.3
3863
+ >>> model.numvars.nmb_calls
3864
+ 44
3865
+
3866
+ If we prevent |ELSModel| from compensatingf or its problems by disallowing it
3867
+ to reduce its integration step size, it does not achieve satisfying results:
3868
+
3869
+ >>> solver.reldtmin(1.0)
3870
+ >>> states.s(1.0)
3871
+ >>> model.numvars.nmb_calls = 0
3872
+ >>> model.solve()
3873
+ >>> states.s
3874
+ s(0.09672)
3875
+ >>> fluxes.q
3876
+ q(0.90328)
3877
+ >>> round_(model.numvars.dt)
3878
+ 1.0
3879
+ >>> model.numvars.nmb_calls
3880
+ 46
3881
+
3882
+ You can restrict the allowed maximum integration step size, which can help to
3883
+ prevent from loosing to much performance due to trying to solve too stiff
3884
+ problems, repeatedly:
3885
+
3886
+ >>> solver.reldtmin(0.001)
3887
+ >>> solver.reldtmax(0.25)
3888
+ >>> states.s(1.0)
3889
+ >>> model.numvars.nmb_calls = 0
3890
+ >>> model.solve()
3891
+ >>> states.s
3892
+ s(0.016806)
3893
+ >>> fluxes.q
3894
+ q(0.983194)
3895
+ >>> round_(model.numvars.dt)
3896
+ 0.25
3897
+ >>> model.numvars.nmb_calls
3898
+ 33
3899
+
3900
+ Alternatively, you can restrict the available number of Lobatto methods. Using
3901
+ two methods only is an inefficient choice for the given initial value problem
3902
+ but at least solves it with the required accuracy:
3903
+
3904
+ >>> solver.reldtmax(1.0)
3905
+ >>> model.numconsts.nmb_methods = 2
3906
+ >>> states.s(1.0)
3907
+ >>> model.numvars.nmb_calls = 0
3908
+ >>> model.solve()
3909
+ >>> states.s
3910
+ s(0.020284)
3911
+ >>> fluxes.q
3912
+ q(0.979716)
3913
+ >>> round_(model.numvars.dt)
3914
+ 0.156698
3915
+ >>> model.numvars.nmb_calls
3916
+ 74
3917
+
3918
+ In the above examples, we control numerical accuracies based on absolute error
3919
+ estimates only via parameter |test_solver.AbsErrorMax|. After assigning an
3920
+ actual value to parameter |test_solver.RelErrorMax|, |ELSModel| also takes
3921
+ relative errors into account. We modify some of the above examples to show how
3922
+ this works.
3923
+
3924
+ Generally, it is sufficient to meet one of both criteria. If we repeat the
3925
+ second example with a relaxed absolute but a strict relative tolerance, we
3926
+ reproduce the original result due to our absolute criteria being the relevant
3927
+ one:
3928
+
3929
+ >>> solver.abserrormax(0.1)
3930
+ >>> solver.relerrormax(0.000001)
3931
+ >>> k(0.1)
3932
+ >>> states.s(1.0)
3933
+ >>> model.solve()
3934
+ >>> states.s
3935
+ s(0.905)
3936
+ >>> fluxes.q
3937
+ q(0.095)
3938
+
3939
+ The same holds for the opposite case of a strict absolute but a relaxed
3940
+ relative tolerance:
3941
+
3942
+ >>> solver.abserrormax(0.000001)
3943
+ >>> solver.relerrormax(0.1)
3944
+ >>> k(0.1)
3945
+ >>> states.s(1.0)
3946
+ >>> model.solve()
3947
+ >>> states.s
3948
+ s(0.905)
3949
+ >>> fluxes.q
3950
+ q(0.095)
3951
+
3952
+ Reiterating the "more dynamical parameterisation" example results in slightly
3953
+ different but also correct results:
3954
+
3955
+ >>> k(0.5)
3956
+ >>> states.s(1.0)
3957
+ >>> model.solve()
3958
+ >>> states.s
3959
+ s(0.607196)
3960
+ >>> fluxes.q
3961
+ q(0.392804)
3962
+
3963
+ Reiterating the stiffest example with a relative instead of an absolute error
3964
+ tolerance of 0.1 achieves higher accuracy, as to be expected due to the value
3965
+ of |test_states.S| being far below 1.0 for some time:
3966
+
3967
+ >>> k(4.0)
3968
+ >>> states.s(1.0)
3969
+ >>> model.solve()
3970
+ >>> states.s
3971
+ s(0.0185)
3972
+ >>> fluxes.q
3973
+ q(0.9815)
3974
+
3975
+ Besides its weaknesses with stiff problems, |ELSModel| cannot solve
3976
+ discontinuous problems well. We use the |test_stiff0d| example model to
3977
+ demonstrate how |ELSModel| behaves when confronted with such a problem.
3978
+
3979
+ >>> from hydpy import reverse_model_wildcard_import
3980
+ >>> reverse_model_wildcard_import()
3981
+ >>> from hydpy.models.test_discontinous import *
3982
+ >>> parameterstep()
3983
+
3984
+ Everything works fine as long as the discontinuity does not affect the
3985
+ considered simulation step:
3986
+
3987
+ >>> k(0.5)
3988
+ >>> solver.abserrormax(0.01)
3989
+ >>> solver.reldtmin(0.001)
3990
+ >>> solver.reldtmax(1.0)
3991
+ >>> solver.relerrormax(nan)
3992
+ >>> states.s(1.0)
3993
+ >>> model.numvars.nmb_calls = 0
3994
+ >>> model.solve()
3995
+ >>> states.s
3996
+ s(0.5)
3997
+ >>> fluxes.q
3998
+ q(0.5)
3999
+ >>> model.numvars.idx_method
4000
+ 2
4001
+ >>> model.numvars.dt
4002
+ 1.0
4003
+ >>> model.numvars.nmb_calls
4004
+ 2
4005
+
4006
+ The occurrence of a discontinuity within the simulation step often increases
4007
+ computation times more than a stiff parameterisation:
4008
+
4009
+ >>> k(2.0)
4010
+ >>> states.s(1.0)
4011
+ >>> model.numvars.nmb_calls = 0
4012
+ >>> model.solve()
4013
+ >>> states.s
4014
+ s(-0.006827)
4015
+ >>> fluxes.q
4016
+ q(1.006827)
4017
+ >>> model.numvars.nmb_calls
4018
+ 58
4019
+
4020
+ >>> k(2.1)
4021
+ >>> states.s(1.0)
4022
+ >>> model.numvars.nmb_calls = 0
4023
+ >>> model.solve()
4024
+ >>> states.s
4025
+ s(-0.00072)
4026
+ >>> fluxes.q
4027
+ q(1.00072)
4028
+ >>> model.numvars.nmb_calls
4029
+ 50
4030
+
4031
+ When working in Cython mode, the standard model import overrides this generic
4032
+ Python version with a model-specific Cython version.
4033
+ """
4034
+ self.numvars.use_relerror = not modelutils.isnan(
4035
+ self.parameters.solver.relerrormax.value
4036
+ )
4037
+ self.numvars.t0, self.numvars.t1 = 0.0, 1.0
4038
+ self.numvars.dt_est = 1.0 * self.parameters.solver.reldtmax
4039
+ self.numvars.f0_ready = False
4040
+ self.reset_sum_fluxes()
4041
+ while self.numvars.t0 < self.numvars.t1 - 1e-14:
4042
+ self.numvars.last_abserror = modelutils.inf
4043
+ self.numvars.last_relerror = modelutils.inf
4044
+ self.numvars.dt = min(
4045
+ self.numvars.t1 - self.numvars.t0,
4046
+ 1.0 * self.parameters.solver.reldtmax.value,
4047
+ max(self.numvars.dt_est, self.parameters.solver.reldtmin.value),
4048
+ )
4049
+ if not self.numvars.f0_ready:
4050
+ self.calculate_single_terms()
4051
+ self.numvars.idx_method = 0
4052
+ self.numvars.idx_stage = 0
4053
+ self.set_point_fluxes()
4054
+ self.set_point_states()
4055
+ self.set_result_states()
4056
+ for self.numvars.idx_method in range(1, self.numconsts.nmb_methods + 1):
4057
+ for self.numvars.idx_stage in range(1, self.numvars.idx_method):
4058
+ self.get_point_states()
4059
+ self.calculate_single_terms()
4060
+ self.set_point_fluxes()
4061
+ for self.numvars.idx_stage in range(1, self.numvars.idx_method + 1):
4062
+ self.integrate_fluxes()
4063
+ self.calculate_full_terms()
4064
+ self.set_point_states()
4065
+ self.set_result_fluxes()
4066
+ self.set_result_states()
4067
+ self.calculate_error()
4068
+ self.extrapolate_error()
4069
+ if self.numvars.idx_method == 1:
4070
+ continue
4071
+ if (self.numvars.abserror <= self.parameters.solver.abserrormax) or (
4072
+ self.numvars.relerror <= self.parameters.solver.relerrormax
4073
+ ):
4074
+ self.numvars.dt_est = self.numconsts.dt_increase * self.numvars.dt
4075
+ self.numvars.f0_ready = False
4076
+ self.addup_fluxes()
4077
+ self.numvars.t0 = self.numvars.t0 + self.numvars.dt
4078
+ self.new2old()
4079
+ break
4080
+ decrease_dt = self.numvars.dt > self.parameters.solver.reldtmin
4081
+ decrease_dt = decrease_dt and (
4082
+ self.numvars.extrapolated_abserror
4083
+ > self.parameters.solver.abserrormax
4084
+ )
4085
+ if self.numvars.use_relerror:
4086
+ decrease_dt = decrease_dt and (
4087
+ self.numvars.extrapolated_relerror
4088
+ > self.parameters.solver.relerrormax
4089
+ )
4090
+ if decrease_dt:
4091
+ self.numvars.f0_ready = True
4092
+ self.numvars.dt_est = self.numvars.dt / self.numconsts.dt_decrease
4093
+ break
4094
+ self.numvars.last_abserror = self.numvars.abserror
4095
+ self.numvars.last_relerror = self.numvars.relerror
4096
+ self.numvars.f0_ready = True
4097
+ else:
4098
+ if self.numvars.dt <= self.parameters.solver.reldtmin:
4099
+ self.numvars.f0_ready = False
4100
+ self.addup_fluxes()
4101
+ self.numvars.t0 = self.numvars.t0 + self.numvars.dt
4102
+ self.new2old()
4103
+ else:
4104
+ self.numvars.f0_ready = True
4105
+ self.numvars.dt_est = self.numvars.dt / self.numconsts.dt_decrease
4106
+ self.get_sum_fluxes()
4107
+
4108
+ def calculate_single_terms(self) -> None:
4109
+ """Apply all methods stored in the `PART_ODE_METHODS` tuple.
4110
+
4111
+ >>> from hydpy.models.test_stiff0d import *
4112
+ >>> parameterstep()
4113
+ >>> k(0.25)
4114
+ >>> states.s = 1.0
4115
+ >>> model.calculate_single_terms()
4116
+ >>> fluxes.q
4117
+ q(0.25)
4118
+ """
4119
+ self.numvars.nmb_calls = self.numvars.nmb_calls + 1
4120
+ for method in self.PART_ODE_METHODS:
4121
+ method.__call__(self) # pylint: disable=unnecessary-dunder-call
4122
+
4123
+ def calculate_full_terms(self) -> None:
4124
+ """Apply all methods stored in the `FULL_ODE_METHODS` tuple.
4125
+
4126
+ >>> from hydpy.models.test_stiff0d import *
4127
+ >>> parameterstep()
4128
+ >>> k(0.25)
4129
+ >>> states.s.old = 1.0
4130
+ >>> fluxes.q = 0.25
4131
+ >>> model.calculate_full_terms()
4132
+ >>> states.s.old
4133
+ 1.0
4134
+ >>> states.s.new
4135
+ 0.75
4136
+ """
4137
+ for method in self.FULL_ODE_METHODS:
4138
+ method.__call__(self) # pylint: disable=unnecessary-dunder-call
4139
+
4140
+ def get_point_states(self) -> None:
4141
+ """Load the states corresponding to the actual stage.
4142
+
4143
+ >>> from hydpy import round_
4144
+ >>> from hydpy.models.test_stiff0d import *
4145
+ >>> parameterstep()
4146
+ >>> states.s.old = 2.0
4147
+ >>> states.s.new = 2.0
4148
+ >>> model.numvars.idx_stage = 2
4149
+ >>> points = numpy.asarray(states.fastaccess._s_points)
4150
+ >>> points[:4] = 0.0, 0.0, 1.0, 0.0
4151
+ >>> model.get_point_states()
4152
+ >>> round_(states.s.old)
4153
+ 2.0
4154
+ >>> round_(states.s.new)
4155
+ 1.0
4156
+
4157
+ >>> from hydpy import reverse_model_wildcard_import, print_vector
4158
+ >>> reverse_model_wildcard_import()
4159
+ >>> from hydpy.models.test_stiff1d import *
4160
+ >>> parameterstep()
4161
+ >>> n(2)
4162
+ >>> states.sv.old = 3.0, 3.0
4163
+ >>> states.sv.new = 3.0, 3.0
4164
+ >>> model.numvars.idx_stage = 2
4165
+ >>> points = numpy.asarray(states.fastaccess._sv_points)
4166
+ >>> points[:4, 0] = 0.0, 0.0, 1.0, 0.0
4167
+ >>> points[:4, 1] = 0.0, 0.0, 2.0, 0.0
4168
+ >>> model.get_point_states()
4169
+ >>> print_vector(states.sv.old)
4170
+ 3.0, 3.0
4171
+ >>> print_vector(states.sv.new)
4172
+ 1.0, 2.0
4173
+ """
4174
+ self._get_states(self.numvars.idx_stage, "points")
4175
+
4176
+ def _get_states(self, idx: int, type_: str) -> None:
4177
+ states = self.sequences.states
4178
+ for state in states:
4179
+ temp = getattr(states.fastaccess, f"_{state.name}_{type_}")
4180
+ state.new = temp[idx]
4181
+
4182
+ def set_point_states(self) -> None:
4183
+ """Save the states corresponding to the actual stage.
4184
+
4185
+ >>> from hydpy import print_vector
4186
+ >>> from hydpy.models.test_stiff0d import *
4187
+ >>> parameterstep()
4188
+ >>> states.s.old = 2.0
4189
+ >>> states.s.new = 1.0
4190
+ >>> model.numvars.idx_stage = 2
4191
+ >>> points = numpy.asarray(states.fastaccess._s_points)
4192
+ >>> points[:] = 0.
4193
+ >>> model.set_point_states()
4194
+ >>> print_vector(points[:4])
4195
+ 0.0, 0.0, 1.0, 0.0
4196
+
4197
+ >>> from hydpy import reverse_model_wildcard_import
4198
+ >>> reverse_model_wildcard_import()
4199
+ >>> from hydpy.models.test_stiff1d import *
4200
+ >>> parameterstep()
4201
+ >>> n(2)
4202
+ >>> states.sv.old = 3.0, 3.0
4203
+ >>> states.sv.new = 1.0, 2.0
4204
+ >>> model.numvars.idx_stage = 2
4205
+ >>> points = numpy.asarray(states.fastaccess._sv_points)
4206
+ >>> points[:] = 0.
4207
+ >>> model.set_point_states()
4208
+ >>> print_vector(points[:4, 0])
4209
+ 0.0, 0.0, 1.0, 0.0
4210
+ >>> print_vector(points[:4, 1])
4211
+ 0.0, 0.0, 2.0, 0.0
4212
+ """
4213
+ self._set_states(self.numvars.idx_stage, "points")
4214
+
4215
+ def set_result_states(self) -> None:
4216
+ """Save the final states of the actual method.
4217
+
4218
+ >>> from hydpy import print_vector
4219
+ >>> from hydpy.models.test_stiff0d import *
4220
+ >>> parameterstep()
4221
+ >>> states.s.old = 2.0
4222
+ >>> states.s.new = 1.0
4223
+ >>> model.numvars.idx_method = 2
4224
+ >>> results = numpy.asarray(states.fastaccess._s_results)
4225
+ >>> results[:] = 0.0
4226
+ >>> model.set_result_states()
4227
+ >>> print_vector(results[:4])
4228
+ 0.0, 0.0, 1.0, 0.0
4229
+
4230
+ >>> from hydpy import reverse_model_wildcard_import
4231
+ >>> reverse_model_wildcard_import()
4232
+ >>> from hydpy.models.test_stiff1d import *
4233
+ >>> parameterstep()
4234
+ >>> n(2)
4235
+ >>> states.sv.old = 3.0, 3.0
4236
+ >>> states.sv.new = 1.0, 2.0
4237
+ >>> model.numvars.idx_method = 2
4238
+ >>> results = numpy.asarray(states.fastaccess._sv_results)
4239
+ >>> results[:] = 0.0
4240
+ >>> model.set_result_states()
4241
+ >>> print_vector(results[:4, 0])
4242
+ 0.0, 0.0, 1.0, 0.0
4243
+ >>> print_vector(results[:4, 1])
4244
+ 0.0, 0.0, 2.0, 0.0
4245
+ """
4246
+ self._set_states(self.numvars.idx_method, "results")
4247
+
4248
+ def _set_states(self, idx: int, type_: str) -> None:
4249
+ states = self.sequences.states
4250
+ for state in states:
4251
+ temp = getattr(states.fastaccess, f"_{state.name}_{type_}")
4252
+ temp[idx] = state.new
4253
+
4254
+ def get_sum_fluxes(self) -> None:
4255
+ """Get the sum of the fluxes calculated so far.
4256
+
4257
+ >>> from hydpy.models.test_stiff0d import *
4258
+ >>> parameterstep()
4259
+ >>> fluxes.q = 0.0
4260
+ >>> fluxes.fastaccess._q_sum = 1.0
4261
+ >>> model.get_sum_fluxes()
4262
+ >>> fluxes.q
4263
+ q(1.0)
4264
+
4265
+ >>> from hydpy import reverse_model_wildcard_import, print_vector
4266
+ >>> reverse_model_wildcard_import()
4267
+ >>> from hydpy.models.test_stiff1d import *
4268
+ >>> parameterstep()
4269
+ >>> n(2)
4270
+ >>> fluxes.qv = 0.0, 0.0
4271
+ >>> numpy.asarray(fluxes.fastaccess._qv_sum)[:] = 1.0, 2.0
4272
+ >>> model.get_sum_fluxes()
4273
+ >>> fluxes.qv
4274
+ qv(1.0, 2.0)
4275
+ """
4276
+ fluxes = self.sequences.fluxes
4277
+ for flux in fluxes.numericsequences:
4278
+ flux(getattr(fluxes.fastaccess, f"_{flux.name}_sum"))
4279
+
4280
+ def set_point_fluxes(self) -> None:
4281
+ """Save the fluxes corresponding to the actual stage.
4282
+
4283
+ >>> from hydpy import print_vector
4284
+ >>> from hydpy.models.test_stiff0d import *
4285
+ >>> parameterstep()
4286
+ >>> fluxes.q = 1.0
4287
+ >>> model.numvars.idx_stage = 2
4288
+ >>> points = numpy.asarray(fluxes.fastaccess._q_points)
4289
+ >>> points[:] = 0.0
4290
+ >>> model.set_point_fluxes()
4291
+ >>> print_vector(points[:4])
4292
+ 0.0, 0.0, 1.0, 0.0
4293
+
4294
+ >>> from hydpy import reverse_model_wildcard_import
4295
+ >>> reverse_model_wildcard_import()
4296
+ >>> from hydpy.models.test_stiff1d import *
4297
+ >>> parameterstep()
4298
+ >>> n(2)
4299
+ >>> fluxes.qv = 1.0, 2.0
4300
+ >>> model.numvars.idx_stage = 2
4301
+ >>> points = numpy.asarray(fluxes.fastaccess._qv_points)
4302
+ >>> points[:] = 0.0
4303
+ >>> model.set_point_fluxes()
4304
+ >>> print_vector(points[:4, 0])
4305
+ 0.0, 0.0, 1.0, 0.0
4306
+ >>> print_vector(points[:4, 1])
4307
+ 0.0, 0.0, 2.0, 0.0
4308
+ """
4309
+ self._set_fluxes(self.numvars.idx_stage, "points")
4310
+
4311
+ def set_result_fluxes(self) -> None:
4312
+ """Save the final fluxes of the actual method.
4313
+
4314
+ >>> from hydpy import print_vector
4315
+ >>> from hydpy.models.test_stiff0d import *
4316
+ >>> parameterstep()
4317
+ >>> fluxes.q = 1.0
4318
+ >>> model.numvars.idx_method = 2
4319
+ >>> results = numpy.asarray(fluxes.fastaccess._q_results)
4320
+ >>> results[:] = 0.0
4321
+ >>> model.set_result_fluxes()
4322
+ >>> from hydpy import round_
4323
+ >>> print_vector(results[:4])
4324
+ 0.0, 0.0, 1.0, 0.0
4325
+
4326
+ >>> from hydpy import reverse_model_wildcard_import
4327
+ >>> reverse_model_wildcard_import()
4328
+ >>> from hydpy.models.test_stiff1d import *
4329
+ >>> parameterstep()
4330
+ >>> n(2)
4331
+ >>> fluxes.qv = 1.0, 2.0
4332
+ >>> model.numvars.idx_method = 2
4333
+ >>> results = numpy.asarray(fluxes.fastaccess._qv_results)
4334
+ >>> results[:] = 0.0
4335
+ >>> model.set_result_fluxes()
4336
+ >>> print_vector(results[:4, 0])
4337
+ 0.0, 0.0, 1.0, 0.0
4338
+ >>> print_vector(results[:4, 1])
4339
+ 0.0, 0.0, 2.0, 0.0
4340
+ """
4341
+ self._set_fluxes(self.numvars.idx_method, "results")
4342
+
4343
+ def _set_fluxes(self, idx: int, type_: str) -> None:
4344
+ fluxes = self.sequences.fluxes
4345
+ for flux in fluxes.numericsequences:
4346
+ temp = getattr(fluxes.fastaccess, f"_{flux.name}_{type_}")
4347
+ temp[idx] = flux
4348
+
4349
+ def integrate_fluxes(self) -> None:
4350
+ """Perform a dot multiplication between the fluxes and the A coefficients
4351
+ associated with the different stages of the actual method.
4352
+
4353
+ >>> from hydpy import print_vector
4354
+ >>> from hydpy.models.test_stiff0d import *
4355
+ >>> parameterstep()
4356
+ >>> model.numvars.idx_method = 2
4357
+ >>> model.numvars.idx_stage = 1
4358
+ >>> model.numvars.dt = 0.5
4359
+ >>> points = numpy.asarray(fluxes.fastaccess._q_points)
4360
+ >>> points[:4] = 15.0, 2.0, -999.0, 0.0
4361
+ >>> model.integrate_fluxes()
4362
+ >>> from hydpy import round_
4363
+ >>> from hydpy import pub
4364
+ >>> print_vector(numpy.asarray(model.numconsts.a_coefs)[1, 1, :2])
4365
+ 0.375, 0.125
4366
+ >>> fluxes.q
4367
+ q(2.9375)
4368
+
4369
+ >>> from hydpy import reverse_model_wildcard_import
4370
+ >>> reverse_model_wildcard_import()
4371
+ >>> from hydpy.models.test_stiff1d import *
4372
+ >>> parameterstep()
4373
+ >>> n(2)
4374
+ >>> model.numvars.idx_method = 2
4375
+ >>> model.numvars.idx_stage = 1
4376
+ >>> model.numvars.dt = 0.5
4377
+ >>> points = numpy.asarray(fluxes.fastaccess._qv_points)
4378
+ >>> points[:4, 0] = 1.0, 1.0, -999.0, 0.0
4379
+ >>> points[:4, 1] = 15.0, 2.0, -999.0, 0.0
4380
+ >>> model.integrate_fluxes()
4381
+ >>> print_vector(numpy.asarray(model.numconsts.a_coefs)[1, 1, :2])
4382
+ 0.375, 0.125
4383
+ >>> fluxes.qv
4384
+ qv(0.25, 2.9375)
4385
+ """
4386
+ fluxes = self.sequences.fluxes
4387
+ for flux in fluxes.numericsequences:
4388
+ points = getattr(fluxes.fastaccess, f"_{flux.name}_points")
4389
+ coefs = self.numconsts.a_coefs[
4390
+ self.numvars.idx_method - 1,
4391
+ self.numvars.idx_stage,
4392
+ : self.numvars.idx_method,
4393
+ ]
4394
+ flux(self.numvars.dt * numpy.dot(coefs, points[: self.numvars.idx_method]))
4395
+
4396
+ def reset_sum_fluxes(self) -> None:
4397
+ """Set the sum of the fluxes calculated so far to zero.
4398
+
4399
+ >>> from hydpy.models.test_stiff0d import *
4400
+ >>> parameterstep()
4401
+ >>> fluxes.fastaccess._q_sum = 5.0
4402
+ >>> model.reset_sum_fluxes()
4403
+ >>> fluxes.fastaccess._q_sum
4404
+ 0.0
4405
+
4406
+ >>> from hydpy import reverse_model_wildcard_import, print_vector
4407
+ >>> reverse_model_wildcard_import()
4408
+ >>> from hydpy.models.test_stiff1d import *
4409
+ >>> parameterstep()
4410
+ >>> n(2)
4411
+ >>> import numpy
4412
+ >>> sums = numpy.asarray(fluxes.fastaccess._qv_sum)
4413
+ >>> sums[:] = 5.0, 5.0
4414
+ >>> model.reset_sum_fluxes()
4415
+ >>> print_vector(fluxes.fastaccess._qv_sum)
4416
+ 0.0, 0.0
4417
+ """
4418
+ fluxes = self.sequences.fluxes
4419
+ for flux in fluxes.numericsequences:
4420
+ if flux.NDIM:
4421
+ getattr(fluxes.fastaccess, f"_{flux.name}_sum")[:] = 0.0
4422
+ else:
4423
+ setattr(fluxes.fastaccess, f"_{flux.name}_sum", 0.0)
4424
+
4425
+ def addup_fluxes(self) -> None:
4426
+ """Add up the sum of the fluxes calculated so far.
4427
+
4428
+ >>> from hydpy.models.test_stiff0d import *
4429
+ >>> parameterstep()
4430
+ >>> fluxes.fastaccess._q_sum = 1.0
4431
+ >>> fluxes.q(2.0)
4432
+ >>> model.addup_fluxes()
4433
+ >>> fluxes.fastaccess._q_sum
4434
+ 3.0
4435
+
4436
+ >>> from hydpy import reverse_model_wildcard_import, print_vector
4437
+ >>> reverse_model_wildcard_import()
4438
+ >>> from hydpy.models.test_stiff1d import *
4439
+ >>> parameterstep()
4440
+ >>> n(2)
4441
+ >>> sums = numpy.asarray(fluxes.fastaccess._qv_sum)
4442
+ >>> sums[:] = 1.0, 2.0
4443
+ >>> fluxes.qv(3.0, 4.0)
4444
+ >>> model.addup_fluxes()
4445
+ >>> print_vector(sums)
4446
+ 4.0, 6.0
4447
+ """
4448
+ fluxes = self.sequences.fluxes
4449
+ for flux in fluxes.numericsequences:
4450
+ sum_ = getattr(fluxes.fastaccess, f"_{flux.name}_sum")
4451
+ sum_ += flux
4452
+ setattr(fluxes.fastaccess, f"_{flux.name}_sum", sum_)
4453
+
4454
+ def calculate_error(self) -> None:
4455
+ """Estimate the numerical error based on the relevant fluxes calculated by the
4456
+ current and the last method.
4457
+
4458
+ "Relevant fluxes" are those contained within the `SOLVERSEQUENCES` tuple. If
4459
+ this tuple is empty, method |ELSModel.calculate_error| selects all flux
4460
+ sequences of the respective model with a |True| `NUMERIC` attribute.
4461
+
4462
+ >>> from hydpy import round_
4463
+ >>> from hydpy.models.test_stiff0d import *
4464
+ >>> parameterstep()
4465
+ >>> results = numpy.asarray(fluxes.fastaccess._q_results)
4466
+ >>> results[:5] = 0.0, 0.0, 3.0, 4.0, 4.0
4467
+ >>> model.numvars.use_relerror = False
4468
+ >>> model.numvars.idx_method = 3
4469
+ >>> model.calculate_error()
4470
+ >>> round_(model.numvars.abserror)
4471
+ 1.0
4472
+ >>> round_(model.numvars.relerror)
4473
+ inf
4474
+
4475
+ >>> model.numvars.use_relerror = True
4476
+ >>> model.calculate_error()
4477
+ >>> round_(model.numvars.abserror)
4478
+ 1.0
4479
+ >>> round_(model.numvars.relerror)
4480
+ 0.25
4481
+
4482
+ >>> model.numvars.idx_method = 4
4483
+ >>> model.calculate_error()
4484
+ >>> round_(model.numvars.abserror)
4485
+ 0.0
4486
+ >>> round_(model.numvars.relerror)
4487
+ 0.0
4488
+
4489
+ >>> model.numvars.idx_method = 1
4490
+ >>> model.calculate_error()
4491
+ >>> round_(model.numvars.abserror)
4492
+ 0.0
4493
+ >>> round_(model.numvars.relerror)
4494
+ inf
4495
+
4496
+ >>> from hydpy import reverse_model_wildcard_import
4497
+ >>> reverse_model_wildcard_import()
4498
+ >>> from hydpy.models.test_stiff1d import *
4499
+ >>> parameterstep()
4500
+ >>> n(2)
4501
+ >>> model.numvars.use_relerror = True
4502
+ >>> model.numvars.idx_method = 3
4503
+ >>> results = numpy.asarray(fluxes.fastaccess._qv_results)
4504
+ >>> results[:5, 0] = 0.0, 0.0, -4.0, -2.0, -2.0
4505
+ >>> results[:5, 1] = 0.0, 0.0, -8.0, -4.0, -4.0
4506
+ >>> model.calculate_error()
4507
+ >>> round_(model.numvars.abserror)
4508
+ 4.0
4509
+ >>> round_(model.numvars.relerror)
4510
+ 1.0
4511
+
4512
+ >>> model.numvars.idx_method = 4
4513
+ >>> model.calculate_error()
4514
+ >>> round_(model.numvars.abserror)
4515
+ 0.0
4516
+ >>> round_(model.numvars.relerror)
4517
+ 0.0
4518
+
4519
+ >>> model.numvars.idx_method = 1
4520
+ >>> model.calculate_error()
4521
+ >>> round_(model.numvars.abserror)
4522
+ 0.0
4523
+ >>> round_(model.numvars.relerror)
4524
+ inf
4525
+ """
4526
+ self.numvars.abserror = 0.0
4527
+ if self.numvars.use_relerror:
4528
+ self.numvars.relerror = 0.0
4529
+ else:
4530
+ self.numvars.relerror = numpy.inf
4531
+ fluxes = self.sequences.fluxes
4532
+ solversequences = self.SOLVERSEQUENCES
4533
+ for flux in fluxes.numericsequences:
4534
+ if solversequences and not isinstance(flux, solversequences):
4535
+ continue
4536
+ results = getattr(fluxes.fastaccess, f"_{flux.name}_results")
4537
+ absdiff = numpy.abs(
4538
+ results[self.numvars.idx_method] - results[self.numvars.idx_method - 1]
4539
+ )
4540
+ try:
4541
+ maxdiff = numpy.max(absdiff)
4542
+ except ValueError:
4543
+ continue
4544
+ self.numvars.abserror = max(self.numvars.abserror, maxdiff)
4545
+ if self.numvars.use_relerror:
4546
+ idxs = results[self.numvars.idx_method] != 0.0
4547
+ if numpy.any(idxs):
4548
+ reldiff = absdiff[idxs] / results[self.numvars.idx_method][idxs]
4549
+ else:
4550
+ reldiff = numpy.inf
4551
+ self.numvars.relerror = max(
4552
+ self.numvars.relerror, numpy.max(numpy.abs(reldiff))
4553
+ )
4554
+
4555
+ def extrapolate_error(self) -> None:
4556
+ """Estimate the numerical error expected when applying all methods available
4557
+ based on the results of the current and the last method.
4558
+
4559
+ Note that you cannot apply this extrapolation strategy to the first method. If
4560
+ the current method is the first one, method |ELSModel.extrapolate_error|
4561
+ returns `-999.9`:
4562
+
4563
+ >>> from hydpy.models.test_stiff0d import *
4564
+ >>> parameterstep()
4565
+ >>> model.numvars.use_relerror = False
4566
+ >>> model.numvars.abserror = 0.01
4567
+ >>> model.numvars.last_abserror = 0.1
4568
+ >>> model.numvars.idx_method = 10
4569
+ >>> model.extrapolate_error()
4570
+ >>> from hydpy import round_
4571
+ >>> round_(model.numvars.extrapolated_abserror)
4572
+ 0.01
4573
+ >>> model.numvars.extrapolated_relerror
4574
+ inf
4575
+
4576
+ >>> model.numvars.use_relerror = True
4577
+ >>> model.numvars.relerror = 0.001
4578
+ >>> model.numvars.last_relerror = 0.01
4579
+ >>> model.extrapolate_error()
4580
+ >>> round_(model.numvars.extrapolated_abserror)
4581
+ 0.01
4582
+ >>> round_(model.numvars.extrapolated_relerror)
4583
+ 0.001
4584
+
4585
+ >>> model.numvars.idx_method = 9
4586
+ >>> model.extrapolate_error()
4587
+ >>> round_(model.numvars.extrapolated_abserror)
4588
+ 0.001
4589
+ >>> round_(model.numvars.extrapolated_relerror)
4590
+ 0.0001
4591
+
4592
+ >>> model.numvars.relerror = inf
4593
+ >>> model.extrapolate_error()
4594
+ >>> round_(model.numvars.extrapolated_relerror)
4595
+ inf
4596
+
4597
+ >>> model.numvars.abserror = 0.0
4598
+ >>> model.extrapolate_error()
4599
+ >>> round_(model.numvars.extrapolated_abserror)
4600
+ 0.0
4601
+ >>> round_(model.numvars.extrapolated_relerror)
4602
+ 0.0
4603
+ """
4604
+ if self.numvars.abserror <= 0.0:
4605
+ self.numvars.extrapolated_abserror = 0.0
4606
+ self.numvars.extrapolated_relerror = 0.0
4607
+ else:
4608
+ if self.numvars.idx_method > 2:
4609
+ self.numvars.extrapolated_abserror = modelutils.exp(
4610
+ modelutils.log(self.numvars.abserror)
4611
+ + (
4612
+ modelutils.log(self.numvars.abserror)
4613
+ - modelutils.log(self.numvars.last_abserror)
4614
+ )
4615
+ * (self.numconsts.nmb_methods - self.numvars.idx_method)
4616
+ )
4617
+ else:
4618
+ self.numvars.extrapolated_abserror = -999.9
4619
+ if self.numvars.use_relerror:
4620
+ if self.numvars.idx_method > 2:
4621
+ if modelutils.isinf(self.numvars.relerror):
4622
+ self.numvars.extrapolated_relerror = modelutils.inf
4623
+ else:
4624
+ self.numvars.extrapolated_relerror = modelutils.exp(
4625
+ modelutils.log(self.numvars.relerror)
4626
+ + (
4627
+ modelutils.log(self.numvars.relerror)
4628
+ - modelutils.log(self.numvars.last_relerror)
4629
+ )
4630
+ * (self.numconsts.nmb_methods - self.numvars.idx_method)
4631
+ )
4632
+ else:
4633
+ self.numvars.extrapolated_relerror = -999.9
4634
+ else:
4635
+ self.numvars.extrapolated_relerror = modelutils.inf
4636
+
4637
+
4638
+ class SubmodelInterface(Model, abc.ABC):
4639
+ """Base class for defining interfaces for submodels."""
4640
+
4641
+ INTERFACE_METHODS: ClassVar[tuple[type[Method], ...]]
4642
+ _submodeladder: importtools.SubmodelAdder | None
4643
+ preparemethod2arguments: dict[str, tuple[tuple[Any, ...], dict[str, Any]]]
4644
+
4645
+ typeid: ClassVar[int]
4646
+ """Type identifier that we use for differentiating submodels that target the same
4647
+ process group (e.g. infiltration) but follow different interfaces.
4648
+
4649
+ For `Submodel_V1`, |SubmodelInterface.typeid| is 1, for `Submodel_V2` 2, and so on.
4650
+
4651
+ We prefer using |SubmodelInterface.typeid| over the standard |isinstance| checks in
4652
+ model equations as it allows releasing Python's Globel Interpreter Lock in Cython.
4653
+ """
4654
+
4655
+ def __init__(self) -> None:
4656
+ super().__init__()
4657
+ self._submodeladder = None
4658
+ self.preparemethod2arguments = {}
4659
+
4660
+ @staticmethod
4661
+ @contextlib.contextmanager
4662
+ def share_configuration( # pylint: disable=unused-argument
4663
+ sharable_configuration: SharableConfiguration,
4664
+ ) -> Generator[None, None, None]:
4665
+ """Share class-level configurations between a main model and a submodel
4666
+ temporarily.
4667
+
4668
+ The default implementation of method |SubmodelInterface.share_configuration|
4669
+ does nothing. Submodels can overwrite it to adjust their classes to the
4670
+ current main model during initialisation.
4671
+ """
4672
+ yield
4673
+
4674
+ def add_mainmodel_as_subsubmodel( # pylint: disable=unused-argument
4675
+ self, mainmodel: Model
4676
+ ) -> bool:
4677
+ """If appropriate, add the given main model as a sub-submodel of the current
4678
+ submodel.
4679
+
4680
+ The default implementation of method
4681
+ |SubmodelInterface.add_mainmodel_as_subsubmodel| just returns |False|.
4682
+ Submodels can overwrite it to enable them to query data from their main models
4683
+ actively. If a submodel accepts a main model as a sub-submodel, it must return
4684
+ |True|; otherwise, |False|.
4685
+ """
4686
+ return False
4687
+
4688
+
4689
+ class SharableSubmodelInterface(SubmodelInterface, abc.ABC):
4690
+ """Base class for defining interfaces for submodels designed as "sharable".
4691
+
4692
+ Currently, |SharableSubmodelInterface| implements no functionality. Its sole
4693
+ purpose is to allow model developers to mark a submodel as sharable, meaning
4694
+ multiple main model instances can share the same submodel instance. It is more of
4695
+ a safety mechanism to prevent reusing submodels that are not designed for this
4696
+ purpose.
4697
+ """
4698
+
4699
+
4700
+ class Submodel:
4701
+ """Base class for implementing "submodels" that serve to deal with (possibly
4702
+ complicated) general mathematical algorithms (e.g. root-finding algorithms) within
4703
+ hydrological model methods.
4704
+
4705
+
4706
+ You might find class |Submodel| useful when trying to implement algorithms
4707
+ requiring some interaction with the respective model without any Python overhead.
4708
+ See the modules |roottools| and `rootutils` as an example, implementing Python
4709
+ interfaces and Cython implementations of a root-finding algorithms, respectively.
4710
+ """
4711
+
4712
+ METHODS: ClassVar[tuple[type[Method], ...]]
4713
+ CYTHONBASECLASS: ClassVar[type[object]]
4714
+ PYTHONCLASS: ClassVar[type[object]]
4715
+ name: ClassVar[str]
4716
+ _cysubmodel: object
4717
+
4718
+ def __init_subclass__(cls) -> None:
4719
+ cls.name = cls.__name__.lower()
4720
+
4721
+ def __init__(self, model: Model) -> None:
4722
+ if model.cymodel:
4723
+ self._cysubmodel = getattr(model.cymodel, self.name)
4724
+ else:
4725
+ self._cysubmodel = self.PYTHONCLASS()
4726
+ for idx, methodtype in enumerate(self.METHODS):
4727
+ setattr(
4728
+ self._cysubmodel,
4729
+ f"method{idx}",
4730
+ getattr(model, methodtype.__name__.lower()),
4731
+ )
4732
+
4733
+
4734
+ class CoupleModels(Protocol[TypeModel_co]):
4735
+ """Specification for defining custom "couple_models" functions to be wrapped by
4736
+ function |define_modelcoupler|."""
4737
+
4738
+ __name__: str
4739
+
4740
+ def __call__(
4741
+ self, *, nodes: devicetools.Nodes, elements: devicetools.Elements
4742
+ ) -> TypeModel_co: ...
4743
+
4744
+
4745
+ def define_modelcoupler(
4746
+ inputtypes: tuple[type[TypeModel_contra], ...], outputtype: type[TypeModel_co]
4747
+ ) -> Callable[
4748
+ [CoupleModels[TypeModel_co]], ModelCoupler[TypeModel_co, TypeModel_contra]
4749
+ ]:
4750
+ """Wrap a model-specific function for creating a composite model based given on
4751
+ |Node| and |Element| objects and their handled "normal" |Model| instances."""
4752
+
4753
+ def _define_modelcoupler(
4754
+ wrapped: CoupleModels[TypeModel_co],
4755
+ ) -> ModelCoupler[TypeModel_co, TypeModel_contra]:
4756
+ return ModelCoupler(
4757
+ inputtypes=inputtypes, outputtype=outputtype, wrapped=wrapped
4758
+ )
4759
+
4760
+ return _define_modelcoupler
4761
+
4762
+
4763
+ class ModelCoupler(Generic[TypeModel_co, TypeModel_contra]):
4764
+ """Wrapper that extends the functionality of model-specific functions for coupling
4765
+ "normal" models to composite models.
4766
+
4767
+ One benefit of using |ModelCoupler| over raw "couple_models" is that it
4768
+ alternatively accepts |Selection| objects instead of |Nodes| and |Elements|
4769
+ objects:
4770
+
4771
+ >>> from hydpy import Element, Elements, Node, Nodes, prepare_model, Selection
4772
+ >>> n12 = Node("n12", variable="LongQ")
4773
+ >>> e1 = Element("e1", outlets=n12)
4774
+ >>> channel1 = prepare_model("sw1d_channel")
4775
+ >>> channel1.parameters.control.nmbsegments(1)
4776
+ >>> with channel1.add_storagemodel_v1("sw1d_storage", position=0, update=False):
4777
+ ... pass
4778
+ >>> with channel1.add_routingmodel_v2("sw1d_lias", position=1, update=False):
4779
+ ... pass
4780
+ >>> e1.model = channel1
4781
+ >>> e2 = Element("e2", inlets=n12)
4782
+ >>> channel2 = prepare_model("sw1d_channel")
4783
+ >>> channel2.parameters.control.nmbsegments(1)
4784
+ >>> with channel2.add_storagemodel_v1("sw1d_storage", position=0, update=False):
4785
+ ... pass
4786
+ >>> e2.model = channel2
4787
+
4788
+ >>> network1 = e1.model.couple_models(nodes=Nodes(n12), elements=Elements(e1, e2))
4789
+ >>> assert network1.storagemodels[0] is channel1.storagemodels[0]
4790
+ >>> assert network1.storagemodels[1] is channel2.storagemodels[0]
4791
+ >>> assert network1.routingmodels[0] is channel1.routingmodels[1]
4792
+ >>> assert network1.storagemodels[0].routingmodelsdownstream.number == 1
4793
+ >>> assert network1.storagemodels[1].routingmodelsupstream.number == 1
4794
+
4795
+ >>> selection = Selection("test", nodes=n12, elements=[e1, e2])
4796
+ >>> network2 = e1.model.couple_models(selection=selection)
4797
+ >>> assert network2.storagemodels[0] is channel1.storagemodels[0]
4798
+ >>> assert network2.storagemodels[1] is channel2.storagemodels[0]
4799
+ >>> assert network2.routingmodels[0] is channel1.routingmodels[1]
4800
+ >>> assert network2.storagemodels[0].routingmodelsdownstream.number == 1
4801
+ >>> assert network2.storagemodels[1].routingmodelsupstream.number == 1
4802
+
4803
+ It additionally checks if the wrapped "couple_models" function supports the types
4804
+ of all passed model instances:
4805
+
4806
+ >>> e3 = Element("e3", inlets="n3_in", outlets="n3_out")
4807
+ >>> e3.model = prepare_model("musk_classic")
4808
+ >>> e1.model.couple_models(nodes=Nodes(n12), elements=Elements(e1, e2, e3))
4809
+ Traceback (most recent call last):
4810
+ ...
4811
+ TypeError: While trying to couple the given model instances to a composite model \
4812
+ of type `sw1d_network` based on function `combine_channels`, the following error \
4813
+ occurred: `musk_classic` of element `e3` is not among the supported model types: \
4814
+ sw1d_channel.
4815
+ """
4816
+
4817
+ _inputtypes: tuple[type[TypeModel_contra], ...]
4818
+ _outputtype: type[TypeModel_co]
4819
+ _wrapped: CoupleModels
4820
+
4821
+ def __init__(
4822
+ self,
4823
+ inputtypes: tuple[type[TypeModel_contra], ...],
4824
+ outputtype: type[TypeModel_co],
4825
+ wrapped: CoupleModels[TypeModel_co],
4826
+ ) -> None:
4827
+ self._inputtypes = inputtypes
4828
+ self._outputtype = outputtype
4829
+ self._wrapped = wrapped
4830
+ functools.update_wrapper(wrapper=self, wrapped=wrapped)
4831
+
4832
+ @overload
4833
+ def __call__(self, *, selection: selectiontools.Selection) -> TypeModel_co: ...
4834
+
4835
+ @overload
4836
+ def __call__(
4837
+ self, *, nodes: devicetools.Nodes, elements: devicetools.Elements
4838
+ ) -> TypeModel_co: ...
4839
+
4840
+ def __call__(
4841
+ self,
4842
+ *,
4843
+ nodes: devicetools.Nodes | None = None,
4844
+ elements: devicetools.Elements | None = None,
4845
+ selection: selectiontools.Selection | None = None,
4846
+ ) -> TypeModel_co:
4847
+ try:
4848
+ if selection is None:
4849
+ assert nodes is not None
4850
+ assert elements is not None
4851
+ else:
4852
+ nodes = selection.nodes
4853
+ elements = selection.elements
4854
+ for element in elements:
4855
+ if not isinstance(element.model, self._inputtypes):
4856
+ modeltypes = (m.__HYDPY_NAME__ for m in self._inputtypes)
4857
+ raise TypeError(
4858
+ f"{objecttools.elementphrase(element.model)} is not among the "
4859
+ f"supported model types: "
4860
+ f"{objecttools.enumeration(modeltypes)}."
4861
+ )
4862
+ return self._wrapped(nodes=nodes, elements=elements)
4863
+ except BaseException:
4864
+ objecttools.augment_excmessage(
4865
+ f"While trying to couple the given model instances to a composite "
4866
+ f"model of type `{self._outputtype.__HYDPY_NAME__}` based on function "
4867
+ f"`{self._wrapped.__name__}`"
4868
+ )