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.
- hydpy/__init__.py +275 -0
- hydpy/aliases.py +2554 -0
- hydpy/auxs/__init__.py +0 -0
- hydpy/auxs/anntools.py +1305 -0
- hydpy/auxs/armatools.py +883 -0
- hydpy/auxs/calibtools.py +3337 -0
- hydpy/auxs/interptools.py +1094 -0
- hydpy/auxs/iuhtools.py +543 -0
- hydpy/auxs/networktools.py +597 -0
- hydpy/auxs/ppolytools.py +809 -0
- hydpy/auxs/quadtools.py +61 -0
- hydpy/auxs/roottools.py +228 -0
- hydpy/auxs/smoothtools.py +273 -0
- hydpy/auxs/statstools.py +2125 -0
- hydpy/auxs/validtools.py +81 -0
- hydpy/conf/HydPyConfigBase.xsd +68637 -0
- hydpy/conf/HydPyConfigBase.xsdt +358 -0
- hydpy/conf/HydPyConfigMultipleRuns.xsd +25 -0
- hydpy/conf/HydPyConfigSingleRun.xsd +24 -0
- hydpy/conf/__init__.py +0 -0
- hydpy/conf/a_coefficients_explicit_lobatto_sequence.npy +0 -0
- hydpy/conf/support_points_for_smoothpar_logistic2.npy +0 -0
- hydpy/config.py +42 -0
- hydpy/core/__init__.py +0 -0
- hydpy/core/aliastools.py +214 -0
- hydpy/core/autodoctools.py +1947 -0
- hydpy/core/auxfiletools.py +1169 -0
- hydpy/core/devicetools.py +3810 -0
- hydpy/core/exceptiontools.py +269 -0
- hydpy/core/filetools.py +1985 -0
- hydpy/core/hydpytools.py +3089 -0
- hydpy/core/importtools.py +1398 -0
- hydpy/core/indextools.py +345 -0
- hydpy/core/itemtools.py +1849 -0
- hydpy/core/masktools.py +460 -0
- hydpy/core/modeltools.py +4868 -0
- hydpy/core/netcdftools.py +2683 -0
- hydpy/core/objecttools.py +2023 -0
- hydpy/core/optiontools.py +1045 -0
- hydpy/core/parametertools.py +4674 -0
- hydpy/core/printtools.py +222 -0
- hydpy/core/propertytools.py +643 -0
- hydpy/core/pubtools.py +254 -0
- hydpy/core/selectiontools.py +1571 -0
- hydpy/core/sequencetools.py +4476 -0
- hydpy/core/seriestools.py +339 -0
- hydpy/core/testtools.py +2483 -0
- hydpy/core/timetools.py +3567 -0
- hydpy/core/typingtools.py +333 -0
- hydpy/core/variabletools.py +2615 -0
- hydpy/cythons/__init__.py +24 -0
- hydpy/cythons/annutils.pxd +33 -0
- hydpy/cythons/annutils.pyi +25 -0
- hydpy/cythons/annutils.pyx +120 -0
- hydpy/cythons/autogen/__init__.py +0 -0
- hydpy/cythons/autogen/annutils.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/annutils.pxd +42 -0
- hydpy/cythons/autogen/annutils.pyx +129 -0
- hydpy/cythons/autogen/c_arma.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_arma.pxd +179 -0
- hydpy/cythons/autogen/c_arma.pyx +356 -0
- hydpy/cythons/autogen/c_arma_rimorido.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_arma_rimorido.pxd +179 -0
- hydpy/cythons/autogen/c_arma_rimorido.pyx +356 -0
- hydpy/cythons/autogen/c_conv.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_conv.pxd +198 -0
- hydpy/cythons/autogen/c_conv.pyx +491 -0
- hydpy/cythons/autogen/c_conv_idw.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_conv_idw.pxd +124 -0
- hydpy/cythons/autogen/c_conv_idw.pyx +264 -0
- hydpy/cythons/autogen/c_conv_idw_ed.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_conv_idw_ed.pxd +197 -0
- hydpy/cythons/autogen/c_conv_idw_ed.pyx +481 -0
- hydpy/cythons/autogen/c_conv_nn.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_conv_nn.pxd +120 -0
- hydpy/cythons/autogen/c_conv_nn.pyx +224 -0
- hydpy/cythons/autogen/c_dam.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam.pxd +805 -0
- hydpy/cythons/autogen/c_dam.pyx +1477 -0
- hydpy/cythons/autogen/c_dam_llake.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_llake.pxd +364 -0
- hydpy/cythons/autogen/c_dam_llake.pyx +705 -0
- hydpy/cythons/autogen/c_dam_lreservoir.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_lreservoir.pxd +365 -0
- hydpy/cythons/autogen/c_dam_lreservoir.pyx +708 -0
- hydpy/cythons/autogen/c_dam_lretention.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_lretention.pxd +340 -0
- hydpy/cythons/autogen/c_dam_lretention.pyx +625 -0
- hydpy/cythons/autogen/c_dam_pump.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_pump.pxd +402 -0
- hydpy/cythons/autogen/c_dam_pump.pyx +724 -0
- hydpy/cythons/autogen/c_dam_pump_sluice.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_pump_sluice.pxd +452 -0
- hydpy/cythons/autogen/c_dam_pump_sluice.pyx +829 -0
- hydpy/cythons/autogen/c_dam_sluice.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_sluice.pxd +404 -0
- hydpy/cythons/autogen/c_dam_sluice.pyx +726 -0
- hydpy/cythons/autogen/c_dam_v001.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_v001.pxd +452 -0
- hydpy/cythons/autogen/c_dam_v001.pyx +816 -0
- hydpy/cythons/autogen/c_dam_v002.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_v002.pxd +394 -0
- hydpy/cythons/autogen/c_dam_v002.pyx +703 -0
- hydpy/cythons/autogen/c_dam_v003.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_v003.pxd +417 -0
- hydpy/cythons/autogen/c_dam_v003.pyx +744 -0
- hydpy/cythons/autogen/c_dam_v004.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_v004.pxd +486 -0
- hydpy/cythons/autogen/c_dam_v004.pyx +891 -0
- hydpy/cythons/autogen/c_dam_v005.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dam_v005.pxd +524 -0
- hydpy/cythons/autogen/c_dam_v005.pyx +928 -0
- hydpy/cythons/autogen/c_dummy.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dummy.pxd +151 -0
- hydpy/cythons/autogen/c_dummy.pyx +263 -0
- hydpy/cythons/autogen/c_dummy_interceptedwater.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dummy_interceptedwater.pxd +69 -0
- hydpy/cythons/autogen/c_dummy_interceptedwater.pyx +104 -0
- hydpy/cythons/autogen/c_dummy_node2node.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dummy_node2node.pxd +89 -0
- hydpy/cythons/autogen/c_dummy_node2node.pyx +148 -0
- hydpy/cythons/autogen/c_dummy_snowalbedo.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dummy_snowalbedo.pxd +69 -0
- hydpy/cythons/autogen/c_dummy_snowalbedo.pyx +104 -0
- hydpy/cythons/autogen/c_dummy_snowcover.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dummy_snowcover.pxd +69 -0
- hydpy/cythons/autogen/c_dummy_snowcover.pyx +104 -0
- hydpy/cythons/autogen/c_dummy_snowycanopy.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dummy_snowycanopy.pxd +69 -0
- hydpy/cythons/autogen/c_dummy_snowycanopy.pyx +104 -0
- hydpy/cythons/autogen/c_dummy_soilwater.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_dummy_soilwater.pxd +69 -0
- hydpy/cythons/autogen/c_dummy_soilwater.pyx +104 -0
- hydpy/cythons/autogen/c_evap.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap.pxd +1029 -0
- hydpy/cythons/autogen/c_evap.pyx +2601 -0
- hydpy/cythons/autogen/c_evap_aet_hbv96.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap_aet_hbv96.pxd +227 -0
- hydpy/cythons/autogen/c_evap_aet_hbv96.pyx +584 -0
- hydpy/cythons/autogen/c_evap_aet_minhas.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap_aet_minhas.pxd +193 -0
- hydpy/cythons/autogen/c_evap_aet_minhas.pyx +478 -0
- hydpy/cythons/autogen/c_evap_aet_morsim.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap_aet_morsim.pxd +681 -0
- hydpy/cythons/autogen/c_evap_aet_morsim.pyx +1642 -0
- hydpy/cythons/autogen/c_evap_pet_ambav1.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap_pet_ambav1.pxd +532 -0
- hydpy/cythons/autogen/c_evap_pet_ambav1.pyx +1296 -0
- hydpy/cythons/autogen/c_evap_pet_hbv96.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap_pet_hbv96.pxd +179 -0
- hydpy/cythons/autogen/c_evap_pet_hbv96.pyx +328 -0
- hydpy/cythons/autogen/c_evap_pet_m.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap_pet_m.pxd +124 -0
- hydpy/cythons/autogen/c_evap_pet_m.pyx +214 -0
- hydpy/cythons/autogen/c_evap_pet_mlc.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap_pet_mlc.pxd +126 -0
- hydpy/cythons/autogen/c_evap_pet_mlc.pyx +214 -0
- hydpy/cythons/autogen/c_evap_ret_fao56.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap_ret_fao56.pxd +305 -0
- hydpy/cythons/autogen/c_evap_ret_fao56.pyx +624 -0
- hydpy/cythons/autogen/c_evap_ret_io.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap_ret_io.pxd +112 -0
- hydpy/cythons/autogen/c_evap_ret_io.pyx +176 -0
- hydpy/cythons/autogen/c_evap_ret_tw2002.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_evap_ret_tw2002.pxd +139 -0
- hydpy/cythons/autogen/c_evap_ret_tw2002.pyx +273 -0
- hydpy/cythons/autogen/c_exch.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_exch.pxd +230 -0
- hydpy/cythons/autogen/c_exch.pyx +462 -0
- hydpy/cythons/autogen/c_exch_branch_hbv96.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_exch_branch_hbv96.pxd +134 -0
- hydpy/cythons/autogen/c_exch_branch_hbv96.pyx +255 -0
- hydpy/cythons/autogen/c_exch_waterlevel.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_exch_waterlevel.pxd +54 -0
- hydpy/cythons/autogen/c_exch_waterlevel.pyx +78 -0
- hydpy/cythons/autogen/c_exch_weir_hbv96.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_exch_weir_hbv96.pxd +156 -0
- hydpy/cythons/autogen/c_exch_weir_hbv96.pyx +282 -0
- hydpy/cythons/autogen/c_ga.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_ga.pxd +353 -0
- hydpy/cythons/autogen/c_ga.pyx +1204 -0
- hydpy/cythons/autogen/c_ga_garto.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_ga_garto.pxd +330 -0
- hydpy/cythons/autogen/c_ga_garto.pyx +1105 -0
- hydpy/cythons/autogen/c_ga_garto_submodel1.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_ga_garto_submodel1.pxd +236 -0
- hydpy/cythons/autogen/c_ga_garto_submodel1.pyx +944 -0
- hydpy/cythons/autogen/c_gland.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_gland.pxd +437 -0
- hydpy/cythons/autogen/c_gland.pyx +726 -0
- hydpy/cythons/autogen/c_gland_gr4.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_gland_gr4.pxd +382 -0
- hydpy/cythons/autogen/c_gland_gr4.pyx +605 -0
- hydpy/cythons/autogen/c_gland_gr5.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_gland_gr5.pxd +368 -0
- hydpy/cythons/autogen/c_gland_gr5.pyx +568 -0
- hydpy/cythons/autogen/c_gland_gr6.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_gland_gr6.pxd +420 -0
- hydpy/cythons/autogen/c_gland_gr6.pyx +673 -0
- hydpy/cythons/autogen/c_hland.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_hland.pxd +855 -0
- hydpy/cythons/autogen/c_hland.pyx +2486 -0
- hydpy/cythons/autogen/c_hland_96.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_hland_96.pxd +631 -0
- hydpy/cythons/autogen/c_hland_96.pyx +1724 -0
- hydpy/cythons/autogen/c_hland_96c.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_hland_96c.pxd +621 -0
- hydpy/cythons/autogen/c_hland_96c.pyx +1822 -0
- hydpy/cythons/autogen/c_hland_96p.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_hland_96p.pxd +683 -0
- hydpy/cythons/autogen/c_hland_96p.pyx +1911 -0
- hydpy/cythons/autogen/c_kinw.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_kinw.pxd +509 -0
- hydpy/cythons/autogen/c_kinw.pyx +965 -0
- hydpy/cythons/autogen/c_kinw_williams.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_kinw_williams.pxd +409 -0
- hydpy/cythons/autogen/c_kinw_williams.pyx +763 -0
- hydpy/cythons/autogen/c_kinw_williams_ext.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_kinw_williams_ext.pxd +220 -0
- hydpy/cythons/autogen/c_kinw_williams_ext.pyx +440 -0
- hydpy/cythons/autogen/c_lland.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_lland.pxd +1386 -0
- hydpy/cythons/autogen/c_lland.pyx +3679 -0
- hydpy/cythons/autogen/c_lland_dd.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_lland_dd.pxd +679 -0
- hydpy/cythons/autogen/c_lland_dd.pyx +1719 -0
- hydpy/cythons/autogen/c_lland_knauf.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_lland_knauf.pxd +1096 -0
- hydpy/cythons/autogen/c_lland_knauf.pyx +2784 -0
- hydpy/cythons/autogen/c_lland_knauf_ic.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_lland_knauf_ic.pxd +1369 -0
- hydpy/cythons/autogen/c_lland_knauf_ic.pyx +3625 -0
- hydpy/cythons/autogen/c_meteo.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_meteo.pxd +469 -0
- hydpy/cythons/autogen/c_meteo.pyx +879 -0
- hydpy/cythons/autogen/c_meteo_clear_glob_io.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_meteo_clear_glob_io.pxd +75 -0
- hydpy/cythons/autogen/c_meteo_clear_glob_io.pyx +107 -0
- hydpy/cythons/autogen/c_meteo_glob_fao56.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_meteo_glob_fao56.pxd +209 -0
- hydpy/cythons/autogen/c_meteo_glob_fao56.pyx +339 -0
- hydpy/cythons/autogen/c_meteo_glob_io.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_meteo_glob_io.pxd +63 -0
- hydpy/cythons/autogen/c_meteo_glob_io.pyx +91 -0
- hydpy/cythons/autogen/c_meteo_glob_morsim.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_meteo_glob_morsim.pxd +289 -0
- hydpy/cythons/autogen/c_meteo_glob_morsim.pyx +527 -0
- hydpy/cythons/autogen/c_meteo_precip_io.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_meteo_precip_io.pxd +112 -0
- hydpy/cythons/autogen/c_meteo_precip_io.pyx +176 -0
- hydpy/cythons/autogen/c_meteo_psun_sun_glob_io.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_meteo_psun_sun_glob_io.pxd +87 -0
- hydpy/cythons/autogen/c_meteo_psun_sun_glob_io.pyx +123 -0
- hydpy/cythons/autogen/c_meteo_sun_fao56.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_meteo_sun_fao56.pxd +209 -0
- hydpy/cythons/autogen/c_meteo_sun_fao56.pyx +343 -0
- hydpy/cythons/autogen/c_meteo_sun_morsim.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_meteo_sun_morsim.pxd +286 -0
- hydpy/cythons/autogen/c_meteo_sun_morsim.pyx +519 -0
- hydpy/cythons/autogen/c_meteo_temp_io.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_meteo_temp_io.pxd +112 -0
- hydpy/cythons/autogen/c_meteo_temp_io.pyx +176 -0
- hydpy/cythons/autogen/c_musk.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_musk.pxd +282 -0
- hydpy/cythons/autogen/c_musk.pyx +605 -0
- hydpy/cythons/autogen/c_musk_classic.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_musk_classic.pxd +138 -0
- hydpy/cythons/autogen/c_musk_classic.pyx +226 -0
- hydpy/cythons/autogen/c_musk_mct.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_musk_mct.pxd +282 -0
- hydpy/cythons/autogen/c_musk_mct.pyx +609 -0
- hydpy/cythons/autogen/c_rconc.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_rconc.pxd +119 -0
- hydpy/cythons/autogen/c_rconc.pyx +174 -0
- hydpy/cythons/autogen/c_rconc_nash.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_rconc_nash.pxd +111 -0
- hydpy/cythons/autogen/c_rconc_nash.pyx +185 -0
- hydpy/cythons/autogen/c_rconc_uh.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_rconc_uh.pxd +92 -0
- hydpy/cythons/autogen/c_rconc_uh.pyx +125 -0
- hydpy/cythons/autogen/c_sw1d.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d.pxd +511 -0
- hydpy/cythons/autogen/c_sw1d.pyx +1263 -0
- hydpy/cythons/autogen/c_sw1d_channel.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d_channel.pxd +119 -0
- hydpy/cythons/autogen/c_sw1d_channel.pyx +300 -0
- hydpy/cythons/autogen/c_sw1d_gate_out.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d_gate_out.pxd +240 -0
- hydpy/cythons/autogen/c_sw1d_gate_out.pyx +476 -0
- hydpy/cythons/autogen/c_sw1d_lias.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d_lias.pxd +320 -0
- hydpy/cythons/autogen/c_sw1d_lias.pyx +619 -0
- hydpy/cythons/autogen/c_sw1d_lias_sluice.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d_lias_sluice.pxd +325 -0
- hydpy/cythons/autogen/c_sw1d_lias_sluice.pyx +644 -0
- hydpy/cythons/autogen/c_sw1d_network.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d_network.pxd +90 -0
- hydpy/cythons/autogen/c_sw1d_network.pyx +246 -0
- hydpy/cythons/autogen/c_sw1d_pump.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d_pump.pxd +256 -0
- hydpy/cythons/autogen/c_sw1d_pump.pyx +502 -0
- hydpy/cythons/autogen/c_sw1d_q_in.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d_q_in.pxd +224 -0
- hydpy/cythons/autogen/c_sw1d_q_in.pyx +383 -0
- hydpy/cythons/autogen/c_sw1d_q_out.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d_q_out.pxd +224 -0
- hydpy/cythons/autogen/c_sw1d_q_out.pyx +383 -0
- hydpy/cythons/autogen/c_sw1d_storage.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d_storage.pxd +193 -0
- hydpy/cythons/autogen/c_sw1d_storage.pyx +349 -0
- hydpy/cythons/autogen/c_sw1d_weir_out.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_sw1d_weir_out.pxd +212 -0
- hydpy/cythons/autogen/c_sw1d_weir_out.pyx +404 -0
- hydpy/cythons/autogen/c_test.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_test.pxd +175 -0
- hydpy/cythons/autogen/c_test.pyx +348 -0
- hydpy/cythons/autogen/c_test_discontinous.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_test_discontinous.pxd +146 -0
- hydpy/cythons/autogen/c_test_discontinous.pyx +256 -0
- hydpy/cythons/autogen/c_test_stiff0d.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_test_stiff0d.pxd +146 -0
- hydpy/cythons/autogen/c_test_stiff0d.pyx +250 -0
- hydpy/cythons/autogen/c_test_stiff1d.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_test_stiff1d.pxd +145 -0
- hydpy/cythons/autogen/c_test_stiff1d.pyx +294 -0
- hydpy/cythons/autogen/c_whmod.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_whmod.pxd +482 -0
- hydpy/cythons/autogen/c_whmod.pyx +1156 -0
- hydpy/cythons/autogen/c_whmod_rural.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_whmod_rural.pxd +411 -0
- hydpy/cythons/autogen/c_whmod_rural.pyx +982 -0
- hydpy/cythons/autogen/c_whmod_urban.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_whmod_urban.pxd +482 -0
- hydpy/cythons/autogen/c_whmod_urban.pyx +1155 -0
- hydpy/cythons/autogen/c_wland.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_wland.pxd +842 -0
- hydpy/cythons/autogen/c_wland.pyx +1890 -0
- hydpy/cythons/autogen/c_wland_gd.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_wland_gd.pxd +829 -0
- hydpy/cythons/autogen/c_wland_gd.pyx +1847 -0
- hydpy/cythons/autogen/c_wland_wag.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_wland_wag.pxd +810 -0
- hydpy/cythons/autogen/c_wland_wag.pyx +1780 -0
- hydpy/cythons/autogen/c_wq.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_wq.pxd +275 -0
- hydpy/cythons/autogen/c_wq.pyx +652 -0
- hydpy/cythons/autogen/c_wq_trapeze.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_wq_trapeze.pxd +170 -0
- hydpy/cythons/autogen/c_wq_trapeze.pyx +400 -0
- hydpy/cythons/autogen/c_wq_trapeze_strickler.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_wq_trapeze_strickler.pxd +243 -0
- hydpy/cythons/autogen/c_wq_trapeze_strickler.pyx +578 -0
- hydpy/cythons/autogen/c_wq_walrus.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/c_wq_walrus.pxd +61 -0
- hydpy/cythons/autogen/c_wq_walrus.pyx +82 -0
- hydpy/cythons/autogen/configutils.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/configutils.pxd +17 -0
- hydpy/cythons/autogen/configutils.pyx +119 -0
- hydpy/cythons/autogen/interfaceutils.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/interfaceutils.pxd +31 -0
- hydpy/cythons/autogen/interfaceutils.pyx +82 -0
- hydpy/cythons/autogen/interputils.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/interputils.pxd +42 -0
- hydpy/cythons/autogen/interputils.pyx +118 -0
- hydpy/cythons/autogen/masterinterface.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/masterinterface.pxd +153 -0
- hydpy/cythons/autogen/masterinterface.pyx +222 -0
- hydpy/cythons/autogen/pointerutils.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/pointerutils.pxd +31 -0
- hydpy/cythons/autogen/pointerutils.pyx +650 -0
- hydpy/cythons/autogen/ppolyutils.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/ppolyutils.pxd +35 -0
- hydpy/cythons/autogen/ppolyutils.pyx +59 -0
- hydpy/cythons/autogen/quadutils.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/quadutils.pxd +26 -0
- hydpy/cythons/autogen/quadutils.pyx +973 -0
- hydpy/cythons/autogen/rootutils.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/rootutils.pxd +28 -0
- hydpy/cythons/autogen/rootutils.pyx +109 -0
- hydpy/cythons/autogen/sequenceutils.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/sequenceutils.pxd +45 -0
- hydpy/cythons/autogen/sequenceutils.pyx +101 -0
- hydpy/cythons/autogen/smoothutils.cp313-win_amd64.pyd +0 -0
- hydpy/cythons/autogen/smoothutils.pxd +29 -0
- hydpy/cythons/autogen/smoothutils.pyx +833 -0
- hydpy/cythons/configutils.pxd +8 -0
- hydpy/cythons/configutils.pyi +5 -0
- hydpy/cythons/configutils.pyx +110 -0
- hydpy/cythons/interfaceutils.pxd +22 -0
- hydpy/cythons/interfaceutils.pyi +15 -0
- hydpy/cythons/interfaceutils.pyx +73 -0
- hydpy/cythons/interputils.pxd +33 -0
- hydpy/cythons/interputils.pyi +32 -0
- hydpy/cythons/interputils.pyx +109 -0
- hydpy/cythons/modelutils.py +2990 -0
- hydpy/cythons/pointerutils.pxd +22 -0
- hydpy/cythons/pointerutils.pyi +89 -0
- hydpy/cythons/pointerutils.pyx +641 -0
- hydpy/cythons/ppolyutils.pxd +26 -0
- hydpy/cythons/ppolyutils.pyi +21 -0
- hydpy/cythons/ppolyutils.pyx +50 -0
- hydpy/cythons/quadutils.pxd +17 -0
- hydpy/cythons/quadutils.pyi +13 -0
- hydpy/cythons/quadutils.pyx +964 -0
- hydpy/cythons/rootutils.pxd +19 -0
- hydpy/cythons/rootutils.pyi +21 -0
- hydpy/cythons/rootutils.pyx +100 -0
- hydpy/cythons/sequenceutils.pxd +36 -0
- hydpy/cythons/sequenceutils.pyi +7 -0
- hydpy/cythons/sequenceutils.pyx +92 -0
- hydpy/cythons/smoothutils.pxd +20 -0
- hydpy/cythons/smoothutils.pyi +15 -0
- hydpy/cythons/smoothutils.pyx +824 -0
- hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/land_dill_assl.py +13 -0
- hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/land_lahn_kalk.py +13 -0
- hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/land_lahn_leun.py +14 -0
- hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/land_lahn_marb.py +13 -0
- hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/stream_dill_assl_lahn_leun.py +5 -0
- hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/stream_lahn_leun_lahn_kalk.py +5 -0
- hydpy/data/HydPy-H-Lahn/conditions/init_1996_01_01_00_00_00/stream_lahn_marb_lahn_leun.py +5 -0
- hydpy/data/HydPy-H-Lahn/control/default/land.py +9 -0
- hydpy/data/HydPy-H-Lahn/control/default/land_dill_assl.py +57 -0
- hydpy/data/HydPy-H-Lahn/control/default/land_lahn_kalk.py +57 -0
- hydpy/data/HydPy-H-Lahn/control/default/land_lahn_leun.py +56 -0
- hydpy/data/HydPy-H-Lahn/control/default/land_lahn_marb.py +57 -0
- hydpy/data/HydPy-H-Lahn/control/default/stream_dill_assl_lahn_leun.py +7 -0
- hydpy/data/HydPy-H-Lahn/control/default/stream_lahn_leun_lahn_kalk.py +7 -0
- hydpy/data/HydPy-H-Lahn/control/default/stream_lahn_marb_lahn_leun.py +7 -0
- hydpy/data/HydPy-H-Lahn/multiple_runs.xml +309 -0
- hydpy/data/HydPy-H-Lahn/multiple_runs_alpha.xml +71 -0
- hydpy/data/HydPy-H-Lahn/network/default/headwaters.py +11 -0
- hydpy/data/HydPy-H-Lahn/network/default/nonheadwaters.py +11 -0
- hydpy/data/HydPy-H-Lahn/network/default/streams.py +8 -0
- hydpy/data/HydPy-H-Lahn/series/default/dill_assl_obs_q.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/evap_pet_hbv96_input_normalairtemperature.nc +0 -0
- hydpy/data/HydPy-H-Lahn/series/default/evap_pet_hbv96_input_normalevapotranspiration.nc +0 -0
- hydpy/data/HydPy-H-Lahn/series/default/hland_96_input_p.nc +0 -0
- hydpy/data/HydPy-H-Lahn/series/default/hland_96_input_t.nc +0 -0
- hydpy/data/HydPy-H-Lahn/series/default/lahn_kalk_obs_q.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/lahn_leun_obs_q.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/lahn_marb_obs_q.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_dill_assl_evap_pet_hbv96_input_normalairtemperature.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_dill_assl_evap_pet_hbv96_input_normalevapotranspiration.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_dill_assl_hland_96_input_p.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_dill_assl_hland_96_input_t.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_kalk_evap_pet_hbv96_input_normalairtemperature.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_kalk_evap_pet_hbv96_input_normalevapotranspiration.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_kalk_hland_96_input_p.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_kalk_hland_96_input_t.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_leun_evap_pet_hbv96_input_normalairtemperature.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_leun_evap_pet_hbv96_input_normalevapotranspiration.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_leun_hland_96_input_p.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_leun_hland_96_input_t.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_marb_evap_pet_hbv96_input_normalairtemperature.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_marb_evap_pet_hbv96_input_normalevapotranspiration.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_marb_hland_96_input_p.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/land_lahn_marb_hland_96_input_t.asc +11387 -0
- hydpy/data/HydPy-H-Lahn/series/default/obs_q.nc +0 -0
- hydpy/data/HydPy-H-Lahn/single_run.xml +152 -0
- hydpy/data/HydPy-H-Lahn/single_run.xmlt +143 -0
- hydpy/data/__init__.py +17 -0
- hydpy/docs/__init__.py +0 -0
- hydpy/docs/autofigs/__init__.py +0 -0
- hydpy/docs/bib/__init__.py +0 -0
- hydpy/docs/bib/refs.bib +566 -0
- hydpy/docs/combine_docversions.py +133 -0
- hydpy/docs/draw_model_sketches.py +1301 -0
- hydpy/docs/enable_autodoc.py +7 -0
- hydpy/docs/figs/HydPy-G-GR4.png +0 -0
- hydpy/docs/figs/HydPy-G-GR5.png +0 -0
- hydpy/docs/figs/HydPy-G-GR6.png +0 -0
- hydpy/docs/figs/HydPy-H-HBV96-COSERO.png +0 -0
- hydpy/docs/figs/HydPy-H-HBV96-PREVAH.png +0 -0
- hydpy/docs/figs/HydPy-H-HBV96.png +0 -0
- hydpy/docs/figs/HydPy-H-Lahn.png +0 -0
- hydpy/docs/figs/HydPy-KinW-Williams.png +0 -0
- hydpy/docs/figs/HydPy-L-DD.png +0 -0
- hydpy/docs/figs/HydPy-W-Wag.png +0 -0
- hydpy/docs/figs/HydPy_Logo.png +0 -0
- hydpy/docs/figs/HydPy_Logo_Text.png +0 -0
- hydpy/docs/figs/IDLE-editor.png +0 -0
- hydpy/docs/figs/IDLE-shell.png +0 -0
- hydpy/docs/figs/LAWA_river-basin-bumbers.png +0 -0
- hydpy/docs/figs/__init__.py +0 -0
- hydpy/docs/html_/__init__.py +0 -0
- hydpy/docs/polish_html.py +57 -0
- hydpy/docs/prepare.py +224 -0
- hydpy/docs/publish_docs.py +53 -0
- hydpy/docs/rst/HydPy-ARMA.rst +27 -0
- hydpy/docs/rst/HydPy-Conv.rst +22 -0
- hydpy/docs/rst/HydPy-Dam.rst +79 -0
- hydpy/docs/rst/HydPy-Dummy.rst +21 -0
- hydpy/docs/rst/HydPy-Evap.rst +26 -0
- hydpy/docs/rst/HydPy-Exch.rst +36 -0
- hydpy/docs/rst/HydPy-G.rst +40 -0
- hydpy/docs/rst/HydPy-GA.rst +34 -0
- hydpy/docs/rst/HydPy-H.rst +24 -0
- hydpy/docs/rst/HydPy-KinW.rst +32 -0
- hydpy/docs/rst/HydPy-L.rst +42 -0
- hydpy/docs/rst/HydPy-Meteo.rst +28 -0
- hydpy/docs/rst/HydPy-Musk.rst +21 -0
- hydpy/docs/rst/HydPy-Rconc.rst +17 -0
- hydpy/docs/rst/HydPy-SW1D.rst +49 -0
- hydpy/docs/rst/HydPy-Test.rst +19 -0
- hydpy/docs/rst/HydPy-W.rst +20 -0
- hydpy/docs/rst/HydPy-WHMod.rst +19 -0
- hydpy/docs/rst/HydPy-WQ.rst +20 -0
- hydpy/docs/rst/__init__.py +0 -0
- hydpy/docs/rst/additional_repositories.rst +40 -0
- hydpy/docs/rst/auxiliaries.rst +31 -0
- hydpy/docs/rst/continuous_integration.rst +75 -0
- hydpy/docs/rst/core.rst +75 -0
- hydpy/docs/rst/cythons.rst +47 -0
- hydpy/docs/rst/definitions.rst +506 -0
- hydpy/docs/rst/developer_guide.rst +54 -0
- hydpy/docs/rst/example_projects.rst +40 -0
- hydpy/docs/rst/execution.rst +22 -0
- hydpy/docs/rst/framework_tools.rst +56 -0
- hydpy/docs/rst/how_to_read_the_reference_manual.rst +156 -0
- hydpy/docs/rst/hydpydependencies.rst +55 -0
- hydpy/docs/rst/index.rst +125 -0
- hydpy/docs/rst/installation.rst +155 -0
- hydpy/docs/rst/model_families.rst +79 -0
- hydpy/docs/rst/model_overview.rst +291 -0
- hydpy/docs/rst/modelimports.rst +10 -0
- hydpy/docs/rst/options.rst +119 -0
- hydpy/docs/rst/programming_style.rst +572 -0
- hydpy/docs/rst/project_structure.rst +520 -0
- hydpy/docs/rst/quickstart.rst +304 -0
- hydpy/docs/rst/reference_manual.rst +29 -0
- hydpy/docs/rst/required_tools.rst +50 -0
- hydpy/docs/rst/simulation.rst +514 -0
- hydpy/docs/rst/submodel_interfaces.rst +32 -0
- hydpy/docs/rst/tests_and_documentation.rst +85 -0
- hydpy/docs/rst/user_guide.rst +38 -0
- hydpy/docs/rst/version_control.rst +116 -0
- hydpy/docs/rst/zbibliography.rst +8 -0
- hydpy/docs/sphinx/__init__.py +0 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/changes/frameset.html +11 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/changes/rstsource.html +15 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/changes/versionchanges.html +33 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/defindex.html +35 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/domainindex.html +56 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/genindex-single.html +63 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/genindex-split.html +41 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/genindex.html +76 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/globaltoc.html +11 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/layout.html +221 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/localtoc.html +15 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/opensearch.xml +13 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/page.html +13 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/relations.html +23 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/search.html +65 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/searchbox.html +21 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/searchfield.html +23 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/sourcelink.html +18 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/static/basic.css_t +925 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/static/doctools.js +156 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/static/documentation_options.js_t +13 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/static/file.png +0 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/static/language_data.js_t +26 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/static/minus.png +0 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/static/plus.png +0 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/static/searchtools.js +574 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/static/sphinx_highlight.js +154 -0
- hydpy/docs/sphinx/_themes/basic_hydpy/theme.conf +16 -0
- hydpy/docs/sphinx/_themes/classic_hydpy/layout.html +23 -0
- hydpy/docs/sphinx/_themes/classic_hydpy/static/classic.css_t +358 -0
- hydpy/docs/sphinx/_themes/classic_hydpy/static/sidebar.js_t +72 -0
- hydpy/docs/sphinx/_themes/classic_hydpy/theme.conf +32 -0
- hydpy/docs/sphinx/conf.py +398 -0
- hydpy/docs/sphinx/defaultlinks_extension.py +36 -0
- hydpy/docs/sphinx/integrationtest_extension.py +104 -0
- hydpy/docs/sphinx/projectstructure_extension.py +58 -0
- hydpy/docs/sphinx/submodelgraph_extension.py +53 -0
- hydpy/exe/__init__.py +0 -0
- hydpy/exe/commandtools.py +651 -0
- hydpy/exe/hyd.py +277 -0
- hydpy/exe/modelimports.py +41 -0
- hydpy/exe/replacetools.py +216 -0
- hydpy/exe/servertools.py +2348 -0
- hydpy/exe/xmltools.py +3280 -0
- hydpy/interfaces/__init__.py +0 -0
- hydpy/interfaces/aetinterfaces.py +94 -0
- hydpy/interfaces/dischargeinterfaces.py +45 -0
- hydpy/interfaces/petinterfaces.py +117 -0
- hydpy/interfaces/precipinterfaces.py +42 -0
- hydpy/interfaces/radiationinterfaces.py +79 -0
- hydpy/interfaces/rconcinterfaces.py +30 -0
- hydpy/interfaces/routinginterfaces.py +324 -0
- hydpy/interfaces/soilinterfaces.py +96 -0
- hydpy/interfaces/stateinterfaces.py +98 -0
- hydpy/interfaces/tempinterfaces.py +46 -0
- hydpy/models/__init__.py +0 -0
- hydpy/models/arma/__init__.py +14 -0
- hydpy/models/arma/arma_control.py +383 -0
- hydpy/models/arma/arma_derived.py +204 -0
- hydpy/models/arma/arma_fluxes.py +41 -0
- hydpy/models/arma/arma_inlets.py +11 -0
- hydpy/models/arma/arma_logs.py +19 -0
- hydpy/models/arma/arma_model.py +461 -0
- hydpy/models/arma/arma_outlets.py +11 -0
- hydpy/models/arma_rimorido.py +381 -0
- hydpy/models/conv/__init__.py +12 -0
- hydpy/models/conv/conv_control.py +303 -0
- hydpy/models/conv/conv_derived.py +271 -0
- hydpy/models/conv/conv_fluxes.py +54 -0
- hydpy/models/conv/conv_inlets.py +11 -0
- hydpy/models/conv/conv_model.py +687 -0
- hydpy/models/conv/conv_outlets.py +11 -0
- hydpy/models/conv_idw.py +120 -0
- hydpy/models/conv_idw_ed.py +184 -0
- hydpy/models/conv_nn.py +112 -0
- hydpy/models/dam/__init__.py +16 -0
- hydpy/models/dam/dam_aides.py +17 -0
- hydpy/models/dam/dam_control.py +346 -0
- hydpy/models/dam/dam_derived.py +559 -0
- hydpy/models/dam/dam_factors.py +46 -0
- hydpy/models/dam/dam_fluxes.py +179 -0
- hydpy/models/dam/dam_inlets.py +29 -0
- hydpy/models/dam/dam_logs.py +52 -0
- hydpy/models/dam/dam_model.py +5011 -0
- hydpy/models/dam/dam_outlets.py +23 -0
- hydpy/models/dam/dam_receivers.py +41 -0
- hydpy/models/dam/dam_senders.py +23 -0
- hydpy/models/dam/dam_solver.py +75 -0
- hydpy/models/dam/dam_states.py +11 -0
- hydpy/models/dam_llake.py +499 -0
- hydpy/models/dam_lreservoir.py +548 -0
- hydpy/models/dam_lretention.py +343 -0
- hydpy/models/dam_pump.py +278 -0
- hydpy/models/dam_pump_sluice.py +339 -0
- hydpy/models/dam_sluice.py +319 -0
- hydpy/models/dam_v001.py +1127 -0
- hydpy/models/dam_v002.py +381 -0
- hydpy/models/dam_v003.py +422 -0
- hydpy/models/dam_v004.py +665 -0
- hydpy/models/dam_v005.py +479 -0
- hydpy/models/dummy/__init__.py +15 -0
- hydpy/models/dummy/dummy_control.py +22 -0
- hydpy/models/dummy/dummy_fluxes.py +11 -0
- hydpy/models/dummy/dummy_inlets.py +11 -0
- hydpy/models/dummy/dummy_inputs.py +41 -0
- hydpy/models/dummy/dummy_model.py +196 -0
- hydpy/models/dummy/dummy_outlets.py +11 -0
- hydpy/models/dummy_interceptedwater.py +85 -0
- hydpy/models/dummy_node2node.py +83 -0
- hydpy/models/dummy_snowalbedo.py +84 -0
- hydpy/models/dummy_snowcover.py +84 -0
- hydpy/models/dummy_snowycanopy.py +86 -0
- hydpy/models/dummy_soilwater.py +85 -0
- hydpy/models/evap/__init__.py +13 -0
- hydpy/models/evap/evap_control.py +354 -0
- hydpy/models/evap/evap_derived.py +236 -0
- hydpy/models/evap/evap_factors.py +188 -0
- hydpy/models/evap/evap_fixed.py +68 -0
- hydpy/models/evap/evap_fluxes.py +150 -0
- hydpy/models/evap/evap_inputs.py +54 -0
- hydpy/models/evap/evap_logs.py +91 -0
- hydpy/models/evap/evap_masks.py +48 -0
- hydpy/models/evap/evap_model.py +9170 -0
- hydpy/models/evap/evap_parameters.py +149 -0
- hydpy/models/evap/evap_sequences.py +32 -0
- hydpy/models/evap/evap_states.py +18 -0
- hydpy/models/evap_aet_hbv96.py +372 -0
- hydpy/models/evap_aet_minhas.py +331 -0
- hydpy/models/evap_aet_morsim.py +627 -0
- hydpy/models/evap_pet_ambav1.py +483 -0
- hydpy/models/evap_pet_hbv96.py +147 -0
- hydpy/models/evap_pet_m.py +94 -0
- hydpy/models/evap_pet_mlc.py +107 -0
- hydpy/models/evap_ret_fao56.py +265 -0
- hydpy/models/evap_ret_io.py +74 -0
- hydpy/models/evap_ret_tw2002.py +165 -0
- hydpy/models/exch/__init__.py +14 -0
- hydpy/models/exch/exch_control.py +262 -0
- hydpy/models/exch/exch_derived.py +36 -0
- hydpy/models/exch/exch_factors.py +26 -0
- hydpy/models/exch/exch_fluxes.py +48 -0
- hydpy/models/exch/exch_inlets.py +11 -0
- hydpy/models/exch/exch_logs.py +12 -0
- hydpy/models/exch/exch_model.py +451 -0
- hydpy/models/exch/exch_outlets.py +17 -0
- hydpy/models/exch/exch_receivers.py +17 -0
- hydpy/models/exch_branch_hbv96.py +186 -0
- hydpy/models/exch_waterlevel.py +73 -0
- hydpy/models/exch_weir_hbv96.py +609 -0
- hydpy/models/ga/__init__.py +14 -0
- hydpy/models/ga/ga_aides.py +17 -0
- hydpy/models/ga/ga_control.py +208 -0
- hydpy/models/ga/ga_derived.py +77 -0
- hydpy/models/ga/ga_fluxes.py +83 -0
- hydpy/models/ga/ga_inputs.py +26 -0
- hydpy/models/ga/ga_logs.py +17 -0
- hydpy/models/ga/ga_model.py +2952 -0
- hydpy/models/ga/ga_states.py +87 -0
- hydpy/models/ga_garto.py +1001 -0
- hydpy/models/ga_garto_submodel1.py +79 -0
- hydpy/models/gland/__init__.py +14 -0
- hydpy/models/gland/gland_control.py +90 -0
- hydpy/models/gland/gland_derived.py +113 -0
- hydpy/models/gland/gland_fluxes.py +137 -0
- hydpy/models/gland/gland_inputs.py +12 -0
- hydpy/models/gland/gland_model.py +1439 -0
- hydpy/models/gland/gland_outlets.py +11 -0
- hydpy/models/gland/gland_states.py +90 -0
- hydpy/models/gland_gr4.py +501 -0
- hydpy/models/gland_gr5.py +463 -0
- hydpy/models/gland_gr6.py +487 -0
- hydpy/models/hland/__init__.py +20 -0
- hydpy/models/hland/hland_aides.py +19 -0
- hydpy/models/hland/hland_constants.py +37 -0
- hydpy/models/hland/hland_control.py +1530 -0
- hydpy/models/hland/hland_derived.py +683 -0
- hydpy/models/hland/hland_factors.py +57 -0
- hydpy/models/hland/hland_fixed.py +42 -0
- hydpy/models/hland/hland_fluxes.py +279 -0
- hydpy/models/hland/hland_inputs.py +19 -0
- hydpy/models/hland/hland_masks.py +107 -0
- hydpy/models/hland/hland_model.py +4664 -0
- hydpy/models/hland/hland_outlets.py +11 -0
- hydpy/models/hland/hland_parameters.py +227 -0
- hydpy/models/hland/hland_sequences.py +382 -0
- hydpy/models/hland/hland_states.py +236 -0
- hydpy/models/hland_96.py +1812 -0
- hydpy/models/hland_96c.py +1196 -0
- hydpy/models/hland_96p.py +1204 -0
- hydpy/models/kinw/__init__.py +18 -0
- hydpy/models/kinw/kinw_aides.py +306 -0
- hydpy/models/kinw/kinw_control.py +270 -0
- hydpy/models/kinw/kinw_derived.py +197 -0
- hydpy/models/kinw/kinw_fixed.py +33 -0
- hydpy/models/kinw/kinw_fluxes.py +37 -0
- hydpy/models/kinw/kinw_inlets.py +11 -0
- hydpy/models/kinw/kinw_model.py +3026 -0
- hydpy/models/kinw/kinw_outlets.py +11 -0
- hydpy/models/kinw/kinw_solver.py +45 -0
- hydpy/models/kinw/kinw_states.py +17 -0
- hydpy/models/kinw_williams.py +1299 -0
- hydpy/models/kinw_williams_ext.py +768 -0
- hydpy/models/lland/__init__.py +42 -0
- hydpy/models/lland/lland_aides.py +38 -0
- hydpy/models/lland/lland_constants.py +88 -0
- hydpy/models/lland/lland_control.py +1329 -0
- hydpy/models/lland/lland_derived.py +380 -0
- hydpy/models/lland/lland_factors.py +18 -0
- hydpy/models/lland/lland_fixed.py +128 -0
- hydpy/models/lland/lland_fluxes.py +626 -0
- hydpy/models/lland/lland_inlets.py +12 -0
- hydpy/models/lland/lland_inputs.py +33 -0
- hydpy/models/lland/lland_logs.py +17 -0
- hydpy/models/lland/lland_masks.py +212 -0
- hydpy/models/lland/lland_model.py +7690 -0
- hydpy/models/lland/lland_outlets.py +12 -0
- hydpy/models/lland/lland_parameters.py +195 -0
- hydpy/models/lland/lland_sequences.py +67 -0
- hydpy/models/lland/lland_states.py +280 -0
- hydpy/models/lland_dd.py +2270 -0
- hydpy/models/lland_knauf.py +2156 -0
- hydpy/models/lland_knauf_ic.py +1920 -0
- hydpy/models/meteo/__init__.py +12 -0
- hydpy/models/meteo/meteo_control.py +154 -0
- hydpy/models/meteo/meteo_derived.py +159 -0
- hydpy/models/meteo/meteo_factors.py +88 -0
- hydpy/models/meteo/meteo_fixed.py +19 -0
- hydpy/models/meteo/meteo_fluxes.py +46 -0
- hydpy/models/meteo/meteo_inputs.py +47 -0
- hydpy/models/meteo/meteo_logs.py +30 -0
- hydpy/models/meteo/meteo_model.py +2904 -0
- hydpy/models/meteo/meteo_parameters.py +14 -0
- hydpy/models/meteo/meteo_sequences.py +22 -0
- hydpy/models/meteo_clear_glob_io.py +77 -0
- hydpy/models/meteo_glob_fao56.py +217 -0
- hydpy/models/meteo_glob_io.py +68 -0
- hydpy/models/meteo_glob_morsim.py +444 -0
- hydpy/models/meteo_precip_io.py +76 -0
- hydpy/models/meteo_psun_sun_glob_io.py +83 -0
- hydpy/models/meteo_sun_fao56.py +188 -0
- hydpy/models/meteo_sun_morsim.py +466 -0
- hydpy/models/meteo_temp_io.py +76 -0
- hydpy/models/musk/__init__.py +15 -0
- hydpy/models/musk/musk_control.py +328 -0
- hydpy/models/musk/musk_derived.py +32 -0
- hydpy/models/musk/musk_factors.py +53 -0
- hydpy/models/musk/musk_fluxes.py +24 -0
- hydpy/models/musk/musk_inlets.py +11 -0
- hydpy/models/musk/musk_masks.py +15 -0
- hydpy/models/musk/musk_model.py +838 -0
- hydpy/models/musk/musk_outlets.py +11 -0
- hydpy/models/musk/musk_sequences.py +88 -0
- hydpy/models/musk/musk_solver.py +68 -0
- hydpy/models/musk/musk_states.py +64 -0
- hydpy/models/musk_classic.py +228 -0
- hydpy/models/musk_mct.py +1247 -0
- hydpy/models/rconc/__init__.py +12 -0
- hydpy/models/rconc/rconc_control.py +473 -0
- hydpy/models/rconc/rconc_derived.py +76 -0
- hydpy/models/rconc/rconc_fluxes.py +19 -0
- hydpy/models/rconc/rconc_logs.py +74 -0
- hydpy/models/rconc/rconc_model.py +260 -0
- hydpy/models/rconc/rconc_states.py +11 -0
- hydpy/models/rconc_nash.py +48 -0
- hydpy/models/rconc_uh.py +53 -0
- hydpy/models/sw1d/__init__.py +17 -0
- hydpy/models/sw1d/sw1d_control.py +356 -0
- hydpy/models/sw1d/sw1d_derived.py +85 -0
- hydpy/models/sw1d/sw1d_factors.py +78 -0
- hydpy/models/sw1d/sw1d_fixed.py +12 -0
- hydpy/models/sw1d/sw1d_fluxes.py +55 -0
- hydpy/models/sw1d/sw1d_inlets.py +17 -0
- hydpy/models/sw1d/sw1d_model.py +3385 -0
- hydpy/models/sw1d/sw1d_outlets.py +11 -0
- hydpy/models/sw1d/sw1d_receivers.py +11 -0
- hydpy/models/sw1d/sw1d_senders.py +11 -0
- hydpy/models/sw1d/sw1d_states.py +23 -0
- hydpy/models/sw1d_channel.py +2051 -0
- hydpy/models/sw1d_gate_out.py +599 -0
- hydpy/models/sw1d_lias.py +105 -0
- hydpy/models/sw1d_lias_sluice.py +531 -0
- hydpy/models/sw1d_network.py +1219 -0
- hydpy/models/sw1d_pump.py +448 -0
- hydpy/models/sw1d_q_in.py +79 -0
- hydpy/models/sw1d_q_out.py +81 -0
- hydpy/models/sw1d_storage.py +78 -0
- hydpy/models/sw1d_weir_out.py +75 -0
- hydpy/models/test/__init__.py +14 -0
- hydpy/models/test/test_control.py +28 -0
- hydpy/models/test/test_fluxes.py +17 -0
- hydpy/models/test/test_model.py +201 -0
- hydpy/models/test/test_solver.py +48 -0
- hydpy/models/test/test_states.py +17 -0
- hydpy/models/test_discontinous.py +46 -0
- hydpy/models/test_stiff0d.py +47 -0
- hydpy/models/test_stiff1d.py +42 -0
- hydpy/models/whmod/__init__.py +21 -0
- hydpy/models/whmod/whmod_constants.py +77 -0
- hydpy/models/whmod/whmod_control.py +333 -0
- hydpy/models/whmod/whmod_derived.py +210 -0
- hydpy/models/whmod/whmod_factors.py +9 -0
- hydpy/models/whmod/whmod_fluxes.py +105 -0
- hydpy/models/whmod/whmod_inputs.py +15 -0
- hydpy/models/whmod/whmod_masks.py +178 -0
- hydpy/models/whmod/whmod_model.py +2091 -0
- hydpy/models/whmod/whmod_parameters.py +155 -0
- hydpy/models/whmod/whmod_sequences.py +193 -0
- hydpy/models/whmod/whmod_states.py +73 -0
- hydpy/models/whmod_rural.py +794 -0
- hydpy/models/whmod_urban.py +1011 -0
- hydpy/models/wland/__init__.py +43 -0
- hydpy/models/wland/wland_aides.py +55 -0
- hydpy/models/wland/wland_constants.py +103 -0
- hydpy/models/wland/wland_control.py +508 -0
- hydpy/models/wland/wland_derived.py +330 -0
- hydpy/models/wland/wland_factors.py +11 -0
- hydpy/models/wland/wland_fixed.py +12 -0
- hydpy/models/wland/wland_fluxes.py +166 -0
- hydpy/models/wland/wland_inputs.py +33 -0
- hydpy/models/wland/wland_masks.py +54 -0
- hydpy/models/wland/wland_model.py +3755 -0
- hydpy/models/wland/wland_outlets.py +11 -0
- hydpy/models/wland/wland_parameters.py +214 -0
- hydpy/models/wland/wland_sequences.py +108 -0
- hydpy/models/wland/wland_solver.py +45 -0
- hydpy/models/wland/wland_states.py +56 -0
- hydpy/models/wland_gd.py +888 -0
- hydpy/models/wland_wag.py +1244 -0
- hydpy/models/wq/__init__.py +14 -0
- hydpy/models/wq/wq_control.py +117 -0
- hydpy/models/wq/wq_derived.py +182 -0
- hydpy/models/wq/wq_factors.py +79 -0
- hydpy/models/wq/wq_fluxes.py +17 -0
- hydpy/models/wq/wq_model.py +1889 -0
- hydpy/models/wq_trapeze.py +168 -0
- hydpy/models/wq_trapeze_strickler.py +101 -0
- hydpy/models/wq_walrus.py +57 -0
- hydpy/py.typed +0 -0
- hydpy/tests/.coveragerc +22 -0
- hydpy/tests/__init__.py +0 -0
- hydpy/tests/check_consistency.py +32 -0
- hydpy/tests/hydpydoctestcustomize.pth +1 -0
- hydpy/tests/hydpydoctestcustomize.py +15 -0
- hydpy/tests/iotesting/__init__.py +0 -0
- hydpy/tests/run_doctests.py +233 -0
- hydpy-6.2.dev1.data/scripts/hyd.py +277 -0
- hydpy-6.2.dev1.dist-info/LICENSE +165 -0
- hydpy-6.2.dev1.dist-info/METADATA +163 -0
- hydpy-6.2.dev1.dist-info/RECORD +890 -0
- hydpy-6.2.dev1.dist-info/WHEEL +5 -0
- hydpy-6.2.dev1.dist-info/licenses_hydpy_installer.txt +42 -0
- hydpy-6.2.dev1.dist-info/top_level.txt +1 -0
hydpy/core/modeltools.py
ADDED
|
@@ -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
|
+
)
|