From e7a3aec02ff8c4476b06bd1795b6a83c20e5a7ff Mon Sep 17 00:00:00 2001 From: Jiang Date: Tue, 3 Mar 2026 09:47:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0native.api=E6=BA=90=E7=A0=81?= =?UTF-8?q?=EF=BC=9B=E4=B8=B4=E6=97=B6=E5=A4=84=E7=90=86run=5Fsimulation?= =?UTF-8?q?=E4=B8=ADiot=E6=95=B0=E6=8D=AE=E5=BA=93name=E7=9A=84=E5=88=A4?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/algorithms/api_ex/run_simulation.py | 2 +- app/native/api/__init__.py | 175 +++++++ app/native/api/batch_api.py | 53 ++ app/native/api/batch_api_cs.py | 238 +++++++++ app/native/api/batch_exe.py | 380 ++++++++++++++ app/native/api/clean_api.py | 45 ++ app/native/api/connection.py | 3 + app/native/api/database.py | 349 +++++++++++++ app/native/api/extension_data.py | 62 +++ app/native/api/inp_in.py | 428 ++++++++++++++++ app/native/api/inp_out.py | 287 +++++++++++ app/native/api/postgresql_info.py | 36 ++ app/native/api/project.py | 192 ++++++++ app/native/api/project_backup.py | 183 +++++++ app/native/api/s0_base.py | 262 ++++++++++ app/native/api/s10_status.py | 110 +++++ app/native/api/s11_patterns.py | 163 ++++++ app/native/api/s12_curves.py | 186 +++++++ app/native/api/s13_controls.py | 52 ++ app/native/api/s14_rules.py | 48 ++ app/native/api/s15_energy.py | 240 +++++++++ app/native/api/s16_emitters.py | 98 ++++ app/native/api/s17_quality.py | 95 ++++ app/native/api/s18_sources.py | 153 ++++++ app/native/api/s19_reactions.py | 263 ++++++++++ app/native/api/s1_title.py | 40 ++ app/native/api/s20_mixing.py | 150 ++++++ app/native/api/s21_times.py | 112 +++++ app/native/api/s22_report.py | 34 ++ app/native/api/s23_options.py | 81 +++ app/native/api/s23_options_util.py | 401 +++++++++++++++ app/native/api/s23_options_v3.py | 79 +++ app/native/api/s24_coordinates.py | 92 ++++ app/native/api/s25_vertices.py | 120 +++++ app/native/api/s26_labels.py | 137 ++++++ app/native/api/s27_backdrop.py | 39 ++ app/native/api/s28_end.py | 0 app/native/api/s29_scada_device.py | 123 +++++ app/native/api/s2_junctions.py | 191 ++++++++ app/native/api/s30_scada_device_data.py | 90 ++++ app/native/api/s31_scada_element.py | 197 ++++++++ app/native/api/s32_region.py | 95 ++++ app/native/api/s32_region_util.py | 463 ++++++++++++++++++ app/native/api/s33_dma.py | 230 +++++++++ app/native/api/s33_dma_cal.py | 152 ++++++ app/native/api/s33_dma_gen.py | 47 ++ app/native/api/s34_sa.py | 217 ++++++++ app/native/api/s34_sa_cal.py | 198 ++++++++ app/native/api/s34_sa_gen.py | 23 + app/native/api/s35_vd.py | 202 ++++++++ app/native/api/s35_vd_cal.py | 66 +++ app/native/api/s35_vd_gen.py | 21 + app/native/api/s36_wda.py | 0 app/native/api/s36_wda_cal.py | 104 ++++ app/native/api/s38_scada_info.py | 42 ++ app/native/api/s39_user.py | 37 ++ app/native/api/s3_reservoirs.py | 193 ++++++++ app/native/api/s40_schema.py | 30 ++ app/native/api/s41_pipe_risk_probability.py | 87 ++++ app/native/api/s42_sensor_placement.py | 7 + app/native/api/s43_burst_locate_result.py | 6 + app/native/api/s4_tanks.py | 250 ++++++++++ app/native/api/s5_pipes.py | 214 ++++++++ app/native/api/s6_pumps.py | 231 +++++++++ app/native/api/s7_valves.py | 210 ++++++++ app/native/api/s8_tags.py | 142 ++++++ app/native/api/s9_demands.py | 115 +++++ app/native/api/sections.py | 90 ++++ .../__init__.cp312-win_amd64.pyd | Bin .../__init__.cpython-312-x86_64-linux-gnu.so | Bin .../batch_api.cp312-win_amd64.pyd | Bin .../batch_api.cpython-312-x86_64-linux-gnu.so | Bin .../batch_api_cs.cp312-win_amd64.pyd | Bin ...tch_api_cs.cpython-312-x86_64-linux-gnu.so | Bin .../batch_exe.cp312-win_amd64.pyd | Bin .../batch_exe.cpython-312-x86_64-linux-gnu.so | Bin .../clean_api.cp312-win_amd64.pyd | Bin .../clean_api.cpython-312-x86_64-linux-gnu.so | Bin .../connection.cp312-win_amd64.pyd | Bin ...connection.cpython-312-x86_64-linux-gnu.so | Bin .../database.cp312-win_amd64.pyd | Bin .../database.cpython-312-x86_64-linux-gnu.so | Bin .../extension_data.cp312-win_amd64.pyd | Bin ...nsion_data.cpython-312-x86_64-linux-gnu.so | Bin .../inp_in.cp312-win_amd64.pyd | Bin .../inp_in.cpython-312-x86_64-linux-gnu.so | Bin .../inp_out.cp312-win_amd64.pyd | Bin .../inp_out.cpython-312-x86_64-linux-gnu.so | Bin .../postgresql_info.cp312-win_amd64.pyd | Bin ...resql_info.cpython-312-x86_64-linux-gnu.so | Bin .../project.cp312-win_amd64.pyd | Bin .../project.cpython-312-x86_64-linux-gnu.so | Bin .../s0_base.cp312-win_amd64.pyd | Bin .../s0_base.cpython-312-x86_64-linux-gnu.so | Bin .../s10_status.cp312-win_amd64.pyd | Bin ...s10_status.cpython-312-x86_64-linux-gnu.so | Bin .../s11_patterns.cp312-win_amd64.pyd | Bin ...1_patterns.cpython-312-x86_64-linux-gnu.so | Bin .../s12_curves.cp312-win_amd64.pyd | Bin ...s12_curves.cpython-312-x86_64-linux-gnu.so | Bin .../s13_controls.cp312-win_amd64.pyd | Bin ...3_controls.cpython-312-x86_64-linux-gnu.so | Bin .../s14_rules.cp312-win_amd64.pyd | Bin .../s14_rules.cpython-312-x86_64-linux-gnu.so | Bin .../s15_energy.cp312-win_amd64.pyd | Bin ...s15_energy.cpython-312-x86_64-linux-gnu.so | Bin .../s16_emitters.cp312-win_amd64.pyd | Bin ...6_emitters.cpython-312-x86_64-linux-gnu.so | Bin .../s17_quality.cp312-win_amd64.pyd | Bin ...17_quality.cpython-312-x86_64-linux-gnu.so | Bin .../s18_sources.cp312-win_amd64.pyd | Bin ...18_sources.cpython-312-x86_64-linux-gnu.so | Bin .../s19_reactions.cp312-win_amd64.pyd | Bin ..._reactions.cpython-312-x86_64-linux-gnu.so | Bin .../s1_title.cp312-win_amd64.pyd | Bin .../s1_title.cpython-312-x86_64-linux-gnu.so | Bin .../s20_mixing.cp312-win_amd64.pyd | Bin ...s20_mixing.cpython-312-x86_64-linux-gnu.so | Bin .../s21_times.cp312-win_amd64.pyd | Bin .../s21_times.cpython-312-x86_64-linux-gnu.so | Bin .../s22_report.cp312-win_amd64.pyd | Bin ...s22_report.cpython-312-x86_64-linux-gnu.so | Bin .../s23_options.cp312-win_amd64.pyd | Bin ...23_options.cpython-312-x86_64-linux-gnu.so | Bin .../s23_options_util.cp312-win_amd64.pyd | Bin ...tions_util.cpython-312-x86_64-linux-gnu.so | Bin .../s23_options_v3.cp312-win_amd64.pyd | Bin ...options_v3.cpython-312-x86_64-linux-gnu.so | Bin .../s24_coordinates.cp312-win_amd64.pyd | Bin ...oordinates.cpython-312-x86_64-linux-gnu.so | Bin .../s25_vertices.cp312-win_amd64.pyd | Bin ...5_vertices.cpython-312-x86_64-linux-gnu.so | Bin .../s26_labels.cp312-win_amd64.pyd | Bin ...s26_labels.cpython-312-x86_64-linux-gnu.so | Bin .../s27_backdrop.cp312-win_amd64.pyd | Bin ...7_backdrop.cpython-312-x86_64-linux-gnu.so | Bin .../s28_end.cp312-win_amd64.pyd | Bin .../s28_end.cpython-312-x86_64-linux-gnu.so | Bin .../s29_scada_device.cp312-win_amd64.pyd | Bin ...ada_device.cpython-312-x86_64-linux-gnu.so | Bin .../s2_junctions.cp312-win_amd64.pyd | Bin ..._junctions.cpython-312-x86_64-linux-gnu.so | Bin .../s30_scada_device_data.cp312-win_amd64.pyd | Bin ...evice_data.cpython-312-x86_64-linux-gnu.so | Bin .../s31_scada_element.cp312-win_amd64.pyd | Bin ...da_element.cpython-312-x86_64-linux-gnu.so | Bin .../s32_region.cp312-win_amd64.pyd | Bin ...s32_region.cpython-312-x86_64-linux-gnu.so | Bin .../s32_region_util.cp312-win_amd64.pyd | Bin ...egion_util.cpython-312-x86_64-linux-gnu.so | Bin .../s33_dma.cp312-win_amd64.pyd | Bin .../s33_dma.cpython-312-x86_64-linux-gnu.so | Bin .../s33_dma_cal.cp312-win_amd64.pyd | Bin ...33_dma_cal.cpython-312-x86_64-linux-gnu.so | Bin .../s33_dma_gen.cp312-win_amd64.pyd | Bin ...33_dma_gen.cpython-312-x86_64-linux-gnu.so | Bin .../s34_sa.cp312-win_amd64.pyd | Bin .../s34_sa.cpython-312-x86_64-linux-gnu.so | Bin .../s34_sa_cal.cp312-win_amd64.pyd | Bin ...s34_sa_cal.cpython-312-x86_64-linux-gnu.so | Bin .../s34_sa_gen.cp312-win_amd64.pyd | Bin ...s34_sa_gen.cpython-312-x86_64-linux-gnu.so | Bin .../s35_vd.cp312-win_amd64.pyd | Bin .../s35_vd.cpython-312-x86_64-linux-gnu.so | Bin .../s35_vd_cal.cp312-win_amd64.pyd | Bin ...s35_vd_cal.cpython-312-x86_64-linux-gnu.so | Bin .../s35_vd_gen.cp312-win_amd64.pyd | Bin ...s35_vd_gen.cpython-312-x86_64-linux-gnu.so | Bin .../s36_wda.cp312-win_amd64.pyd | Bin .../s36_wda.cpython-312-x86_64-linux-gnu.so | Bin .../s36_wda_cal.cp312-win_amd64.pyd | Bin ...36_wda_cal.cpython-312-x86_64-linux-gnu.so | Bin .../s38_scada_info.cp312-win_amd64.pyd | Bin ...scada_info.cpython-312-x86_64-linux-gnu.so | Bin .../s39_user.cp312-win_amd64.pyd | Bin .../s39_user.cpython-312-x86_64-linux-gnu.so | Bin .../s3_reservoirs.cp312-win_amd64.pyd | Bin ...reservoirs.cpython-312-x86_64-linux-gnu.so | Bin .../s40_schema.cp312-win_amd64.pyd | Bin ...s40_schema.cpython-312-x86_64-linux-gnu.so | Bin ..._pipe_risk_probability.cp312-win_amd64.pyd | Bin ...robability.cpython-312-x86_64-linux-gnu.so | Bin .../s42_sensor_placement.cp312-win_amd64.pyd | Bin ..._placement.cpython-312-x86_64-linux-gnu.so | Bin ...43_burst_locate_result.cp312-win_amd64.pyd | Bin ...ate_result.cpython-312-x86_64-linux-gnu.so | Bin .../s4_tanks.cp312-win_amd64.pyd | Bin .../s4_tanks.cpython-312-x86_64-linux-gnu.so | Bin .../s5_pipes.cp312-win_amd64.pyd | Bin .../s5_pipes.cpython-312-x86_64-linux-gnu.so | Bin .../s6_pumps.cp312-win_amd64.pyd | Bin .../s6_pumps.cpython-312-x86_64-linux-gnu.so | Bin .../s7_valves.cp312-win_amd64.pyd | Bin .../s7_valves.cpython-312-x86_64-linux-gnu.so | Bin .../s8_tags.cp312-win_amd64.pyd | Bin .../s8_tags.cpython-312-x86_64-linux-gnu.so | Bin .../s9_demands.cp312-win_amd64.pyd | Bin ...s9_demands.cpython-312-x86_64-linux-gnu.so | Bin .../sections.cp312-win_amd64.pyd | Bin .../sections.cpython-312-x86_64-linux-gnu.so | Bin app/services/epanet/epanet.py | 4 +- app/services/simulation.py | 9 +- scripts/run_server.py | 2 +- 203 files changed, 9470 insertions(+), 6 deletions(-) create mode 100644 app/native/api/__init__.py create mode 100644 app/native/api/batch_api.py create mode 100644 app/native/api/batch_api_cs.py create mode 100644 app/native/api/batch_exe.py create mode 100644 app/native/api/clean_api.py create mode 100644 app/native/api/connection.py create mode 100644 app/native/api/database.py create mode 100644 app/native/api/extension_data.py create mode 100644 app/native/api/inp_in.py create mode 100644 app/native/api/inp_out.py create mode 100644 app/native/api/postgresql_info.py create mode 100644 app/native/api/project.py create mode 100644 app/native/api/project_backup.py create mode 100644 app/native/api/s0_base.py create mode 100644 app/native/api/s10_status.py create mode 100644 app/native/api/s11_patterns.py create mode 100644 app/native/api/s12_curves.py create mode 100644 app/native/api/s13_controls.py create mode 100644 app/native/api/s14_rules.py create mode 100644 app/native/api/s15_energy.py create mode 100644 app/native/api/s16_emitters.py create mode 100644 app/native/api/s17_quality.py create mode 100644 app/native/api/s18_sources.py create mode 100644 app/native/api/s19_reactions.py create mode 100644 app/native/api/s1_title.py create mode 100644 app/native/api/s20_mixing.py create mode 100644 app/native/api/s21_times.py create mode 100644 app/native/api/s22_report.py create mode 100644 app/native/api/s23_options.py create mode 100644 app/native/api/s23_options_util.py create mode 100644 app/native/api/s23_options_v3.py create mode 100644 app/native/api/s24_coordinates.py create mode 100644 app/native/api/s25_vertices.py create mode 100644 app/native/api/s26_labels.py create mode 100644 app/native/api/s27_backdrop.py create mode 100644 app/native/api/s28_end.py create mode 100644 app/native/api/s29_scada_device.py create mode 100644 app/native/api/s2_junctions.py create mode 100644 app/native/api/s30_scada_device_data.py create mode 100644 app/native/api/s31_scada_element.py create mode 100644 app/native/api/s32_region.py create mode 100644 app/native/api/s32_region_util.py create mode 100644 app/native/api/s33_dma.py create mode 100644 app/native/api/s33_dma_cal.py create mode 100644 app/native/api/s33_dma_gen.py create mode 100644 app/native/api/s34_sa.py create mode 100644 app/native/api/s34_sa_cal.py create mode 100644 app/native/api/s34_sa_gen.py create mode 100644 app/native/api/s35_vd.py create mode 100644 app/native/api/s35_vd_cal.py create mode 100644 app/native/api/s35_vd_gen.py create mode 100644 app/native/api/s36_wda.py create mode 100644 app/native/api/s36_wda_cal.py create mode 100644 app/native/api/s38_scada_info.py create mode 100644 app/native/api/s39_user.py create mode 100644 app/native/api/s3_reservoirs.py create mode 100644 app/native/api/s40_schema.py create mode 100644 app/native/api/s41_pipe_risk_probability.py create mode 100644 app/native/api/s42_sensor_placement.py create mode 100644 app/native/api/s43_burst_locate_result.py create mode 100644 app/native/api/s4_tanks.py create mode 100644 app/native/api/s5_pipes.py create mode 100644 app/native/api/s6_pumps.py create mode 100644 app/native/api/s7_valves.py create mode 100644 app/native/api/s8_tags.py create mode 100644 app/native/api/s9_demands.py create mode 100644 app/native/api/sections.py rename app/native/{api => api_encap}/__init__.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/__init__.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/batch_api.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/batch_api.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/batch_api_cs.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/batch_api_cs.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/batch_exe.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/batch_exe.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/clean_api.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/clean_api.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/connection.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/connection.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/database.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/database.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/extension_data.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/extension_data.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/inp_in.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/inp_in.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/inp_out.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/inp_out.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/postgresql_info.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/postgresql_info.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/project.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/project.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s0_base.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s0_base.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s10_status.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s10_status.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s11_patterns.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s11_patterns.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s12_curves.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s12_curves.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s13_controls.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s13_controls.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s14_rules.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s14_rules.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s15_energy.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s15_energy.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s16_emitters.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s16_emitters.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s17_quality.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s17_quality.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s18_sources.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s18_sources.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s19_reactions.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s19_reactions.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s1_title.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s1_title.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s20_mixing.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s20_mixing.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s21_times.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s21_times.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s22_report.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s22_report.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s23_options.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s23_options.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s23_options_util.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s23_options_util.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s23_options_v3.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s23_options_v3.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s24_coordinates.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s24_coordinates.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s25_vertices.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s25_vertices.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s26_labels.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s26_labels.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s27_backdrop.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s27_backdrop.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s28_end.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s28_end.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s29_scada_device.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s29_scada_device.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s2_junctions.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s2_junctions.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s30_scada_device_data.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s30_scada_device_data.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s31_scada_element.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s31_scada_element.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s32_region.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s32_region.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s32_region_util.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s32_region_util.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s33_dma.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s33_dma.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s33_dma_cal.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s33_dma_cal.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s33_dma_gen.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s33_dma_gen.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s34_sa.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s34_sa.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s34_sa_cal.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s34_sa_cal.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s34_sa_gen.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s34_sa_gen.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s35_vd.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s35_vd.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s35_vd_cal.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s35_vd_cal.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s35_vd_gen.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s35_vd_gen.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s36_wda.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s36_wda.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s36_wda_cal.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s36_wda_cal.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s38_scada_info.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s38_scada_info.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s39_user.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s39_user.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s3_reservoirs.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s3_reservoirs.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s40_schema.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s40_schema.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s41_pipe_risk_probability.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s41_pipe_risk_probability.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s42_sensor_placement.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s42_sensor_placement.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s43_burst_locate_result.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s43_burst_locate_result.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s4_tanks.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s4_tanks.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s5_pipes.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s5_pipes.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s6_pumps.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s6_pumps.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s7_valves.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s7_valves.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s8_tags.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s8_tags.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/s9_demands.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/s9_demands.cpython-312-x86_64-linux-gnu.so (100%) rename app/native/{api => api_encap}/sections.cp312-win_amd64.pyd (100%) rename app/native/{api => api_encap}/sections.cpython-312-x86_64-linux-gnu.so (100%) diff --git a/app/algorithms/api_ex/run_simulation.py b/app/algorithms/api_ex/run_simulation.py index 045c5e4..e80a9ff 100644 --- a/app/algorithms/api_ex/run_simulation.py +++ b/app/algorithms/api_ex/run_simulation.py @@ -1,6 +1,6 @@ import numpy as np from app.services.tjnetwork import * -from api.s36_wda_cal import * +from app.native.api.s36_wda_cal import * # from get_real_status import * from datetime import datetime,timedelta from math import modf diff --git a/app/native/api/__init__.py b/app/native/api/__init__.py new file mode 100644 index 0000000..3981d3e --- /dev/null +++ b/app/native/api/__init__.py @@ -0,0 +1,175 @@ +from .project_backup import list_project, have_project, create_project, delete_project, clean_project +from .project_backup import is_project_open, open_project, close_project +from .project_backup import copy_project + +#DingZQ, 2024-12-28, convert inp v3 to v2 +from .inp_in import read_inp, import_inp, convert_inp_v3_to_v2 +from .inp_out import dump_inp, export_inp + +from .database import API_ADD, API_UPDATE, API_DELETE +from .database import ChangeSet +from .database import get_current_operation +from .database import execute_undo, execute_redo +from .database import list_snapshot +from .database import have_snapshot, have_snapshot_for_operation, have_snapshot_for_current_operation +from .database import take_snapshot_for_operation, take_snapshot_for_current_operation, take_snapshot +from .database import update_snapshot, update_snapshot_for_current_operation +from .database import delete_snapshot, delete_snapshot_by_operation +from .database import get_operation_by_snapshot, get_snapshot_by_operation +from .database import pick_snapshot +from .database import pick_operation, sync_with_server +from .database import get_restore_operation, set_restore_operation, set_restore_operation_to_current, restore +from .database import read, try_read, read_all, write + +from .batch_exe import execute_batch_commands, execute_batch_command + +from .extension_data import get_all_extension_data_keys, get_all_extension_data, get_extension_data, set_extension_data + +from .s0_base import JUNCTION, RESERVOIR, TANK, PIPE, PUMP, VALVE, PATTERN, CURVE +from .s0_base import is_node, is_junction, is_reservoir, is_tank +from .s0_base import is_link, is_pipe, is_pump, is_valve +from .s0_base import is_curve +from .s0_base import is_pattern +from .s0_base import get_nodes, get_nodes_id_and_type, get_junctions, get_reservoirs, get_tanks, get_links, get_links_id_and_type, get_pipes, get_pumps, get_valves, get_curves, get_patterns +from .s0_base import get_node_type, get_link_type, get_element_type, get_element_type_value +from .s0_base import get_node_links, get_link_nodes +from .s0_base import get_major_nodes, get_major_pipes + +from .s1_title import get_title_schema, get_title, set_title + +from .s2_junctions import get_junction_schema, add_junction, get_junction, set_junction, get_all_junctions +from .batch_api import delete_junction_cascade + +from .s3_reservoirs import get_reservoir_schema, add_reservoir, get_reservoir, set_reservoir, get_all_reservoirs +from .batch_api import delete_reservoir_cascade + +from .s4_tanks import OVERFLOW_YES, OVERFLOW_NO +from .s4_tanks import get_tank_schema, add_tank, get_tank, set_tank, get_all_tanks +from .batch_api import delete_tank_cascade + +from .s5_pipes import PIPE_STATUS_OPEN, PIPE_STATUS_CLOSED, PIPE_STATUS_CV +from .s5_pipes import get_pipe_schema, add_pipe, get_pipe, set_pipe, get_all_pipes +from .batch_api import delete_pipe_cascade + +from .s6_pumps import get_pump_schema, add_pump, get_pump, set_pump, get_all_pumps +from .batch_api import delete_pump_cascade + +from .s7_valves import VALVES_TYPE_PRV, VALVES_TYPE_PSV, VALVES_TYPE_PBV, VALVES_TYPE_FCV, VALVES_TYPE_TCV, VALVES_TYPE_GPV +from .s7_valves import get_valve_schema, add_valve, get_valve, set_valve, get_all_valves +from .batch_api import delete_valve_cascade + +from .s8_tags import TAG_TYPE_NODE, TAG_TYPE_LINK +from .s8_tags import get_tag_schema, get_tags, get_tag, set_tag + +from .s9_demands import get_demand_schema, get_demand, set_demand + +from .s10_status import LINK_STATUS_OPEN, LINK_STATUS_CLOSED, LINK_STATUS_ACTIVE +from .s10_status import get_status_schema, get_status, set_status + +from .s11_patterns import get_pattern_schema, get_pattern, set_pattern, add_pattern +from .batch_api import delete_pattern_cascade + +from .s12_curves import CURVE_TYPE_PUMP, CURVE_TYPE_EFFICIENCY, CURVE_TYPE_VOLUME, CURVE_TYPE_HEADLOSS +from .s12_curves import get_curve_schema, get_curve, set_curve, add_curve +from .batch_api import delete_curve_cascade + +from .s13_controls import get_control_schema, get_control, set_control + +from .s14_rules import get_rule_schema, get_rule, set_rule + +from .s15_energy import get_energy_schema, get_energy, set_energy +from .s15_energy import get_pump_energy_schema, get_pump_energy, set_pump_energy + +from .s16_emitters import get_emitter_schema, get_emitter, set_emitter + +from .s17_quality import get_quality_schema, get_quality, set_quality + +from .s18_sources import SOURCE_TYPE_CONCEN, SOURCE_TYPE_MASS, SOURCE_TYPE_FLOWPACED, SOURCE_TYPE_SETPOINT +from .s18_sources import get_source_schema, get_source, set_source, add_source, delete_source + +from .s19_reactions import get_reaction_schema, get_reaction, set_reaction +from .s19_reactions import get_pipe_reaction_schema, get_pipe_reaction, set_pipe_reaction +from .s19_reactions import get_tank_reaction_schema, get_tank_reaction, set_tank_reaction + +from .s20_mixing import MIXING_MODEL_MIXED, MIXING_MODEL_2COMP, MIXING_MODEL_FIFO, MIXING_MODEL_LIFO +from .s20_mixing import get_mixing_schema, get_mixing, set_mixing, add_mixing, delete_mixing + +from .s21_times import TIME_STATISTIC_NONE, TIME_STATISTIC_AVERAGED, TIME_STATISTIC_MINIMUM, TIME_STATISTIC_MAXIMUM, TIME_STATISTIC_RANGE +from .s21_times import get_time_schema, get_time, set_time + +from .s23_options_util import OPTION_UNITS_CFS, OPTION_UNITS_GPM, OPTION_UNITS_MGD, OPTION_UNITS_IMGD, OPTION_UNITS_AFD, OPTION_UNITS_LPS, OPTION_UNITS_LPM, OPTION_UNITS_MLD, OPTION_UNITS_CMH, OPTION_UNITS_CMD +from .s23_options_util import OPTION_PRESSURE_PSI, OPTION_PRESSURE_KPA, OPTION_PRESSURE_METERS +from .s23_options_util import OPTION_HEADLOSS_HW, OPTION_HEADLOSS_DW, OPTION_HEADLOSS_CM +from .s23_options_util import OPTION_UNBALANCED_STOP, OPTION_UNBALANCED_CONTINUE +from .s23_options_util import OPTION_DEMAND_MODEL_DDA, OPTION_DEMAND_MODEL_PDA +from .s23_options_util import OPTION_QUALITY_NONE, OPTION_QUALITY_CHEMICAL, OPTION_QUALITY_AGE, OPTION_QUALITY_TRACE +from .s23_options_util import get_option_schema, get_option +from .batch_api import set_option_ex + +from .s23_options_util import OPTION_V3_FLOW_UNITS_CFS, OPTION_V3_FLOW_UNITS_GPM, OPTION_V3_FLOW_UNITS_MGD, OPTION_V3_FLOW_UNITS_IMGD, OPTION_V3_FLOW_UNITS_AFD, OPTION_V3_FLOW_UNITS_LPS, OPTION_V3_FLOW_UNITS_LPM, OPTION_V3_FLOW_UNITS_MLD, OPTION_V3_FLOW_UNITS_CMH, OPTION_V3_FLOW_UNITS_CMD +from .s23_options_util import OPTION_V3_PRESSURE_UNITS_PSI, OPTION_V3_PRESSURE_UNITS_KPA, OPTION_V3_PRESSURE_UNITS_METERS +from .s23_options_util import OPTION_V3_HEADLOSS_MODEL_HW, OPTION_V3_HEADLOSS_MODEL_DW, OPTION_V3_HEADLOSS_MODEL_CM +from .s23_options_util import OPTION_V3_STEP_SIZING_FULL, OPTION_V3_STEP_SIZING_RELAXATION, OPTION_V3_STEP_SIZING_LINESEARCH +from .s23_options_util import OPTION_V3_IF_UNBALANCED_STOP, OPTION_V3_IF_UNBALANCED_CONTINUE +from .s23_options_util import OPTION_V3_DEMAND_MODEL_FIXED, OPTION_V3_DEMAND_MODEL_CONSTRAINED, OPTION_V3_DEMAND_MODEL_POWER, OPTION_V3_DEMAND_MODEL_LOGISTIC +from .s23_options_util import OPTION_V3_LEAKAGE_MODEL_NONE, OPTION_V3_LEAKAGE_MODEL_POWER, OPTION_V3_LEAKAGE_MODEL_FAVAD +from .s23_options_util import OPTION_V3_QUALITY_MODEL_NONE, OPTION_V3_QUALITY_MODEL_CHEMICAL, OPTION_V3_QUALITY_MODEL_AGE, OPTION_V3_QUALITY_MODEL_TRACE +from .s23_options_util import OPTION_V3_QUALITY_UNITS_HRS, OPTION_V3_QUALITY_UNITS_PCNT, OPTION_V3_QUALITY_UNITS_MGL, OPTION_V3_QUALITY_UNITS_UGL +from .s23_options_util import get_option_v3_schema, get_option_v3 +from .batch_api import set_option_v3_ex + +from .s24_coordinates import get_node_coord, get_nodes_in_extent, get_links_in_extent + +from .s25_vertices import get_vertex_schema, get_vertex, set_vertex, add_vertex, delete_vertex +from .s25_vertices import get_all_vertex_links, get_all_vertices + +from .s26_labels import get_label_schema, get_label, set_label, add_label, delete_label + +from .s27_backdrop import get_backdrop_schema, get_backdrop, set_backdrop + +from .s29_scada_device import SCADA_DEVICE_TYPE_PRESSURE, SCADA_DEVICE_TYPE_DEMAND, SCADA_DEVICE_TYPE_QUALITY, SCADA_DEVICE_TYPE_LEVEL, SCADA_DEVICE_TYPE_FLOW, SCADA_DEVICE_TYPE_UNKNOWN +from .s29_scada_device import get_scada_device_schema, get_scada_device, set_scada_device, add_scada_device, delete_scada_device +from .s29_scada_device import get_all_scada_device_ids, get_all_scada_devices +from .clean_api import clean_scada_device + +from .s30_scada_device_data import get_scada_device_data_schema, get_scada_device_data, set_scada_device_data, add_scada_device_data, delete_scada_device_data +from .clean_api import clean_scada_device_data + +from .s31_scada_element import SCADA_MODEL_TYPE_JUNCTION, SCADA_MODEL_TYPE_RESERVOIR, SCADA_MODEL_TYPE_TANK, SCADA_MODEL_TYPE_PIPE, SCADA_MODEL_TYPE_PUMP, SCADA_MODEL_TYPE_VALVE +from .s31_scada_element import SCADA_ELEMENT_STATUS_OFFLINE, SCADA_ELEMENT_STATUS_ONLINE +from .s31_scada_element import get_scada_element_schema, get_scada_element, set_scada_element, add_scada_element, delete_scada_element +from .s31_scada_element import get_all_scada_element_ids, get_all_scada_elements +from .clean_api import clean_scada_element + +from .s32_region_util import get_nodes_in_boundary, get_nodes_in_region, get_links_on_region_boundary, calculate_convex_hull, calculate_boundary, inflate_boundary, inflate_region +from .s32_region import get_region_schema, get_region, set_region, add_region, delete_region + +from .s33_dma_cal import PARTITION_TYPE_RB, PARTITION_TYPE_KWAY +from .s33_dma_cal import calculate_district_metering_area_for_nodes, calculate_district_metering_area_for_region, calculate_district_metering_area_for_network +from .s33_dma import get_district_metering_area_schema, get_district_metering_area, set_district_metering_area, add_district_metering_area, delete_district_metering_area +from .s33_dma import get_all_district_metering_area_ids, get_all_district_metering_areas +from .s33_dma_gen import generate_district_metering_area, generate_sub_district_metering_area + +from .s34_sa_cal import calculate_service_area +from .s34_sa import get_service_area_schema, get_service_area, set_service_area, add_service_area, delete_service_area +from .s34_sa import get_all_service_area_ids, get_all_service_areas +from .s34_sa_gen import generate_service_area + +from .s35_vd_cal import calculate_virtual_district +from .s35_vd import get_virtual_district_schema, get_virtual_district, set_virtual_district, add_virtual_district, delete_virtual_district +from .s35_vd import get_all_virtual_district_ids, get_all_virtual_districts +from .s35_vd_gen import generate_virtual_district + +from .s36_wda_cal import calculate_demand_to_nodes, calculate_demand_to_region, calculate_demand_to_network + +from .s38_scada_info import get_scada_info_schema, get_scada_info, get_all_scada_info + +from .s39_user import get_user_schema, get_user, get_all_users + +from .s40_schema import get_scheme_schema, get_scheme, get_all_schemes + +from .s41_pipe_risk_probability import get_pipe_risk_probability_now, get_pipe_risk_probability, get_network_pipe_risk_probability_now, get_pipes_risk_probability, get_pipe_risk_probability_geometries + +from .s42_sensor_placement import get_all_sensor_placements + +from .s43_burst_locate_result import get_all_burst_locate_results diff --git a/app/native/api/batch_api.py b/app/native/api/batch_api.py new file mode 100644 index 0000000..1c47d54 --- /dev/null +++ b/app/native/api/batch_api.py @@ -0,0 +1,53 @@ +from .sections import * +from .database import ChangeSet, API_DELETE, API_UPDATE +from .batch_exe import execute_batch_command + + +def delete_junction_cascade(name: str, cs: ChangeSet) -> ChangeSet: + cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s2_junction } + return execute_batch_command(name, cs) + + +def delete_reservoir_cascade(name: str, cs: ChangeSet) -> ChangeSet: + cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s3_reservoir } + return execute_batch_command(name, cs) + + +def delete_tank_cascade(name: str, cs: ChangeSet) -> ChangeSet: + cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s4_tank } + return execute_batch_command(name, cs) + + +def delete_pipe_cascade(name: str, cs: ChangeSet) -> ChangeSet: + cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s5_pipe } + return execute_batch_command(name, cs) + + +def delete_pump_cascade(name: str, cs: ChangeSet) -> ChangeSet: + cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s6_pump } + return execute_batch_command(name, cs) + + +def delete_valve_cascade(name: str, cs: ChangeSet) -> ChangeSet: + cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s7_valve } + return execute_batch_command(name, cs) + + +def delete_pattern_cascade(name: str, cs: ChangeSet) -> ChangeSet: + cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s11_pattern } + return execute_batch_command(name, cs) + + +def delete_curve_cascade(name: str, cs: ChangeSet) -> ChangeSet: + cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s12_curve } + return execute_batch_command(name, cs) + + +def set_option_ex(name: str, cs: ChangeSet) -> ChangeSet: + cs.operations[0] |= { 'operation' : API_UPDATE, 'type' : s23_option } + return execute_batch_command(name, cs) + + +def set_option_v3_ex(name: str, cs: ChangeSet) -> ChangeSet: + cs.operations[0] |= { 'operation' : API_UPDATE, 'type' : s23_option_v3 } + return execute_batch_command(name, cs) diff --git a/app/native/api/batch_api_cs.py b/app/native/api/batch_api_cs.py new file mode 100644 index 0000000..ec94f53 --- /dev/null +++ b/app/native/api/batch_api_cs.py @@ -0,0 +1,238 @@ +from .database import ChangeSet, g_delete_prefix, API_DELETE, API_UPDATE, try_read +from .sections import * + +from .s0_base import * + +from .s3_reservoirs import unset_reservoir_by_pattern +from .s4_tanks import unset_tank_by_curve +from .s6_pumps import unset_pump_by_curve, unset_pump_by_pattern +from .s8_tags import delete_tag_by_node, delete_tag_by_link +from .s9_demands import delete_demand_by_junction, unset_demand_by_pattern +from .s10_status import delete_status_by_link +from .s15_energy import delete_pump_energy_by_pump, unset_pump_energy_by_pattern, unset_pump_energy_by_curve +from .s16_emitters import delete_emitter_by_junction +from .s17_quality import delete_quality_by_node +from .s18_sources import delete_source_by_node, unset_source_by_pattern +from .s19_reactions import delete_pipe_reaction_by_pipe, delete_tank_reaction_by_tank +from .s20_mixing import delete_mixing_by_tank +from .s25_vertices import delete_vertex_by_link +from .s26_labels import unset_label_by_node + +from .s23_options_util import generate_v2, generate_v3 + + +def delete_junction_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: + result = ChangeSet() + + id = cs.operations[0]['id'] + row = try_read(name, f"select * from junctions where id = '{id}'") + if row == None: + return result + + links = get_node_links(name, id) + + for link in links: + if is_pipe(name, link): + result.merge(delete_pipe_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link}))) + if is_pump(name, link): + result.merge(delete_pump_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link}))) + if is_valve(name, link): + result.merge(delete_valve_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link}))) + + result.merge(delete_tag_by_node(name, id)) + result.merge(delete_demand_by_junction(name, id)) + result.merge(delete_emitter_by_junction(name, id)) + result.merge(delete_quality_by_node(name, id)) + result.merge(delete_source_by_node(name, id)) + result.merge(unset_label_by_node(name, id)) + result.merge(cs) + + return result + + +def delete_reservoir_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: + result = ChangeSet() + + id = cs.operations[0]['id'] + row = try_read(name, f"select * from reservoirs where id = '{id}'") + if row == None: + return result + + links = get_node_links(name, id) + + for link in links: + if is_pipe(name, link): + result.merge(delete_pipe_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link}))) + if is_pump(name, link): + result.merge(delete_pump_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link}))) + if is_valve(name, link): + result.merge(delete_valve_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link}))) + + result.merge(delete_tag_by_node(name, id)) + result.merge(delete_quality_by_node(name, id)) + result.merge(delete_source_by_node(name, id)) + result.merge(unset_label_by_node(name, id)) + result.merge(cs) + + return result + + +def delete_tank_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: + result = ChangeSet() + + id = cs.operations[0]['id'] + row = try_read(name, f"select * from tanks where id = '{id}'") + if row == None: + return result + + links = get_node_links(name, id) + + for link in links: + if is_pipe(name, link): + result.merge(delete_pipe_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link}))) + if is_pump(name, link): + result.merge(delete_pump_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link}))) + if is_valve(name, link): + result.merge(delete_valve_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link}))) + + result.merge(delete_tag_by_node(name, id)) + result.merge(delete_quality_by_node(name, id)) + result.merge(delete_source_by_node(name, id)) + result.merge(delete_tank_reaction_by_tank(name, id)) + result.merge(delete_mixing_by_tank(name, id)) + result.merge(unset_label_by_node(name, id)) + result.merge(cs) + + return result + + +def delete_pipe_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: + result = ChangeSet() + + id = cs.operations[0]['id'] + row = try_read(name, f"select * from pipes where id = '{id}'") + if row == None: + return result + + result.merge(delete_tag_by_link(name, id)) + result.merge(delete_status_by_link(name, id)) + result.merge(delete_pipe_reaction_by_pipe(name, id)) + result.merge(delete_vertex_by_link(name, id)) + result.merge(cs) + + return result + + +def delete_pump_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: + result = ChangeSet() + + id = cs.operations[0]['id'] + row = try_read(name, f"select * from pumps where id = '{id}'") + if row == None: + return result + + result.merge(delete_tag_by_link(name, id)) + result.merge(delete_status_by_link(name, id)) + result.merge(delete_pump_energy_by_pump(name, id)) + result.merge(delete_vertex_by_link(name, id)) + result.merge(cs) + + return result + + +def delete_valve_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: + result = ChangeSet() + + id = cs.operations[0]['id'] + row = try_read(name, f"select * from valves where id = '{id}'") + if row == None: + return result + + result.merge(delete_tag_by_link(name, id)) + result.merge(delete_status_by_link(name, id)) + result.merge(delete_vertex_by_link(name, id)) + + result.merge(cs) + + return result + + +def delete_pattern_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: + result = ChangeSet() + + id = cs.operations[0]['id'] + row = try_read(name, f"select * from _pattern where id = '{id}'") + if row == None: + return result + + result.merge(unset_reservoir_by_pattern(name, id)) + result.merge(unset_pump_by_pattern(name, id)) + result.merge(unset_demand_by_pattern(name, id)) + result.merge(unset_pump_energy_by_pattern(name, id)) + result.merge(unset_source_by_pattern(name, id)) + result.merge(cs) + + return result + + +def delete_curve_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: + result = ChangeSet() + + id = cs.operations[0]['id'] + row = try_read(name, f"select * from _curve where id = '{id}'") + if row == None: + return result + + result.merge(unset_tank_by_curve(name, id)) + result.merge(unset_pump_by_curve(name, id)) + result.merge(unset_pump_energy_by_curve(name, id)) + result.merge(cs) + + return result + + +def set_option_cs(cs: ChangeSet) -> ChangeSet: + cs.operations[0]['operation'] = API_UPDATE + cs.operations[0]['type'] = 'option' + new_cs = cs + new_cs.merge(generate_v3(cs)) + return new_cs + + +def set_option_v3_cs(cs: ChangeSet) -> ChangeSet: + cs.operations[0]['operation'] = API_UPDATE + cs.operations[0]['type'] = 'option_v3' + new_cs = cs + new_cs.merge(generate_v2(cs)) + return new_cs + + +def rewrite_batch_api(name: str, cs: ChangeSet) -> ChangeSet: + op = cs.operations[0] + api = op['operation'] + type = op['type'] + + if api == API_DELETE: + if type == s2_junction: + return delete_junction_cascade_batch_cs(name, cs) + elif type == s3_reservoir: + return delete_reservoir_cascade_batch_cs(name, cs) + elif type == s4_tank: + return delete_tank_cascade_batch_cs(name, cs) + elif type == s5_pipe: + return delete_pipe_cascade_batch_cs(name, cs) + elif type == s6_pump: + return delete_pump_cascade_batch_cs(name, cs) + elif type == s7_valve: + return delete_valve_cascade_batch_cs(name, cs) + elif type == s11_pattern: + return delete_pattern_cascade_batch_cs(name, cs) + elif type == s12_curve: + return delete_curve_cascade_batch_cs(name, cs) + elif api == API_UPDATE: + if type == s23_option: + return set_option_cs(cs) + elif type == s23_option_v3: + return set_option_v3_cs(cs) + + return cs diff --git a/app/native/api/batch_exe.py b/app/native/api/batch_exe.py new file mode 100644 index 0000000..080688e --- /dev/null +++ b/app/native/api/batch_exe.py @@ -0,0 +1,380 @@ +from typing import Any +from .sections import * +from .database import API_ADD, API_UPDATE, API_DELETE, ChangeSet, write, read, read_all, get_current_operation +from .extension_data import set_extension_data +from .s1_title import set_title +from .s2_junctions import set_junction, add_junction, delete_junction +from .s3_reservoirs import set_reservoir, add_reservoir, delete_reservoir +from .s4_tanks import set_tank, add_tank, delete_tank +from .s5_pipes import set_pipe, add_pipe, delete_pipe +from .s6_pumps import set_pump, add_pump, delete_pump +from .s7_valves import set_valve, add_valve, delete_valve +from .s8_tags import set_tag +from .s9_demands import set_demand +from .s10_status import set_status +from .s11_patterns import set_pattern, add_pattern, delete_pattern +from .s12_curves import set_curve, add_curve, delete_curve +from .s13_controls import set_control +from .s14_rules import set_rule +from .s15_energy import set_energy, set_pump_energy +from .s16_emitters import set_emitter +from .s17_quality import set_quality +from .s18_sources import set_source, add_source, delete_source +from .s19_reactions import set_reaction, set_pipe_reaction, set_tank_reaction +from .s20_mixing import set_mixing, add_mixing, delete_mixing +from .s21_times import set_time +from .s23_options_util import set_option, set_option_v3 +from .s25_vertices import set_vertex, add_vertex, delete_vertex +from .s26_labels import set_label, add_label, delete_label +from .s27_backdrop import set_backdrop +from .s29_scada_device import set_scada_device, add_scada_device, delete_scada_device +from .s30_scada_device_data import set_scada_device_data, add_scada_device_data, delete_scada_device_data +from .s31_scada_element import set_scada_element, add_scada_element, delete_scada_element +from .s32_region import set_region, add_region, delete_region +from .s33_dma import set_district_metering_area, add_district_metering_area, delete_district_metering_area +from .s34_sa import set_service_area, add_service_area, delete_service_area +from .s35_vd import set_virtual_district, add_virtual_district, delete_virtual_district +from .batch_api_cs import rewrite_batch_api + + +def _execute_add_command(name: str, cs: ChangeSet) -> ChangeSet: + type = cs.operations[0]['type'] + + if type == s1_title: + return ChangeSet() + if type == s2_junction: + return add_junction(name, cs) + elif type == s3_reservoir: + return add_reservoir(name, cs) + elif type == s4_tank: + return add_tank(name, cs) + elif type == s5_pipe: + return add_pipe(name, cs) + elif type == s6_pump: + return add_pump(name, cs) + elif type == s7_valve: + return add_valve(name, cs) + elif type == s8_tag: + return ChangeSet() + elif type == s9_demand: + return ChangeSet() + elif type == s10_status: + return ChangeSet() + elif type == s11_pattern: + return add_pattern(name, cs) + elif type == s12_curve: + return add_curve(name, cs) + elif type == s13_control: + return ChangeSet() + elif type == s14_rule: + return ChangeSet() + elif type == s15_energy: + return ChangeSet() + elif type == s15_pump_energy: + return ChangeSet() + elif type == s16_emitter: + return ChangeSet() + elif type == s17_quality: + return ChangeSet() + elif type == s18_source: + return add_source(name, cs) + elif type == s19_reaction: + return ChangeSet() + elif type == s19_pipe_reaction: + return ChangeSet() + elif type == s19_tank_reaction: + return ChangeSet() + elif type == s20_mixing: + return add_mixing(name, cs) + elif type == s21_time: + return ChangeSet() + elif type == s22_report: + return ChangeSet() + elif type == s23_option: + return ChangeSet() + elif type == s23_option_v3: + return ChangeSet() + elif type == s24_coordinate: + return ChangeSet() + elif type == s25_vertex: + return add_vertex(name, cs) + elif type == s26_label: + return add_label(name, cs) + elif type == s27_backdrop: + return ChangeSet() + elif type == s28_end: + return ChangeSet() + elif type == s29_scada_device: + return add_scada_device(name, cs) + elif type == s30_scada_device_data: + return add_scada_device_data(name, cs) + elif type == s31_scada_element: + return add_scada_element(name, cs) + elif type == s32_region: + return add_region(name, cs) + elif type == s33_dma: + return add_district_metering_area(name, cs) + elif type == s34_sa: + return add_service_area(name, cs) + elif type == s35_vd: + return add_virtual_district(name, cs) + + return ChangeSet() + + +def _execute_update_command(name: str, cs: ChangeSet) -> ChangeSet: + type = cs.operations[0]['type'] + + if type == 'extension_data': + return set_extension_data(name, cs) + if type == s1_title: + return set_title(name, cs) + if type == s2_junction: + return set_junction(name, cs) + elif type == s3_reservoir: + return set_reservoir(name, cs) + elif type == s4_tank: + return set_tank(name, cs) + elif type == s5_pipe: + return set_pipe(name, cs) + elif type == s6_pump: + return set_pump(name, cs) + elif type == s7_valve: + return set_valve(name, cs) + elif type == s8_tag: + return set_tag(name, cs) + elif type == s9_demand: + return set_demand(name, cs) + elif type == s10_status: + return set_status(name, cs) + elif type == s11_pattern: + return set_pattern(name, cs) + elif type == s12_curve: + return set_curve(name, cs) + elif type == s13_control: + return set_control(name, cs) + elif type == s14_rule: + return set_rule(name, cs) + elif type == s15_energy: + return set_energy(name, cs) + elif type == s15_pump_energy: + return set_pump_energy(name, cs) + elif type == s16_emitter: + return set_emitter(name, cs) + elif type == s17_quality: + return set_quality(name, cs) + elif type == s18_source: + return set_source(name, cs) + elif type == s19_reaction: + return set_reaction(name, cs) + elif type == s19_pipe_reaction: + return set_pipe_reaction(name, cs) + elif type == s19_tank_reaction: + return set_tank_reaction(name, cs) + elif type == s20_mixing: + return set_mixing(name, cs) + elif type == s21_time: + return set_time(name, cs) + elif type == s22_report: # no api now + return ChangeSet() + elif type == s23_option: + return set_option(name, cs) + elif type == s23_option_v3: + return set_option_v3(name, cs) + elif type == s24_coordinate: # do not support update here + return ChangeSet() + elif type == s25_vertex: + return set_vertex(name, cs) + elif type == s26_label: + return set_label(name, cs) + elif type == s27_backdrop: + return set_backdrop(name, cs) + elif type == s28_end: # end + return ChangeSet() + elif type == s29_scada_device: + return set_scada_device(name, cs) + elif type == s30_scada_device_data: + return set_scada_device_data(name, cs) + elif type == s31_scada_element: + return set_scada_element(name, cs) + elif type == s32_region: + return set_region(name, cs) + elif type == s33_dma: + return set_district_metering_area(name, cs) + elif type == s34_sa: + return set_service_area(name, cs) + elif type == s35_vd: + return set_virtual_district(name, cs) + + return ChangeSet() + + +def _execute_delete_command(name: str, cs: ChangeSet) -> ChangeSet: + type = cs.operations[0]['type'] + + if type == s1_title: + return ChangeSet() + if type == s2_junction: + return delete_junction(name, cs) + elif type == s3_reservoir: + return delete_reservoir(name, cs) + elif type == s4_tank: + return delete_tank(name, cs) + elif type == s5_pipe: + return delete_pipe(name, cs) + elif type == s6_pump: + return delete_pump(name, cs) + elif type == s7_valve: + return delete_valve(name, cs) + elif type == s8_tag: + return ChangeSet() + elif type == s9_demand: + return ChangeSet() + elif type == s10_status: + return ChangeSet() + elif type == s11_pattern: + return delete_pattern(name, cs) + elif type == s12_curve: + return delete_curve(name, cs) + elif type == s13_control: + return ChangeSet() + elif type == s14_rule: + return ChangeSet() + elif type == s15_energy: + return ChangeSet() + elif type == s15_pump_energy: + return ChangeSet() + elif type == s16_emitter: + return ChangeSet() + elif type == s17_quality: + return ChangeSet() + elif type == s18_source: + return delete_source(name, cs) + elif type == s19_reaction: + return ChangeSet() + elif type == s19_pipe_reaction: + return ChangeSet() + elif type == s19_tank_reaction: + return ChangeSet() + elif type == s20_mixing: + return delete_mixing(name, cs) + elif type == s21_time: + return ChangeSet() + elif type == s22_report: + return ChangeSet() + elif type == s23_option: + return ChangeSet() + elif type == s23_option_v3: + return ChangeSet() + elif type == s24_coordinate: + return ChangeSet() + elif type == s25_vertex: + return delete_vertex(name, cs) + elif type == s26_label: + return delete_label(name, cs) + elif type == s27_backdrop: + return ChangeSet() + elif type == s28_end: + return ChangeSet() + elif type == s29_scada_device: + return delete_scada_device(name, cs) + elif type == s30_scada_device_data: + return delete_scada_device_data(name, cs) + elif type == s31_scada_element: + return delete_scada_element(name, cs) + elif type == s32_region: + return delete_region(name, cs) + elif type == s33_dma: + return delete_district_metering_area(name, cs) + elif type == s34_sa: + return delete_service_area(name, cs) + elif type == s35_vd: + return delete_virtual_district(name, cs) + + return ChangeSet() + + +def execute_batch_commands(name: str, cs: ChangeSet) -> ChangeSet: + new_cs = ChangeSet() + for op in cs.operations: + new_cs.merge(rewrite_batch_api(name, ChangeSet(op))) + + result = ChangeSet() + + todo = {} + + try: + for op in new_cs.operations: + todo = op + operation = op['operation'] + if operation == API_ADD: + result.merge(_execute_add_command(name, ChangeSet(op))) + elif operation == API_UPDATE: + result.merge(_execute_update_command(name, ChangeSet(op))) + elif operation == API_DELETE: + result.merge(_execute_delete_command(name, ChangeSet(op))) + except: + print(f'ERROR: Fail to execute {todo}') + + return result + + +def execute_batch_command(name: str, cs: ChangeSet) -> ChangeSet: + write(name, 'delete from batch_operation where id > 0') + write(name, "update operation_table set option = 'batch_operation' where option = 'operation'") + + new_cs = ChangeSet() + for op in cs.operations: + new_cs.merge(rewrite_batch_api(name, ChangeSet(op))) + + result = ChangeSet() + + todo = {} + + try: + for op in new_cs.operations: + todo = op + operation = op['operation'] + if operation == API_ADD: + result.merge(_execute_add_command(name, ChangeSet(op))) + elif operation == API_UPDATE: + result.merge(_execute_update_command(name, ChangeSet(op))) + elif operation == API_DELETE: + result.merge(_execute_delete_command(name, ChangeSet(op))) + except: + print(f'ERROR: Fail to execute {todo}') + + count = read(name, 'select count(*) as count from batch_operation')['count'] + if count == 1: + write(name, 'delete from batch_operation where id > 0') + write(name, "update operation_table set option = 'operation' where option = 'batch_operation'") + return ChangeSet() + + redo_list: list[str] = [] + redo_cs_list: list[dict[str, Any]] = [] + redo_rows = read_all(name, 'select redo, redo_cs from batch_operation where id > 0 order by id asc') + for row in redo_rows: + redo_list.append(row['redo']) + redo_cs_list += eval(row['redo_cs']) + + undo_list: list[str] = [] + undo_cs_list: list[dict[str, Any]] = [] + undo_rows = read_all(name, 'select undo, undo_cs from batch_operation where id > 0 order by id desc') + for row in undo_rows: + undo_list.append(row['undo']) + undo_cs_list += eval(row['undo_cs']) + + redo = '\n'.join(redo_list).replace("'", "''") + redo_cs = str(redo_cs_list).replace("'", "''") + undo = '\n'.join(undo_list).replace("'", "''") + undo_cs = str(undo_cs_list).replace("'", "''") + + parent = get_current_operation(name) + write(name, f"insert into operation (id, redo, undo, parent, redo_cs, undo_cs) values (default, '{redo}', '{undo}', {parent}, '{redo_cs}', '{undo_cs}')") + current = read(name, 'select max(id) as id from operation')['id'] + write(name, f"update current_operation set id = {current}") + + write(name, 'delete from batch_operation where id > 0') + write(name, "update operation_table set option = 'operation' where option = 'batch_operation'") + + return result diff --git a/app/native/api/clean_api.py b/app/native/api/clean_api.py new file mode 100644 index 0000000..ba3d7f8 --- /dev/null +++ b/app/native/api/clean_api.py @@ -0,0 +1,45 @@ +from .database import ChangeSet, read_all +from .batch_exe import execute_batch_command + +# TODO: merge to batch_api + +def clean_scada_device_cs(name: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, 'select id from scada_device acs') + for row in rows: + cs.delete({ 'type': 'scada_device', 'id': row['id'] }) + + return cs + + +def clean_scada_device_data_cs(name: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, 'select distinct device_id from scada_device_data acs') + for row in rows: + cs.update({ 'type': 'scada_device_data', 'device_id': row['device_id'], 'data': [] }) + + return cs + + +def clean_scada_element_cs(name: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, 'select id from scada_element acs') + for row in rows: + cs.delete({ 'type': 'scada_element', 'id': row['id'] }) + + return cs + + +def clean_scada_device(name: str) -> ChangeSet: + return execute_batch_command(name, clean_scada_device_cs(name)) + + +def clean_scada_device_data(name: str) -> ChangeSet: + return execute_batch_command(name, clean_scada_device_data_cs(name)) + + +def clean_scada_element(name: str) -> ChangeSet: + return execute_batch_command(name, clean_scada_element_cs(name)) diff --git a/app/native/api/connection.py b/app/native/api/connection.py new file mode 100644 index 0000000..b42b481 --- /dev/null +++ b/app/native/api/connection.py @@ -0,0 +1,3 @@ +import psycopg as pg + +g_conn_dict : dict[str, pg.Connection] = {} \ No newline at end of file diff --git a/app/native/api/database.py b/app/native/api/database.py new file mode 100644 index 0000000..c043d03 --- /dev/null +++ b/app/native/api/database.py @@ -0,0 +1,349 @@ +from typing import Any +from psycopg.rows import dict_row, Row +from .connection import g_conn_dict as conn + +API_ADD = 'add' +API_UPDATE = 'update' +API_DELETE = 'delete' + +g_add_prefix = { 'operation': API_ADD } +g_update_prefix = { 'operation': API_UPDATE } +g_delete_prefix = { 'operation': API_DELETE } + + +class ChangeSet: + def __init__(self, ps: dict[str, Any] | None = None): + self.operations : list[dict[str, Any]] = [] + if ps != None: + self.append(ps) + + @staticmethod + def from_list(ps: list[dict[str, Any]]): + cs = ChangeSet() + for _cs in ps: + cs.append(_cs) + return cs + + def add(self, ps: dict[str, Any]): + self.operations.append(g_add_prefix | ps) + return self + + def update(self, ps: dict[str, Any]): + self.operations.append(g_update_prefix | ps) + return self + + def delete(self, ps: dict[str, Any]): + self.operations.append(g_delete_prefix | ps) + return self + + def append(self, ps: dict[str, Any]): + self.operations.append(ps) + return self + + def merge(self, cs): + if len(cs.operations) > 0: + self.operations += cs.operations + return self + + def dump(self): + for op in self.operations: + print(op) + + def compress(self): + return self + + +class DbChangeSet: + def __init__(self, redo_sql: str, undo_sql: str, redo_cs: list[dict[str, Any]], undo_cs: list[dict[str, Any]]) -> None: + self.redo_sql = redo_sql + self.undo_sql = undo_sql + self.redo_cs = redo_cs + self.undo_cs = undo_cs + + @staticmethod + def from_list(css): + redo_sql_s : list[str] = [] + undo_sql_s : list[str] = [] + redo_cs_s : list[dict[str, Any]] = [] + undo_cs_s : list[dict[str, Any]] = [] + + for r in css: + redo_sql_s.append(r.redo_sql) + undo_sql_s.append(r.undo_sql) + redo_cs_s += r.redo_cs + r.undo_cs.reverse() # reverse again... + undo_cs_s += r.undo_cs + + redo_sql = '\n'.join(redo_sql_s) + undo_sql_s.reverse() + undo_sql = '\n'.join(undo_sql_s) + undo_cs_s.reverse() + + return DbChangeSet(redo_sql, undo_sql, redo_cs_s, undo_cs_s) + + +def read(name: str, sql: str) -> Row: + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(sql) + row = cur.fetchone() + if row == None: + raise Exception(sql) + return row + + +def read_all(name: str, sql: str) -> list[Row]: + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(sql) + return cur.fetchall() + + +def try_read(name: str, sql: str) -> Row | None: + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(sql) + return cur.fetchone() + + +def write(name: str, sql: str) -> None: + with conn[name].cursor() as cur: + cur.execute(sql) + + +def get_current_operation(name: str) -> int: + return int(read(name, 'select id from current_operation')['id']) + + +def execute_command(name: str, command: DbChangeSet, undo_redo: bool = True) -> ChangeSet: + write(name, command.redo_sql) + + if undo_redo: + op_table = read(name, "select * from operation_table")['option'] + parent = get_current_operation(name) + redo_sql = command.redo_sql.replace("'", "''") + undo_sql = command.undo_sql.replace("'", "''") + redo_cs_str = str(command.redo_cs).replace("'", "''") + undo_cs_str = str(command.undo_cs).replace("'", "''") + write(name, f"insert into {op_table} (id, redo, undo, parent, redo_cs, undo_cs) values (default, '{redo_sql}', '{undo_sql}', {parent}, '{redo_cs_str}', '{undo_cs_str}')") + + if op_table == 'operation': + current = read(name, 'select max(id) as id from operation')['id'] + write(name, f"update current_operation set id = {current}") + + return ChangeSet.from_list(command.redo_cs) + + +def execute_undo(name: str, discard: bool = False) -> ChangeSet: + row = read(name, f'select * from operation where id = {get_current_operation(name)}') + + write(name, row['undo']) + + # update foreign key + write(name, f"update current_operation set id = {row['parent']} where id = {row['id']}") + + if discard: + # update foreign key + write(name, f"update operation set redo_child = null where id = {row['parent']}") + # on delete cascade => child & snapshot + write(name, f"delete from operation where id = {row['id']}") + else: + write(name, f"update operation set redo_child = {row['id']} where id = {row['parent']}") + + e = eval(row['undo_cs']) + return ChangeSet.from_list(e) + + +def execute_redo(name: str) -> ChangeSet: + row = read(name, f'select * from operation where id = {get_current_operation(name)}') + if row['redo_child'] == None: + return ChangeSet() + + row = read(name, f"select * from operation where id = {row['redo_child']}") + write(name, row['redo']) + + write(name, f"update current_operation set id = {row['id']} where id = {row['parent']}") + + e = eval(row['redo_cs']) + return ChangeSet.from_list(e) + + +def list_snapshot(name: str) -> list[tuple[int, str]]: + rows = read_all(name, f'select * from snapshot_operation order by id') + result = [] + for row in rows: + result.append((int(row['id']), str(row['tag']))) + return result + + +def have_snapshot(name: str, tag: str) -> bool: + return try_read(name, f"select id from snapshot_operation where tag = '{tag}'") != None + + +def have_snapshot_for_operation(name: str, operation: int) -> bool: + return try_read(name, f"select id from snapshot_operation where id = {operation}") != None + + +def have_snapshot_for_current_operation(name: str) -> bool: + return have_snapshot_for_operation(name, get_current_operation(name)) + + +def take_snapshot_for_operation(name: str, operation: int, tag: str) -> None: + if tag == None or tag == '': + return None + write(name, f"insert into snapshot_operation (id, tag) values ({operation}, '{tag}')") + + +def take_snapshot_for_current_operation(name: str, tag: str) -> None: + take_snapshot_for_operation(name, get_current_operation(name), tag) + + +# deprecated ! use take_snapshot_for_current_operation instead +def take_snapshot(name: str, tag: str) -> None: + take_snapshot_for_current_operation(name, tag) + + +def update_snapshot(name: str, operation: int, tag: str) -> None: + if tag == None or tag == '': + return None + if have_snapshot_for_operation(name, operation): + write(name, f"update snapshot_operation set tag = '{tag}' where id = {operation}") + else: + take_snapshot_for_operation(name, operation, tag) + + +def update_snapshot_for_current_operation(name: str, tag: str) -> None: + return update_snapshot(name, get_current_operation(name), tag) + + +def delete_snapshot(name: str, tag: str) -> None: + write(name, f"delete from snapshot_operation where tag = '{tag}'") + + +def delete_snapshot_by_operation(name: str, operation: int) -> None: + write(name, f"delete from snapshot_operation where id = {operation}") + + +def get_operation_by_snapshot(name: str, tag: str) -> int | None: + row = try_read(name, f"select id from snapshot_operation where tag = '{tag}'") + return int(row['id']) if row != None else None + + +def get_snapshot_by_operation(name: str, operation: int) -> str | None: + row = try_read(name, f"select tag from snapshot_operation where id = {operation}") + return str(row['tag']) if row != None else None + + +def _get_parents(name: str, id: int) -> list[int]: + ids = [id] + while ids[-1] != 0: + row = read(name, f'select parent from operation where id = {ids[-1]}') + ids.append(int(row['parent'])) + return ids + + +def pick_operation(name: str, operation: int, discard: bool) -> ChangeSet: + target = operation + curr = get_current_operation(name) + + curr_parents = _get_parents(name, curr) + target_parents = _get_parents(name, target) + + change = ChangeSet() + + if target in curr_parents: + for _ in range(curr_parents.index(target)): + change.merge(execute_undo(name, discard)) + + elif curr in target_parents: + target_parents.reverse() + curr_index = target_parents.index(curr) + for i in range(curr_index, len(target_parents) - 1): + write(name, f"update operation set redo_child = '{target_parents[i + 1]}' where id = '{target_parents[i]}'") + change.merge(execute_redo(name)) + + else: + ancestor_index = -1 + while curr_parents[ancestor_index] == target_parents[ancestor_index]: + ancestor_index -= 1 + ancestor = curr_parents[ancestor_index + 1] + + for _ in range(curr_parents.index(ancestor)): + change.merge(execute_undo(name, discard)) + + target_parents.reverse() + curr_index = target_parents.index(ancestor) + for i in range(curr_index, len(target_parents) - 1): + write(name, f"update operation set redo_child = '{target_parents[i + 1]}' where id = '{target_parents[i]}'") + change.merge(execute_redo(name)) + + return change.compress() + + +def pick_snapshot(name: str, tag: str, discard: bool) -> ChangeSet: + if not have_snapshot(name, tag): + return ChangeSet() + + target = int(read(name, f"select id from snapshot_operation where tag = '{tag}'")['id']) + return pick_operation(name, target, discard) + + +def _get_change_set(name: str, operation: int, undo: bool) -> ChangeSet: + row = read(name, f'select * from operation where id = {operation}') + field= 'undo_cs' if undo else 'redo_cs' + return ChangeSet.from_list(eval(row[field])) + + +def sync_with_server(name: str, operation: int) -> ChangeSet: + fr = operation + to = get_current_operation(name) + + fr_parents = _get_parents(name, fr) + to_parents = _get_parents(name, to) + + change = ChangeSet() + + if fr in to_parents: + index = to_parents.index(fr) - 1 + while index >= 0: + change.merge(_get_change_set(name, to_parents[index], False)) #redo + index -= 1 + + elif to in fr_parents: + index = 0 + while index <= fr_parents.index(to) - 1: + change.merge(_get_change_set(name, fr_parents[index], True)) + index += 1 + + else: + ancestor_index = -1 + while fr_parents[ancestor_index] == to_parents[ancestor_index]: + ancestor_index -= 1 + + ancestor = fr_parents[ancestor_index + 1] + + index = 0 + while index <= fr_parents.index(ancestor) - 1: + change.merge(_get_change_set(name, fr_parents[index], True)) + index += 1 + + index = to_parents.index(ancestor) - 1 + while index >= 0: + change.merge(_get_change_set(name, to_parents[index], False)) + index -= 1 + + return change.compress() + + +def get_restore_operation(name: str) -> int: + return read(name, f'select * from restore_operation')['id'] + + +def set_restore_operation(name: str, operation: int) -> None: + write(name, f'update restore_operation set id = {operation}') + + +def set_restore_operation_to_current(name: str) -> None: + return set_restore_operation(name, get_current_operation(name)) + + +def restore(name: str, discard: bool) -> ChangeSet: + op = get_restore_operation(name) + return pick_operation(name, op, discard) diff --git a/app/native/api/extension_data.py b/app/native/api/extension_data.py new file mode 100644 index 0000000..cfce4ea --- /dev/null +++ b/app/native/api/extension_data.py @@ -0,0 +1,62 @@ +from .database import * + + +def get_all_extension_data_keys(name: str) -> list[str]: + result: list[str] = [] + for row in read_all(name, 'select key from extension_data'): + result.append(row['key']) + return result + + +def get_all_extension_data(name: str) -> dict[str, Any]: + result: dict[str, Any] = {} + for row in read_all(name, 'select key, value from extension_data'): + result[row['key']] = row['value'] + return result + + +def get_extension_data(name: str, key: str) -> str | None: + if key == None or key == '': + return None + row = try_read(name, f"select value from extension_data where key = '{key}'") + if row == None: + return None + return row['value'] + + +def _set_extension_data(name: str, cs: ChangeSet) -> DbChangeSet: + op = cs.operations[0] + key, new_val = op['key'], op['value'] + + f_new_val = f"'{new_val}'" if new_val != None else 'null' + + old_val = get_extension_data(name, key) + f_old_val = f"'{old_val}'" if old_val != None else 'null' + + redo_sql = f"delete from extension_data where key = '{key}';" + if new_val != None: + redo_sql += f"insert into extension_data (key, value) values ('{key}', {f_new_val});" + + undo_sql = f"delete from extension_data where key = '{key}';" + if old_val != None: + undo_sql += f"insert into extension_data (key, value) values ('{key}', {f_old_val});" + + redo_cs = g_update_prefix | { 'type': 'extension_data', 'key': key, 'value': new_val } + undo_cs = g_update_prefix | { 'type': 'extension_data', 'key': key, 'value': old_val } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_extension_data(name: str, cs: ChangeSet) -> ChangeSet: + if len(cs.operations) != 1: + return ChangeSet() + + op = cs.operations[0] + if 'key' not in op or 'value' not in op: + return ChangeSet() + + key = op['key'] + if key == None or key == '': + return ChangeSet() + + return execute_command(name, _set_extension_data(name, cs)) diff --git a/app/native/api/inp_in.py b/app/native/api/inp_in.py new file mode 100644 index 0000000..ac77464 --- /dev/null +++ b/app/native/api/inp_in.py @@ -0,0 +1,428 @@ +import datetime +import os +from .project_backup import * +from .database import ChangeSet, write +from .sections import * +from .s0_base import get_region_type +from .s1_title import inp_in_title +from .s2_junctions import inp_in_junction +from .s3_reservoirs import inp_in_reservoir +from .s4_tanks import inp_in_tank +from .s5_pipes import inp_in_pipe +from .s6_pumps import inp_in_pump +from .s7_valves import inp_in_valve +from .s8_tags import inp_in_tag +from .s9_demands import inp_in_demand +from .s10_status import inp_in_status +from .s11_patterns import pattern_v3_types, inp_in_pattern +from .s12_curves import curve_types, inp_in_curve +from .s13_controls import inp_in_control +from .s14_rules import inp_in_rule +from .s15_energy import inp_in_energy +from .s16_emitters import inp_in_emitter +from .s17_quality import inp_in_quality +from .s18_sources import inp_in_source +from .s19_reactions import inp_in_reaction +from .s20_mixing import inp_in_mixing +from .s21_times import inp_in_time +from .s22_report import inp_in_report +from .s23_options import inp_in_option +from .s23_options_v3 import inp_in_option_v3 +from .s24_coordinates import inp_in_coord +from .s25_vertices import inp_in_vertex +from .s26_labels import inp_in_label +from .s27_backdrop import inp_in_backdrop +from .s32_region import inp_in_region,inp_in_bound,inp_in_regionnodes +from .s32_region_util import from_postgis_polygon,to_postgis_polygon + +#DingZQ, 2024-12-28, export inp +from .inp_out import export_inp + +_S = 'S' +_L = 'L' + +def _inp_in_option(section: list[str], version: str = '3') -> str: + return inp_in_option_v3(section) if version == '3' else inp_in_option(section) + +_handler = { + TITLE : (_S, inp_in_title), + JUNCTIONS : (_L, inp_in_junction), # line, demand_outside + RESERVOIRS : (_L, inp_in_reservoir), + TANKS : (_L, inp_in_tank), + PIPES : (_L, inp_in_pipe), + PUMPS : (_L, inp_in_pump), + VALVES : (_L, inp_in_valve), + TAGS : (_L, inp_in_tag), + DEMANDS : (_L, inp_in_demand), + STATUS : (_L, inp_in_status), + PATTERNS : (_L, inp_in_pattern), # line, fixed + CURVES : (_L, inp_in_curve), + CONTROLS : (_L, inp_in_control), + RULES : (_L, inp_in_rule), + ENERGY : (_L, inp_in_energy), + EMITTERS : (_L, inp_in_emitter), + QUALITY : (_L, inp_in_quality), + SOURCES : (_L, inp_in_source), + REACTIONS : (_L, inp_in_reaction), + MIXING : (_L, inp_in_mixing), + TIMES : (_S, inp_in_time), + REPORT : (_S, inp_in_report), + OPTIONS : (_S, _inp_in_option), # line, version + COORDINATES : (_L, inp_in_coord), + VERTICES : (_L, inp_in_vertex), + REGION : (_L, inp_in_region), + BOUND : (_L, inp_in_bound), + REGION_NODES : (_L, inp_in_regionnodes), + LABELS : (_L, inp_in_label), + BACKDROP : (_S, inp_in_backdrop), + #END : 'END', +} + +_level_1 = [ + TITLE, + PATTERNS, + CURVES, + CONTROLS, + RULES, + TIMES, + REPORT, + OPTIONS, + BACKDROP, +] + +_level_2 = [ + JUNCTIONS, + RESERVOIRS, + TANKS, +] + +_level_3 = [ + PIPES, + PUMPS, + VALVES, + DEMANDS, + EMITTERS, + QUALITY, + SOURCES, + MIXING, + COORDINATES, + LABELS, +] + +_level_4 = [ + TAGS, + STATUS, + ENERGY, + REACTIONS, + VERTICES, + REGION, + BOUND, + REGION_NODES, +] + +map_regiontype={ + # map the region types from desktop to server + 'DISTRIBUTION':'WDA', + 'DMA':'DMA', + 'PMA':'PMA', + 'VD':'VD', + 'SA':'SA', +} +class SQLBatch: + def __init__(self, project: str, count: int = 100) -> None: + self.batch: list[str] = [] + self.project = project + self.count = count + + def add(self, sql: str) -> None: + self.batch.append(sql) + if len(self.batch) == self.count: + self.flush() + + def flush(self) -> None: + write(self.project, ''.join(self.batch)) + self.batch.clear() + + +def _print_time(desc: str) -> datetime.datetime: + now = datetime.datetime.now() + time = now.strftime('%Y-%m-%d %H:%M:%S') + print(f"{time}: {desc}") + return now + + +def _get_file_offset(inp: str) -> tuple[dict[str, list[int]], bool]: + offset: dict[str, list[int]] = {} + + current = '' + demand_outside = False + + with open(inp) as f: + while True: + line = f.readline() + if not line: + break + + line = line.strip() + if line.startswith('['): + for s in section_name: + if line.startswith(f'[{s}'): + if s not in offset: + offset[s] = [] + offset[s].append(f.tell()) + current = s + break + elif line != '' and line.startswith(';') == False: + if current == DEMANDS: + demand_outside = True + + return (offset, demand_outside) + + +def parse_file(project: str, inp: str, version: str = '3') -> None: + start = _print_time(f'Start reading file "{inp}"...') + + _print_time("First scan...") + offset, demand_outside = _get_file_offset(inp) + + levels = _level_1 + _level_2 + _level_3 + _level_4 + + # parse the whole section rather than line + sections : dict[str, list[str]]= {} + for [s, t] in _handler.items(): + if t[0] == _S: + sections[s] = [] + + variable_patterns = [] + current_pattern = None + current_curve = None + curve_type_desc_line = None + current_region =None + current_bound=[] + current_bound.clear() + region_list={} + current_region_nodes=[] + current_region_nodes.clear() + + sql_batch = SQLBatch(project) + _print_time("Second scan...") + with open(inp) as f: + for s in levels: + if s not in offset: + continue + + if s == DEMANDS and demand_outside == False: + continue + + _print_time(f"[{s}]") + + is_s = _handler[s][0] == _S + handler = _handler[s][1] + + for ptr in offset[s]: + f.seek(ptr) + + while True: + line = f.readline() + if not line: + break + + line = line.strip() + if line.startswith('['): + break + elif line == '': + continue + + if is_s: + sections[s].append(line) + else: + if line.startswith(';'): + if version != '3': #v2 + line = line.removeprefix(';') + if s == PATTERNS: # ;desc + pass + elif s == CURVES: # ;type: desc + curve_type_desc_line = line + continue + + if s == PATTERNS: + tokens = line.split() + + if tokens[1].upper() in pattern_v3_types: #v3 + sql_batch.add(f"insert into _pattern (id) values ('{tokens[0]}');") + current_pattern = tokens[0] + if tokens[1].upper() == 'VARIABLE': + variable_patterns.append(tokens[0]) + continue + + if current_pattern != tokens[0]: + sql_batch.add(f"insert into _pattern (id) values ('{tokens[0]}');") + current_pattern = tokens[0] + + elif s == CURVES: + tokens = line.split() + + if tokens[1].upper() in curve_types: #v3 + sql_batch.add(f"insert into _curve (id, type) values ('{tokens[0]}', '{tokens[1].upper()}');") + current_curve = tokens[0] + continue + + if current_curve != tokens[0]: + type = curve_types[0] + if curve_type_desc_line != None: + type = curve_type_desc_line.split(':')[0].strip() + sql_batch.add(f"insert into _curve (id, type) values ('{tokens[0]}', '{type}');") + current_curve = tokens[0] + curve_type_desc_line = None + elif s== REGION: + tokens = line.split() + region_list[tokens[0]]=tokens[1] + elif s == BOUND: + tokens = line.split() + if(tokens[0]!=current_region and len(current_bound)>0): + #insert the previous region after get all the vertex of the attatched geometry + current_bound.append(current_bound[0]) + current_geometry=to_postgis_polygon(current_bound) + region_type=map_regiontype[region_list[tokens[0]]] + sql_batch.add(f"insert into region(id, boundary,r_type) values ('{current_region}', '{current_geometry}','{region_type}');") + #start the new region + current_bound.clear() + vertex_point=(float(tokens[1]),float(tokens[2])) + current_bound.append(vertex_point) + current_region=tokens[0] + elif s==REGION_NODES: + tokens = line.split() + if(tokens[0]!=current_region and len(current_region_nodes)>0): + #insert the previous region after get all the vertex of the attatched geometry + sql_batch.add(get_insert_into_region_sql(current_region,current_region_nodes)) + #start the new region + current_region_nodes.clear() + current_region_nodes.append(tokens[1]) + current_region=tokens[0] + if s == JUNCTIONS: + sql_batch.add(handler(line, demand_outside)) + elif s == PATTERNS: + sql_batch.add(handler(line, current_pattern not in variable_patterns)) + elif s==BOUND or s==REGION_NODES: + continue + else: + sql_batch.add(handler(line)) + + f.seek(0) + + if is_s: + if s == OPTIONS: + sql_batch.add(handler(sections[s], version)) + else: + sql_batch.add(handler(sections[s])) + #need to insert the last region into database + if len(current_bound)>0: + current_bound.append(current_bound[0]) + current_geometry=to_postgis_polygon(current_bound) + region_type=map_regiontype[region_list[current_region]] + sql_batch.add(f"insert into region(id, boundary,r_type) values ('{current_region}', '{current_geometry}','{region_type}');") + #reset the current region to none for the [REGION_NODES] session reading + #current_region=None + #need to insert the last region_nodes into database + if len(current_region_nodes)>0: + sql_batch.add(get_insert_into_region_sql(current_region,current_region_nodes)) + #current_region=None + sql_batch.flush() + + end = _print_time(f'End reading file "{inp}"') + print(f"Total (in second): {(end-start).seconds}(s)") + +def get_insert_into_region_sql(region:str,nodes:list[str])->str: + str_sql='' + str_nodes = str(nodes).replace("'", "''") + r_type=region[0:region.index('_')] + if r_type == 'DMA' or r_type == 'SA' or r_type == 'VD': + table = '' + if r_type == 'DMA': + table = 'region_dma' + elif r_type == 'SA': + table = 'region_sa' + source=region[region.index('_')+1:] + str_sql=f"insert into region_sa(id,time_index,source,nodes) values ('{region}', 0,'{source}','{str_nodes}');" + elif r_type == 'VD': + table = 'region_vd' + + return str_sql + +def read_inp(project: str, inp: str, version: str = '3') -> bool: + if version != '3' and version != '2': + version = '2' + + if is_project_open(project): + close_project(project) + + if have_project(project): + delete_project(project) + + create_project(project) + open_project(project) + + parse_file(project, inp, version) + + '''try: + parse_file(project, inp, version) + except: + close_project(project) + delete_project(project) + return False''' + + close_project(project) + return True + +#DingZQ, 2024-12-28, convert v3 to v2 +def convert_inp_v3_to_v2(inp: str) -> ChangeSet: + project = 'v3Tov2' + + if is_project_open(project): + close_project(project) + + if have_project(project): + delete_project(project) + + create_project(project) + open_project(project) + + filename = f'inp/{project}_temp.inp' + if os.path.exists(filename): + os.remove(filename) + + with open(filename, 'w') as f: + f.write(inp) + + parse_file(project, filename, '3') + + '''try: + parse_file(project, inp, version) + except: + close_project(project) + delete_project(project) + return False''' + + return export_inp(project, '2') + +def import_inp(project: str, cs: ChangeSet, version: str = '3') -> bool: + if version != '3' and version != '2': + version = '2' + + if 'inp' not in cs.operations[0]: + return False + + filename = f'inp/{project}_temp.inp' + if os.path.exists(filename): + os.remove(filename) + + _print_time(f'Start writing temp file "{filename}"...') + with open(filename, 'w',encoding="GBK") as f: + f.write(str(cs.operations[0]['inp'])) + _print_time(f'End writing temp file "{filename}"...') + + result = read_inp(project, filename, version) + + #os.remove(filename) + + return result diff --git a/app/native/api/inp_out.py b/app/native/api/inp_out.py new file mode 100644 index 0000000..6cf3f0d --- /dev/null +++ b/app/native/api/inp_out.py @@ -0,0 +1,287 @@ +import os +from .project_backup import * +from .database import ChangeSet +from .sections import * +from .s1_title import inp_out_title +from .s2_junctions import inp_out_junction +from .s3_reservoirs import inp_out_reservoir +from .s4_tanks import inp_out_tank +from .s5_pipes import inp_out_pipe +from .s6_pumps import inp_out_pump +from .s7_valves import inp_out_valve +from .s8_tags import inp_out_tag +from .s9_demands import inp_out_demand +from .s10_status import inp_out_status +from .s11_patterns import inp_out_pattern, inp_out_pattern_v3 +from .s12_curves import inp_out_curve, inp_out_curve_v3 +from .s13_controls import inp_out_control +from .s14_rules import inp_out_rule +from .s15_energy import inp_out_energy +from .s16_emitters import inp_out_emitter +from .s17_quality import inp_out_quality +from .s18_sources import inp_out_source +from .s19_reactions import inp_out_reaction +from .s20_mixing import inp_out_mixing +from .s21_times import inp_out_time +from .s22_report import inp_out_report +from .s23_options import inp_out_option +from .s23_options_v3 import inp_out_option_v3 +from .s24_coordinates import inp_out_coord +from .s25_vertices import inp_out_vertex +from .s26_labels import inp_out_label +from .s27_backdrop import inp_out_backdrop +#from .s28_end import * + + +def dump_inp(project: str, inp: str, version: str = '3'): + if version != '3' and version != '2': + version = '2' + + if not have_project(project): + return + + project_open = is_project_open(project) + + if not project_open: + open_project(project) + + dir = os.getcwd() + path = os.path.join(dir, inp) + + if os.path.exists(path): + os.remove(path) + + file = open(path, mode='w',encoding="UTF-8") + + # REGION, BOUND, REGION_NODES 在 epanet v2 中没有,是我们自己定制的 + # v2 需要去掉我们自己定制的 section + sections = section_names_for_epanetv2 + if version == '3': + sections = section_name + + for name in sections: + if name == TITLE: + file.write(f'[{name}]\n') + else: + file.write(f'\n[{name}]\n') + + if name == TITLE: + file.write('\n'.join(inp_out_title(project))) + + elif name == JUNCTIONS: # + coords + file.write('\n'.join(inp_out_junction(project))) + + elif name == RESERVOIRS: # + coords + file.write('\n'.join(inp_out_reservoir(project))) + + elif name == TANKS: # + coords + file.write('\n'.join(inp_out_tank(project))) + + elif name == PIPES: + file.write('\n'.join(inp_out_pipe(project))) + + elif name == PUMPS: + file.write('\n'.join(inp_out_pump(project))) + + elif name == VALVES: + file.write('\n'.join(inp_out_valve(project))) + + elif name == TAGS: + file.write('\n'.join(inp_out_tag(project))) + + elif name == DEMANDS: + file.write('\n'.join(inp_out_demand(project))) + + elif name == STATUS: + file.write('\n'.join(inp_out_status(project))) + + elif name == PATTERNS: + if version == '3': + file.write('\n'.join(inp_out_pattern_v3(project))) + else: + file.write('\n'.join(inp_out_pattern(project))) + + elif name == CURVES: + if version == '3': + file.write('\n'.join(inp_out_curve_v3(project))) + else: + file.write('\n'.join(inp_out_curve(project))) + + elif name == CONTROLS: + file.write('\n'.join(inp_out_control(project))) + + elif name == RULES: + file.write('\n'.join(inp_out_rule(project))) + + elif name == ENERGY: + file.write('\n'.join(inp_out_energy(project))) + + elif name == EMITTERS: + file.write('\n'.join(inp_out_emitter(project))) + + elif name == QUALITY: + file.write('\n'.join(inp_out_quality(project))) + + elif name == SOURCES: + file.write('\n'.join(inp_out_source(project))) + + elif name == REACTIONS: + file.write('\n'.join(inp_out_reaction(project))) + + elif name == MIXING: + file.write('\n'.join(inp_out_mixing(project))) + + elif name == TIMES: + file.write('\n'.join(inp_out_time(project))) + + elif name == REPORT: + file.write('\n'.join(inp_out_report(project))) + + elif name == OPTIONS: + if version == '3': + file.write('\n'.join(inp_out_option_v3(project))) + else: + file.write('\n'.join(inp_out_option(project))) + + elif name == COORDINATES: + file.write('\n'.join(inp_out_coord(project))) + + elif name == VERTICES: + file.write('\n'.join(inp_out_vertex(project))) + + elif name == LABELS: + file.write('\n'.join(inp_out_label(project))) + + elif name == BACKDROP: + file.write('\n'.join(inp_out_backdrop(project))) + + elif name == END: + pass # :) + + file.write('\n') + + file.close() + + if not project_open: + close_project(project) + + +def export_inp(project: str, version: str = '3') -> ChangeSet: + if version != '3' and version != '2': + version = '2' + + if not have_project(project): + return ChangeSet() + + project_open = is_project_open(project) + + if not project_open: + open_project(project) + + inp = '' + + for name in section_name: + if name == TITLE: + inp += f'[{name}]\n' + else: + inp += f'\n[{name}]\n' + + if name == TITLE: + inp += '\n'.join(inp_out_title(project)) + + elif name == JUNCTIONS: # + coords + inp += '\n'.join(inp_out_junction(project)) + + elif name == RESERVOIRS: # + coords + inp += '\n'.join(inp_out_reservoir(project)) + + elif name == TANKS: # + coords + inp += '\n'.join(inp_out_tank(project)) + + elif name == PIPES: + inp += '\n'.join(inp_out_pipe(project)) + + elif name == PUMPS: + inp += '\n'.join(inp_out_pump(project)) + + elif name == VALVES: + inp += '\n'.join(inp_out_valve(project)) + + elif name == TAGS: + inp += '\n'.join(inp_out_tag(project)) + + elif name == DEMANDS: + inp += '\n'.join(inp_out_demand(project)) + + elif name == STATUS: + inp += '\n'.join(inp_out_status(project)) + + elif name == PATTERNS: + if version == '3': + inp += '\n'.join(inp_out_pattern_v3(project)) + else: + inp += '\n'.join(inp_out_pattern(project)) + + elif name == CURVES: + if version == '3': + inp += '\n'.join(inp_out_curve_v3(project)) + else: + inp += '\n'.join(inp_out_curve(project)) + + elif name == CONTROLS: + inp += '\n'.join(inp_out_control(project)) + + elif name == RULES: + inp += '\n'.join(inp_out_rule(project)) + + elif name == ENERGY: + inp += '\n'.join(inp_out_energy(project)) + + elif name == EMITTERS: + inp += '\n'.join(inp_out_emitter(project)) + + elif name == QUALITY: + inp += '\n'.join(inp_out_quality(project)) + + elif name == SOURCES: + inp += '\n'.join(inp_out_source(project)) + + elif name == REACTIONS: + inp += '\n'.join(inp_out_reaction(project)) + + elif name == MIXING: + inp += '\n'.join(inp_out_mixing(project)) + + elif name == TIMES: + inp += '\n'.join(inp_out_time(project)) + + elif name == REPORT: + inp += '\n'.join(inp_out_report(project)) + + elif name == OPTIONS: + if version == '3': + inp += '\n'.join(inp_out_option_v3(project)) + else: + inp += '\n'.join(inp_out_option(project)) + + elif name == COORDINATES: + inp += '\n'.join(inp_out_coord(project)) + + elif name == VERTICES: + inp += '\n'.join(inp_out_vertex(project)) + + elif name == LABELS: + inp += '\n'.join(inp_out_label(project)) + + elif name == BACKDROP: + inp += '\n'.join(inp_out_backdrop(project)) + + elif name == END: + pass # :) + + inp += '\n' + + if not project_open: + close_project(project) + + return ChangeSet({'operation': 'export', 'inp': inp}) diff --git a/app/native/api/postgresql_info.py b/app/native/api/postgresql_info.py new file mode 100644 index 0000000..be5b0c4 --- /dev/null +++ b/app/native/api/postgresql_info.py @@ -0,0 +1,36 @@ +from dotenv import load_dotenv +import os + +load_dotenv() + +pg_name = os.getenv("DB_NAME") +pg_host = os.getenv("DB_HOST") +pg_port = os.getenv("DB_PORT") +pg_user = os.getenv("DB_USER") +pg_password = os.getenv("DB_PASSWORD") + + +def get_pgconn_string( + db_name=pg_name, + db_host=pg_host, + db_port=pg_port, + db_user=pg_user, + db_password=pg_password, +): + """返回 PostgreSQL 连接字符串""" + return f"dbname={db_name} host={db_host} port={db_port} user={db_user} password={db_password}" + + +def get_pg_config(): + """返回 PostgreSQL 配置变量的字典""" + return { + "name": pg_name, + "host": pg_host, + "port": pg_port, + "user": pg_user, + } + + +def get_pg_password(): + """返回密码(谨慎使用)""" + return pg_password diff --git a/app/native/api/project.py b/app/native/api/project.py new file mode 100644 index 0000000..fbb572b --- /dev/null +++ b/app/native/api/project.py @@ -0,0 +1,192 @@ +import os +import psycopg as pg +from psycopg import sql +from psycopg.rows import dict_row +from .connection import g_conn_dict as conn +from .postgresql_info import get_pgconn_string, get_pg_config, get_pg_password + +# no undo/redo + +_server_databases = ["template0", "template1", "postgres", "project"] + + +def list_project() -> list[str]: + ps = [] + with pg.connect(conninfo=get_pgconn_string(), autocommit=True) as conn: + with conn.cursor(row_factory=dict_row) as cur: + for p in cur.execute( + f"select datname from pg_database where datname <> 'postgres' and datname <> 'template0' and datname <> 'template1' and datname <> 'project'" + ): + ps.append(p["datname"]) + return ps + + +def have_project(name: str) -> bool: + with pg.connect( + conninfo=get_pgconn_string(db_name="postgres"), autocommit=True + ) as conn: + with conn.cursor() as cur: + cur.execute("select 1 from pg_database where datname = %s", (name,)) + return cur.fetchone() is not None + + +def copy_project(source: str, new: str) -> None: + if source in conn: + conn[source].close() + del conn[source] + + with pg.connect( + conninfo=get_pgconn_string(db_name="postgres"), autocommit=True + ) as admin_conn: + with admin_conn.cursor() as cur: + cur.execute( + "update pg_database set datallowconn = false where datname = %s", + (source,), + ) + try: + cur.execute( + "select pg_terminate_backend(pid) from pg_stat_activity where datname = %s and pid <> pg_backend_pid()", + (source,), + ) + cur.execute( + sql.SQL("create database {} with template = {}").format( + sql.Identifier(new), sql.Identifier(source) + ) + ) + finally: + cur.execute( + "update pg_database set datallowconn = true where datname = %s", + (source,), + ) + + +# 2025-02-07, WMH +# copyproject会把pg中operation这个表的全部内容也加进去,我们实际项目运行一周后operation这个表会变得特别大,导致CopyProject花费的时间很长,CopyProjectEx把operation的在复制时没有一块复制过去,节省时间 +class CopyProjectEx: + @staticmethod + def create_database(connection, new_db): + with connection.cursor() as cursor: + cursor.execute(f'create database "{new_db}"') + connection.commit() + + @staticmethod + def execute_pg_dump(source_db, exclude_table_list): + + os.environ["PGPASSWORD"] = get_pg_password() # 设置密码环境变量 + pg_config = get_pg_config() + host = pg_config["host"] + port = pg_config["port"] + user = pg_config["user"] + dump_command_structure = f"pg_dump -h {host} -p {port} -U {user} -F c -s -f source_db_structure.dump {source_db}" + os.system(dump_command_structure) + + if exclude_table_list is not None: + exclude_table = " ".join(["-T {}".format(i) for i in exclude_table_list]) + dump_command_db = f"pg_dump -h {host} -p {port} -U {user} -F c -a {exclude_table} -f source_db.dump {source_db}" + else: + dump_command_db = f"pg_dump -h {host} -p {port} -U {user} -F c -a -f source_db.dump {source_db}" + os.system(dump_command_db) + + @staticmethod + def execute_pg_restore(new_db): + os.environ["PGPASSWORD"] = get_pg_password() # 设置密码环境变量 + pg_config = get_pg_config() + host = pg_config["host"] + port = pg_config["port"] + user = pg_config["user"] + restore_command_structure = f"pg_restore -h {host} -p {port} -U {user} -d {new_db} source_db_structure.dump" + os.system(restore_command_structure) + + restore_command_db = ( + f"pg_restore -h {host} -p {port} -U {user} -d {new_db} source_db.dump" + ) + os.system(restore_command_db) + + @staticmethod + def init_operation_table(connection, excluded_table): + with connection.cursor() as cursor: + if "operation" in excluded_table: + insert_query = "insert into operation (id, redo, undo, redo_cs, undo_cs) values (0, '', '', '', '')" + cursor.execute(insert_query) + + if "current_operation" in excluded_table: + insert_query = "insert into current_operation (id) values (0)" + cursor.execute(insert_query) + + if "restore_operation" in excluded_table: + insert_query = "insert into restore_operation (id) values (0)" + cursor.execute(insert_query) + + if "batch_operation" in excluded_table: + insert_query = "insert into batch_operation (id, redo, undo, redo_cs, undo_cs) values (0, '', '', '', '')" + cursor.execute(insert_query) + + if "operation_table" in excluded_table: + insert_query = ( + "insert into operation_table (option) values ('operation')" + ) + cursor.execute(insert_query) + connection.commit() + + def __call__(self, source: str, new_db: str, excluded_tables: [str] = None) -> None: + source_connection = pg.connect(conninfo=get_pgconn_string(), autocommit=True) + + self.create_database(source_connection, new_db) + + self.execute_pg_dump(source, excluded_tables) + self.execute_pg_restore(new_db) + source_connection.close() + + new_db_connection = pg.connect( + conninfo=get_pgconn_string(db_name=new_db), autocommit=True + ) + self.init_operation_table(new_db_connection, excluded_tables) + new_db_connection.close() + + +def create_project(name: str) -> None: + return copy_project("project", name) + + +def delete_project(name: str) -> None: + with pg.connect(conninfo=get_pgconn_string(), autocommit=True) as conn: + with conn.cursor() as cur: + cur.execute( + f"select pg_terminate_backend(pid) from pg_stat_activity where datname = '{name}'" + ) + cur.execute(f'drop database "{name}"') + + +def clean_project(excluded: list[str] = []) -> None: + projects = list_project() + with pg.connect(conninfo=get_pgconn_string(), autocommit=True) as conn: + with conn.cursor(row_factory=dict_row) as cur: + row = cur.execute(f"select current_database()").fetchone() + if row != None: + current_db = row["current_database"] + if current_db in projects: + projects.remove(current_db) + for project in projects: + if project in _server_databases or project in excluded: + continue + cur.execute( + f"select pg_terminate_backend(pid) from pg_stat_activity where datname = '{project}'" + ) + cur.execute(f'drop database "{project}"') + + +def open_project(name: str) -> None: + if name not in conn: + conn[name] = pg.connect( + conninfo=get_pgconn_string(db_name=name), autocommit=True + ) + + +def is_project_open(name: str) -> bool: + return name in conn + + +def close_project(name: str) -> None: + if name in conn: + conn[name].close() + del conn[name] diff --git a/app/native/api/project_backup.py b/app/native/api/project_backup.py new file mode 100644 index 0000000..e578e40 --- /dev/null +++ b/app/native/api/project_backup.py @@ -0,0 +1,183 @@ +import os +import psycopg as pg +from psycopg import sql +from psycopg.rows import dict_row +from .connection import g_conn_dict as conn +from .postgresql_info import get_pgconn_string + +# no undo/redo + +_server_databases = ["template0", "template1", "postgres", "project"] + + +def list_project() -> list[str]: + ps = [] + + with pg.connect(conninfo=get_pgconn_string(), autocommit=True) as conn: + with conn.cursor(row_factory=dict_row) as cur: + for p in cur.execute( + f"select datname from pg_database where datname <> 'postgres' and datname <> 'template0' and datname <> 'template1' and datname <> 'project'" + ): + ps.append(p["datname"]) + return ps + + +def have_project(name: str) -> bool: + with pg.connect( + conninfo=get_pgconn_string(db_name="postgres"), autocommit=True + ) as conn: + with conn.cursor() as cur: + cur.execute("select 1 from pg_database where datname = %s", (name,)) + return cur.fetchone() is not None + + +def copy_project(source: str, new: str) -> None: + if source in conn: + conn[source].close() + del conn[source] + + with pg.connect( + conninfo=get_pgconn_string(db_name="postgres"), autocommit=True + ) as admin_conn: + with admin_conn.cursor() as cur: + cur.execute( + "update pg_database set datallowconn = false where datname = %s", + (source,), + ) + try: + cur.execute( + "select pg_terminate_backend(pid) from pg_stat_activity where datname = %s and pid <> pg_backend_pid()", + (source,), + ) + cur.execute( + sql.SQL("create database {} with template = {}").format( + sql.Identifier(new), sql.Identifier(source) + ) + ) + finally: + cur.execute( + "update pg_database set datallowconn = true where datname = %s", + (source,), + ) + + +# 2025-02-07, WMH +# copyproject会把pg中operation这个表的全部内容也加进去,我们实际项目运行一周后operation这个表会变得特别大,导致CopyProject花费的时间很长,CopyProjectEx把operation的在复制时没有一块复制过去,节省时间 +class CopyProjectEx: + @staticmethod + def create_database(connection, new_db): + with connection.cursor() as cursor: + cursor.execute(f'create database "{new_db}"') + connection.commit() + + @staticmethod + def execute_pg_dump(hostname, source_db, exclude_table_list): + dump_command_structure = ( + f"pg_dump -h {hostname} -F c -s -f source_db_structure.dump {source_db}" + ) + os.system(dump_command_structure) + + if exclude_table_list is not None: + exclude_table = " ".join(["-T {}".format(i) for i in exclude_table_list]) + dump_command_db = f"pg_dump -h {hostname} -F c -a {exclude_table} -f source_db.dump {source_db}" + else: + dump_command_db = ( + f"pg_dump -h {hostname} -F c -a -f source_db.dump {source_db}" + ) + os.system(dump_command_db) + + @staticmethod + def execute_pg_restore(hostname, new_db): + restore_command_structure = ( + f"pg_restore -h {hostname} -d {new_db} source_db_structure.dump" + ) + os.system(restore_command_structure) + + restore_command_db = f"pg_restore -h {hostname} -d {new_db} source_db.dump" + os.system(restore_command_db) + + @staticmethod + def init_operation_table(connection, excluded_table): + with connection.cursor() as cursor: + if "operation" in excluded_table: + insert_query = "insert into operation (id, redo, undo, redo_cs, undo_cs) values (0, '', '', '', '')" + cursor.execute(insert_query) + + if "current_operation" in excluded_table: + insert_query = "insert into current_operation (id) values (0)" + cursor.execute(insert_query) + + if "restore_operation" in excluded_table: + insert_query = "insert into restore_operation (id) values (0)" + cursor.execute(insert_query) + + if "batch_operation" in excluded_table: + insert_query = "insert into batch_operation (id, redo, undo, redo_cs, undo_cs) values (0, '', '', '', '')" + cursor.execute(insert_query) + + if "operation_table" in excluded_table: + insert_query = ( + "insert into operation_table (option) values ('operation')" + ) + cursor.execute(insert_query) + connection.commit() + + def __call__(self, source: str, new: str, excluded_table: [str] = None) -> None: + connection = pg.connect(conninfo=get_pgconn_string(), autocommit=True) + + self.create_database(connection, new) + self.execute_pg_dump("127.0.0.1", source, excluded_table) + self.execute_pg_restore("127.0.0.1", new) + + connection = pg.connect( + conninfo=get_pgconn_string(db_name=new), autocommit=True + ) + self.init_operation_table(connection, excluded_table) + + +def create_project(name: str) -> None: + return copy_project("project", name) + + +def delete_project(name: str) -> None: + with pg.connect(conninfo=get_pgconn_string(), autocommit=True) as conn: + with conn.cursor() as cur: + cur.execute( + f"select pg_terminate_backend(pid) from pg_stat_activity where datname = '{name}'" + ) + cur.execute(f'drop database "{name}"') + + +def clean_project(excluded: list[str] = []) -> None: + projects = list_project() + with pg.connect(conninfo=get_pgconn_string(), autocommit=True) as conn: + with conn.cursor(row_factory=dict_row) as cur: + row = cur.execute(f"select current_database()").fetchone() + if row != None: + current_db = row["current_database"] + if current_db in projects: + projects.remove(current_db) + for project in projects: + if project in _server_databases or project in excluded: + continue + cur.execute( + f"select pg_terminate_backend(pid) from pg_stat_activity where datname = '{project}'" + ) + cur.execute(f'drop database "{project}"') + + +def open_project(name: str) -> None: + if name not in conn: + conn[name] = pg.connect( + conninfo=get_pgconn_string(db_name=name), autocommit=True + ) + + +def is_project_open(name: str) -> bool: + return name in conn + + +def close_project(name: str) -> None: + if name in conn: + conn[name].close() + del conn[name] diff --git a/app/native/api/s0_base.py b/app/native/api/s0_base.py new file mode 100644 index 0000000..65882ab --- /dev/null +++ b/app/native/api/s0_base.py @@ -0,0 +1,262 @@ +from psycopg.rows import dict_row, Row +from .connection import g_conn_dict as conn +from .database import read +from typing import Any + +_NODE = '_node' +_LINK = '_link' +_CURVE = '_curve' +_PATTERN = '_pattern' +_REGION = '_region' + +JUNCTION = 'junction' +RESERVOIR = 'reservoir' +TANK = 'tank' +PIPE = 'pipe' +PUMP = 'pump' +VALVE = 'valve' + +PATTERN = 'pattern' +CURVE = 'curve' + +REGION = 'region' + +# DingZQ, 2025-02-05 +''' + C++ 代码里已经定义了这些 enum 值 +{ + kNothing = -1, + + //Node + kReservoir = 0, + kTank, + kJunction, + + //Link + kPipe, + kPump, + kValve, +''' +ELEMENT_TYPES : dict[str, int] = { + RESERVOIR : 0, + TANK : 1, + JUNCTION : 2, + PIPE : 3, + PUMP : 4, + VALVE : 5, +} + +def _get_from(name: str, id: str, base_type: str) -> Row | None: + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select * from {base_type} where id = '{id}'") + return cur.fetchone() + + +def is_node(name: str, id: str) -> bool: + return _get_from(name, id, _NODE) != None + + +def is_junction(name: str, id: str) -> bool: + row = _get_from(name, id, _NODE) + return row != None and row['type'] == JUNCTION + + +def is_reservoir(name: str, id: str) -> bool: + row = _get_from(name, id, _NODE) + return row != None and row['type'] == RESERVOIR + + +def is_tank(name: str, id: str) -> bool: + row = _get_from(name, id, _NODE) + return row != None and row['type'] == TANK + + +def is_link(name: str, id: str) -> bool: + return _get_from(name, id, _LINK) != None + + +def is_pipe(name: str, id: str) -> bool: + row = _get_from(name, id, _LINK) + return row != None and row['type'] == PIPE + + +def is_pump(name: str, id: str) -> bool: + row = _get_from(name, id, _LINK) + return row != None and row['type'] == PUMP + + +def is_valve(name: str, id: str) -> bool: + row = _get_from(name, id, _LINK) + return row != None and row['type'] == VALVE + +# DingZQ, 2025-02-05 +def get_node_type(name: str, node_id: str) -> str: + row = _get_from(name, node_id, _NODE) + return row['type'] + + +def get_link_type(name: str, link_id: str) -> str: + row = _get_from(name, link_id, _LINK) + return row['type'] + +def get_element_type(name: str, element_id: str) -> str: + if is_node(name, element_id): + return get_node_type(name, element_id) + elif is_link(name, element_id): + return get_link_type(name, element_id) + else: + return None + +def get_element_type_value(name: str, element_id: str) -> int: + return ELEMENT_TYPES[get_element_type(name, element_id)] + +def is_curve(name: str, id: str) -> bool: + + return _get_from(name, id, _CURVE) != None + + +def is_pattern(name: str, id: str) -> bool: + return _get_from(name, id, _PATTERN) != None + + +def is_region(name: str, id: str) -> bool: + return _get_from(name, id, _REGION) != None + + +def _get_all(name: str, base_type: str) -> list[str]: + ids : list[str] = [] + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select id from {base_type} order by id") + for record in cur: + ids.append(record['id']) + return ids + + +def get_nodes(name: str) -> list[str]: + return _get_all(name, _NODE) + +# DingZQ +def _get_nodes_by_type(name: str, type: str) -> list[str]: + ids : list[str] = [] + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select id from {_NODE} where type = '{type}' order by id") + for record in cur: + ids.append(record['id']) + return ids + +# DingZQ +def get_nodes_id_and_type(name: str) -> dict[str, str]: + nodes_id_and_type: dict[str, str] = {} + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select id, type from {_NODE} order by id") + for record in cur: + nodes_id_and_type[record['id']] = record['type'] + return nodes_id_and_type + +# DingZQ 2024-12-31 +def get_major_nodes(name: str, diameter: int) -> list[str]: + major_nodes_set = set() + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select node1, node2 from pipes where diameter > {diameter}") + for record in cur: + major_nodes_set.add(record['node1']) + major_nodes_set.add(record['node2']) + + return list(major_nodes_set) + +# DingZQs +def get_junctions(name: str) -> list[str]: + return _get_nodes_by_type(name, JUNCTION) + +# DingZQ +def get_reservoirs(name: str) -> list[str]: + return _get_nodes_by_type(name, RESERVOIR) + +# DingZQ +def get_tanks(name: str) -> list[str]: + return _get_nodes_by_type(name, TANK) + +# DingZQ +def get_links(name: str) -> list[str]: + return _get_all(name, _LINK) + +# DingZQ +def _get_links_by_type(name: str, type: str) -> list[str]: + ids : list[str] = [] + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select id from {_LINK} where type = '{type}' order by id") + for record in cur: + ids.append(record['id']) + return ids + +# DingZQ +def get_links_id_and_type(name: str) -> dict[str, str]: + links_id_and_type: dict[str, str] = {} + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select id, type from {_LINK} order by id") + for record in cur: + links_id_and_type[record['id']] = record['type'] + return links_id_and_type + +# DingZQ 2024-12-31 +# 获取直径大于800的管道 +def get_major_pipes(name: str, diameter: int) -> list[str]: + major_pipe_ids: list[str] = [] + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select id from pipes where diameter > {diameter} order by id") + for record in cur: + major_pipe_ids.append(record['id']) + return major_pipe_ids + +# DingZQ +def get_pipes(name: str) -> list[str]: + return _get_links_by_type(name, PIPE) + +# DingZQ +def get_pumps(name: str) -> list[str]: + return _get_links_by_type(name, PUMP) + +# DingZQ +def get_valves(name: str) -> list[str]: + return _get_links_by_type(name, VALVE) + + +def get_curves(name: str) -> list[str]: + return _get_all(name, _CURVE) + + +def get_patterns(name: str) -> list[str]: + return _get_all(name, _PATTERN) + +def get_regions(name: str) -> list[str]: + return _get_all(name, _REGION) + +def get_node_links(name: str, id: str) -> list[str]: + with conn[name].cursor(row_factory=dict_row) as cur: + links: list[str] = [] + for p in cur.execute(f"select id from pipes where node1 = '{id}' or node2 = '{id}'").fetchall(): + links.append(p['id']) + for p in cur.execute(f"select id from pumps where node1 = '{id}' or node2 = '{id}'").fetchall(): + links.append(p['id']) + for p in cur.execute(f"select id from valves where node1 = '{id}' or node2 = '{id}'").fetchall(): + links.append(p['id']) + return links + + +def get_link_nodes(name: str, id: str) -> list[str]: + row = {} + if is_pipe(name, id): + row = read(name, f"select node1, node2 from pipes where id = '{id}'") + elif is_pump(name, id): + row = read(name, f"select node1, node2 from pumps where id = '{id}'") + elif is_valve(name, id): + row = read(name, f"select node1, node2 from valves where id = '{id}'") + return [str(row['node1']), str(row['node2'])] + +def get_region_type(name: str, id: str)->str: + if(is_region(name,id)): + type = read(name, f"select type from _region where id = '{id}'") + return type + + + diff --git a/app/native/api/s10_status.py b/app/native/api/s10_status.py new file mode 100644 index 0000000..33b2a7d --- /dev/null +++ b/app/native/api/s10_status.py @@ -0,0 +1,110 @@ +from .database import * + + +LINK_STATUS_OPEN = 'OPEN' +LINK_STATUS_CLOSED = 'CLOSED' +LINK_STATUS_ACTIVE = 'ACTIVE' + + +def get_status_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'link' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'status' : {'type': 'str' , 'optional': True , 'readonly': False}, + 'setting' : {'type': 'float' , 'optional': True , 'readonly': False} } + + +def get_status(name: str, link: str) -> dict[str, Any]: + s = try_read(name, f"select * from status where link = '{link}'") + if s == None: + return { 'link': link, 'status': None, 'setting': None } + d = {} + d['link'] = str(s['link']) + d['status'] = str(s['status']) if s['status'] != None else None + d['setting'] = float(s['setting']) if s['setting'] != None else None + return d + + +class Status(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'status' + self.link = str(input['link']) + self.status = str(input['status']) if 'status' in input and input['status'] != None else None + self.setting = float(input['setting']) if 'setting' in input and input['setting'] != None else None + + self.f_type = f"'{self.type}'" + self.f_link = f"'{self.link}'" + self.f_status = f"'{self.status}'" if self.status != None else 'null' + self.f_setting = self.setting if self.setting != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'link': self.link, 'status': self.status, 'setting': self.setting } + + +def _set_status(name: str, cs: ChangeSet) -> DbChangeSet: + old = Status(get_status(name, cs.operations[0]['link'])) + raw_new = get_status(name, cs.operations[0]['link']) + + new_dict = cs.operations[0] + schema = get_status_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Status(raw_new) + + redo_sql = f"delete from status where link = {new.f_link};" + if new.status != None or new.setting != None: + redo_sql += f"\ninsert into status (link, status, setting) values ({new.f_link}, {new.f_status}, {new.f_setting});" + + undo_sql = f"delete from status where link = {old.f_link};" + if old.status != None or old.setting != None: + undo_sql += f"\ninsert into status (link, status, setting) values ({old.f_link}, {old.f_status}, {old.f_setting});" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_status(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_status(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# link value +#-------------------------------------------------------------- + + +def inp_in_status(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + link = str(tokens[0]) + value = tokens[1].upper() + if value == LINK_STATUS_OPEN or value == LINK_STATUS_CLOSED or value == LINK_STATUS_ACTIVE: + return str(f"insert into status (link, status, setting) values ('{link}', '{value}', null);") + else: + return str(f"insert into status (link, status, setting) values ('{link}', null, {float(value)});") + + +def inp_out_status(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from status') + for obj in objs: + link = obj['link'] + status = obj['status'] if obj['status'] != None else '' + setting = obj['setting'] if obj['setting'] != None else '' + if status != '': + lines.append(f'{link} {status}') + if setting != '': + lines.append(f'{link} {setting}') + return lines + + +def delete_status_by_link(name: str, link: str) -> ChangeSet: + row = try_read(name, f"select * from status where link = '{link}'") + if row == None: + return ChangeSet() + return ChangeSet(g_update_prefix | {'type': 'status', 'link': link, 'status': None, 'setting': None}) diff --git a/app/native/api/s11_patterns.py b/app/native/api/s11_patterns.py new file mode 100644 index 0000000..382d554 --- /dev/null +++ b/app/native/api/s11_patterns.py @@ -0,0 +1,163 @@ +from .database import * + +PATTERN_V3_TYPE_FIXED = 'FIXED' +PATTERN_V3_TYPE_VARIABLE = 'VARIABLE' + +pattern_v3_types = [PATTERN_V3_TYPE_FIXED, PATTERN_V3_TYPE_VARIABLE] + +def get_pattern_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'factors' : {'type': 'float_list' , 'optional': False , 'readonly': False } } + + +def get_pattern(name: str, id: str) -> dict[str, Any]: + p_one = try_read(name, f"select * from _pattern where id = '{id}'") + if p_one == None: + return {} + pas = read_all(name, f"select * from patterns where id = '{id}' order by _order") + ps = [] + for r in pas: + ps.append(float(r['factor'])) + return { 'id': id, 'factors': ps } + + +def _set_pattern(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + f_id = f"'{id}'" + + old = get_pattern(name, id) + + new = { 'id': id } + if 'factors' in cs.operations[0]: + new['factors'] = cs.operations[0]['factors'] + else: + new['factors'] = old['factors'] + + # TODO: transaction ? + redo_sql = f"delete from patterns where id = {f_id};" + for f_factor in new['factors']: + redo_sql += f"\ninsert into patterns (id, factor) values ({f_id}, {f_factor});" + + undo_sql = f"delete from patterns where id = {f_id};" + for f_factor in old['factors']: + undo_sql += f"\ninsert into patterns (id, factor) values ({f_id}, {f_factor});" + + redo_cs = g_update_prefix | { 'type': 'pattern' } | new + undo_cs = g_update_prefix | { 'type': 'pattern' } | old + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_pattern(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_pattern(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _set_pattern(name, cs)) + + +def _add_pattern(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + f_id = f"'{id}'" + + new = { 'id': id, 'factors': cs.operations[0]['factors'] } + + # TODO: transaction ? + redo_sql = f"insert into _pattern (id) values ({f_id});" + for f_factor in new['factors']: + redo_sql += f"\ninsert into patterns (id, factor) values ({f_id}, {f_factor});" + + undo_sql = f"delete from patterns where id = {f_id};" + undo_sql += f"\ndelete from _pattern where id = {f_id};" + + redo_cs = g_add_prefix | { 'type': 'pattern' } | new + undo_cs = g_delete_prefix | { 'type': 'pattern' } | { 'id': id } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_pattern(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_pattern(name, cs.operations[0]['id']) != {}: + return ChangeSet() + return execute_command(name, _add_pattern(name, cs)) + + +def _delete_pattern(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + f_id = f"'{id}'" + + old = get_pattern(name, id) + + redo_sql = f"delete from patterns where id = {f_id};" + redo_sql += f"\ndelete from _pattern where id = {f_id};" + + # TODO: transaction ? + undo_sql = f"insert into _pattern (id) values ({f_id});" + for f_factor in old['factors']: + undo_sql += f"\ninsert into patterns (id, factor) values ({f_id}, {f_factor});" + + redo_cs = g_delete_prefix | { 'type': 'pattern' } | { 'id': id } + undo_cs = g_add_prefix | { 'type': 'pattern' } | old + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_pattern(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_pattern(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_pattern(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][IN][OUT] +# ;desc +# id mult1 mult2 ..... +#-------------------------------------------------------------- +#-------------------------------------------------------------- +# [EPA3][IN][OUT] +# id FIXED (interval) +# id factor1 factor2 ... +# id VARIABLE +# id time1 factor1 time2 factor2 ... +#-------------------------------------------------------------- + + +def inp_in_pattern(line: str, fixed: bool = True) -> str: + tokens = line.split() + sql = '' + if fixed: + for token in tokens[1:]: + sql += f"insert into patterns (id, factor) values ('{tokens[0]}', {float(token)});" + else: + for token in tokens[1::2]: + sql += f"insert into patterns (id, factor) values ('{tokens[0]}', {float(token)});" + return sql + + +def inp_out_pattern(name: str) -> list[str]: + lines = [] + objs = read_all(name, f"select * from patterns order by _order") + for obj in objs: + id = obj['id'] + factor = obj['factor'] + lines.append(f'{id} {factor}') + return lines + + +def inp_out_pattern_v3(name: str) -> list[str]: + lines = [] + objs = read_all(name, f"select * from patterns order by _order") + ids = [] + for obj in objs: + id = obj['id'] + if id not in ids: + # for EPA3, ignore time of variable pattern... + lines.append(f'{id} FIXED') + ids.append(id) + factor = obj['factor'] + lines.append(f'{id} {factor}') + return lines diff --git a/app/native/api/s12_curves.py b/app/native/api/s12_curves.py new file mode 100644 index 0000000..eb37982 --- /dev/null +++ b/app/native/api/s12_curves.py @@ -0,0 +1,186 @@ +from .database import * + +CURVE_TYPE_PUMP = 'PUMP' +CURVE_TYPE_EFFICIENCY = 'EFFICIENCY' +CURVE_TYPE_VOLUME = 'VOLUME' +CURVE_TYPE_HEADLOSS = 'HEADLOSS' + +curve_types = [CURVE_TYPE_PUMP, CURVE_TYPE_EFFICIENCY, CURVE_TYPE_VOLUME, CURVE_TYPE_HEADLOSS] + +def get_curve_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'c_type' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'coords' : {'type': 'list' , 'optional': False , 'readonly': False, + 'element': { 'x' : {'type': 'float' , 'optional': False , 'readonly': False }, + 'y' : {'type': 'float' , 'optional': False , 'readonly': False } }}} + + +def get_curve(name: str, id: str) -> dict[str, Any]: + c_one = try_read(name, f"select * from _curve where id = '{id}'") + if c_one == None: + return {} + cus = read_all(name, f"select * from curves where id = '{id}' order by _order") + cs = [] + for r in cus: + cs.append({ 'x': float(r['x']), 'y': float(r['y']) }) + d = {} + d['id'] = id + d['c_type'] = c_one['type'] + d['coords'] = cs + return d + + +def _set_curve(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + f_id = f"'{id}'" + + old = get_curve(name, id) + old_f_type = f"'{old['c_type']}'" + + new = { 'id': id } + if 'coords' in cs.operations[0]: + new['coords'] = cs.operations[0]['coords'] + else: + new['coords'] = old['coords'] + if 'c_type' in cs.operations[0]: + new['c_type'] = cs.operations[0]['c_type'] + else: + new['c_type'] = old['c_type'] + new_f_type = f"'{new['c_type']}'" + + # TODO: transaction ? + redo_sql = f"delete from curves where id = {f_id};" + redo_sql += f"\nupdate _curve set type = {new_f_type} where id = {f_id};" + for xy in new['coords']: + f_x, f_y = xy['x'], xy['y'] + redo_sql += f"\ninsert into curves (id, x, y) values ({f_id}, {f_x}, {f_y});" + + undo_sql = f"delete from curves where id = {f_id};" + undo_sql += f"\nupdate _curve set type = {old_f_type} where id = {f_id};" + for xy in old['coords']: + f_x, f_y = xy['x'], xy['y'] + undo_sql += f"\ninsert into curves (id, x, y) values ({f_id}, {f_x}, {f_y});" + + redo_cs = g_update_prefix | { 'type': 'curve' } | new + undo_cs = g_update_prefix | { 'type': 'curve' } | old + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_curve(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_curve(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _set_curve(name, cs)) + + +def _add_curve(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + f_id = f"'{id}'" + + new = { 'id': id, 'c_type': cs.operations[0]['c_type'], 'coords': [] } + new_f_type = f"'{new['c_type']}'" + + # TODO: transaction ? + redo_sql = f"insert into _curve (id, type) values ({f_id}, {new_f_type});" + for xy in cs.operations[0]['coords']: + x, y = float(xy['x']), float(xy['y']) + f_x, f_y = x, y + redo_sql += f"\ninsert into curves (id, x, y) values ({f_id}, {f_x}, {f_y});" + new['coords'].append({ 'x': x, 'y': y }) + + undo_sql = f"delete from curves where id = {f_id};" + undo_sql += f"\ndelete from _curve where id = {f_id};" + + redo_cs = g_add_prefix | { 'type': 'curve' } | new + undo_cs = g_delete_prefix | { 'type': 'curve' } | { 'id' : id } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_curve(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_curve(name, cs.operations[0]['id']) != {}: + return ChangeSet() + return execute_command(name, _add_curve(name, cs)) + + +def _delete_curve(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + f_id = f"'{id}'" + + old = get_curve(name, id) + old_f_type = f"'{old['c_type']}'" + + redo_sql = f"delete from curves where id = {f_id};" + redo_sql += f"\ndelete from _curve where id = {f_id};" + + # TODO: transaction ? + undo_sql = f"insert into _curve (id, type) values ({f_id}, {old_f_type});" + for xy in old['coords']: + f_x, f_y = xy['x'], xy['y'] + undo_sql += f"\ninsert into curves (id, x, y) values ({f_id}, {f_x}, {f_y});" + + redo_cs = g_delete_prefix | { 'type': 'curve' } | { 'id' : id } + undo_cs = g_add_prefix | { 'type': 'curve' } | old + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_curve(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_curve(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_curve(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][IN][OUT] +# ;type: desc +# id x y +#-------------------------------------------------------------- +#-------------------------------------------------------------- +# [EPA3][IN][OUT] +# id type +# id x y +#-------------------------------------------------------------- + + +def inp_in_curve(line: str) -> str: + tokens = line.split() + return str(f"insert into curves (id, x, y) values ('{tokens[0]}', {float(tokens[1])}, {float(tokens[2])});") + + +def inp_out_curve(name: str) -> list[str]: + lines = [] + types = read_all(name, f"select * from _curve") + for type in types: + id = type['id'] + # ;type: desc + lines.append(f";{type['type']}:") + objs = read_all(name, f"select * from curves where id = '{id}' order by _order") + for obj in objs: + id = obj['id'] + x = obj['x'] + y = obj['y'] + lines.append(f'{id} {x} {y}') + return lines + + +def inp_out_curve_v3(name: str) -> list[str]: + lines = [] + types = read_all(name, f"select * from _curve") + for type in types: + id = type['id'] + # id type + lines.append(f"{id} {type['type']}") + objs = read_all(name, f"select * from curves where id = '{id}' order by _order") + for obj in objs: + id = obj['id'] + x = obj['x'] + y = obj['y'] + lines.append(f'{id} {x} {y}') + return lines diff --git a/app/native/api/s13_controls.py b/app/native/api/s13_controls.py new file mode 100644 index 0000000..7b43f2b --- /dev/null +++ b/app/native/api/s13_controls.py @@ -0,0 +1,52 @@ +from .database import * + + +def get_control_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'controls' : {'type': 'str_list' , 'optional': False , 'readonly': False} } + + +def get_control(name: str) -> dict[str, Any]: + cs = read_all(name, f"select * from controls") + ds = [] + for c in cs: + ds.append(c['line']) + return { 'controls': ds } + + +def _set_control(name: str, cs: ChangeSet) -> DbChangeSet: + old = get_control(name) + + redo_sql = 'delete from controls;' + for line in cs.operations[0]['controls']: + redo_sql += f"\ninsert into controls (line) values ('{line}');" + + undo_sql = 'delete from controls;' + for line in old['controls']: + undo_sql += f"\ninsert into controls (line) values ('{line}');" + + redo_cs = g_update_prefix | { 'type': 'control', 'controls': cs.operations[0]['controls'] } + undo_cs = g_update_prefix | { 'type': 'control', 'controls': old['controls'] } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_control(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_control(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3] +# LINK linkID setting IF NODE nodeID {BELOW/ABOVE} level +# LINK linkID setting AT TIME value (units) +# LINK linkID setting AT CLOCKTIME value (units) +# (0) (1) (2) (3) (4) (5) (6) (7) +# todo... +#-------------------------------------------------------------- + + +def inp_in_control(line: str) -> str: + return str(f"insert into controls (line) values ('{line}');") + + +def inp_out_control(name: str) -> list[str]: + return get_control(name)['controls'] diff --git a/app/native/api/s14_rules.py b/app/native/api/s14_rules.py new file mode 100644 index 0000000..b210f71 --- /dev/null +++ b/app/native/api/s14_rules.py @@ -0,0 +1,48 @@ +from .database import * + + +def get_rule_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'rules' : {'type': 'str_list' , 'optional': False , 'readonly': False} } + + +def get_rule(name: str) -> dict[str, Any]: + cs = read_all(name, f"select * from rules") + ds = [] + for c in cs: + ds.append(c['line']) + return { 'rules': ds } + + +def _set_rule(name: str, cs: ChangeSet) -> DbChangeSet: + old = get_rule(name) + + redo_sql = 'delete from rules;' + for line in cs.operations[0]['rules']: + redo_sql += f"\ninsert into rules (line) values ('{line}');" + + undo_sql = 'delete from rules;' + for line in old['rules']: + undo_sql += f"\ninsert into rules (line) values ('{line}');" + + redo_cs = g_update_prefix | { 'type': 'rule', 'rules': cs.operations[0]['rules'] } + undo_cs = g_update_prefix | { 'type': 'rule', 'rules': old['rules'] } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_rule(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_rule(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3] +# TODO... +#-------------------------------------------------------------- + + +def inp_in_rule(line: str) -> str: + return str(f"insert into rules (line) values ('{line}');") + + +def inp_out_rule(name: str) -> list[str]: + return get_rule(name)['rules'] \ No newline at end of file diff --git a/app/native/api/s15_energy.py b/app/native/api/s15_energy.py new file mode 100644 index 0000000..3abb998 --- /dev/null +++ b/app/native/api/s15_energy.py @@ -0,0 +1,240 @@ +from .database import * + + +element_schema = {'type': 'str' , 'optional': True , 'readonly': False} + + +def get_energy_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'GLOBAL PRICE' : element_schema, + 'GLOBAL PATTERN' : element_schema, + 'GLOBAL EFFIC' : element_schema, + 'DEMAND CHARGE' : element_schema } + + +def get_energy(name: str) -> dict[str, Any]: + ts = read_all(name, f"select * from energy") + d = {} + for e in ts: + d[e['key']] = str(e['value']) + return d + + +def _set_energy(name: str, cs: ChangeSet) -> DbChangeSet: + raw_old = get_energy(name) + + old = {} + new = {} + + new_dict = cs.operations[0] + schema = get_energy_schema(name) + for key in schema.keys(): + if key in new_dict: + old[key] = str(raw_old[key]) + new[key] = str(new_dict[key]) + + redo_cs = g_update_prefix | { 'type' : 'energy' } + + redo_sql = '' + for key, value in new.items(): + if redo_sql != '': + redo_sql += '\n' + redo_sql += f"update energy set value = '{value}' where key = '{key}';" + redo_cs |= { key: value } + + undo_cs = g_update_prefix | { 'type' : 'energy' } + + undo_sql = '' + for key, value in old.items(): + if undo_sql != '': + undo_sql += '\n' + undo_sql += f"update energy set value = '{value}' where key = '{key}';" + undo_cs |= { key: value } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_energy(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_energy(name, cs)) + + +def get_pump_energy_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'pump' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'price' : {'type': 'float' , 'optional': True , 'readonly': False}, + 'pattern' : {'type': 'str' , 'optional': True , 'readonly': False}, + 'effic' : {'type': 'str' , 'optional': True , 'readonly': False} } + + +def get_pump_energy(name: str, pump: str) -> dict[str, Any]: + d = {} + d['pump'] = pump + pe = try_read(name, f"select * from energy_pump_price where pump = '{pump}'") + d['price'] = float(pe['price']) if pe != None else None + pe = try_read(name, f"select * from energy_pump_pattern where pump = '{pump}'") + d['pattern'] = str(pe['pattern']) if pe != None else None + pe = try_read(name, f"select * from energy_pump_effic where pump = '{pump}'") + d['effic'] = str(pe['effic']) if pe != None else None + return d + + +class PumpEnergy(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'pump_energy' + self.pump = str(input['pump']) + self.price = float(input['price']) if 'price' in input and input['price'] != None else None + self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None + self.effic = str(input['effic']) if 'effic' in input and input['effic'] != None else None + + self.f_type = f"'{self.type}'" + self.f_pump = f"'{self.pump}'" + self.f_price = self.price if self.price != None else 'null' + self.f_pattern = f"'{self.pattern}'" if self.pattern != None else 'null' + self.f_effic = f"'{self.effic}'" if self.effic != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'pump': self.pump, 'price': self.price, 'pattern': self.pattern, 'effic': self.effic } + + +def _set_pump_energy(name: str, cs: ChangeSet) -> DbChangeSet: + old = PumpEnergy(get_pump_energy(name, cs.operations[0]['pump'])) + raw_new = get_pump_energy(name, cs.operations[0]['pump']) + + new_dict = cs.operations[0] + schema = get_pump_energy_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = PumpEnergy(raw_new) + + redo_sql = f"delete from energy_pump_price where pump = {new.f_pump};\ndelete from energy_pump_pattern where pump = {new.f_pump};\ndelete from energy_pump_effic where pump = {new.f_pump};" + if new.price != None: + redo_sql += f"\ninsert into energy_pump_price (pump, price) values ({new.f_pump}, {new.f_price});" + if new.pattern != None: + redo_sql += f"\ninsert into energy_pump_pattern (pump, pattern) values ({new.f_pump}, {new.f_pattern});" + if new.effic != None: + redo_sql += f"\ninsert into energy_pump_effic (pump, effic) values ({new.f_pump}, {new.f_effic});" + + undo_sql = f"delete from energy_pump_price where pump = {old.f_pump};\ndelete from energy_pump_pattern where pump = {old.f_pump};\ndelete from energy_pump_effic where pump = {old.f_pump};" + if old.price != None: + undo_sql += f"\ninsert into energy_pump_price (pump, price) values ({old.f_pump}, {old.f_price});" + if old.pattern != None: + undo_sql += f"\ninsert into energy_pump_pattern (pump, pattern) values ({old.f_pump}, {old.f_pattern});" + if old.effic != None: + undo_sql += f"\ninsert into energy_pump_effic (pump, effic) values ({old.f_pump}, {old.f_effic});" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_pump_energy(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_pump_energy(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# GLOBAL {PRICE/PATTERN/EFFIC} value +# PUMP id {PRICE/PATTERN/EFFIC} value +# DEMAND CHARGE value +#-------------------------------------------------------------- + + +def inp_in_energy(line: str) -> str: + tokens = line.split() + + if tokens[0].upper() == 'PUMP': + pump = tokens[1] + key = tokens[2].lower() + value = tokens[3] + if key == 'price': + value = float(value) + else: + value = f"'{value}'" + if key == 'efficiency': + key = 'effic' + + return str(f"insert into energy_pump_{key} (pump, {key}) values ('{pump}', {value});") + + else: + line = line.upper().strip() + for key in get_energy_schema('').keys(): + if line.startswith(key): + value = line.removeprefix(key).strip() + + # exception here + if line.startswith('GLOBAL EFFICIENCY'): + value = line.removeprefix('GLOBAL EFFICIENCY').strip() + + return str(f"update energy set value = '{value}' where key = '{key}';") + + return str('') + + +def inp_out_energy(name: str) -> list[str]: + lines = [] + + objs = read_all(name, f"select * from energy") + for obj in objs: + key = obj['key'] + value = obj['value'] + if value.strip() != '': + lines.append(f'{key} {value}') + + objs = read_all(name, f"select * from energy_pump_price") + for obj in objs: + pump = obj['pump'] + value = obj['price'] + lines.append(f'PUMP {pump} PRICE {value}') + + objs = read_all(name, f"select * from energy_pump_pattern") + for obj in objs: + pump = obj['pump'] + value = obj['pattern'] + lines.append(f'PUMP {pump} PATTERN {value}') + + objs = read_all(name, f"select * from energy_pump_effic") + for obj in objs: + pump = obj['pump'] + value = obj['effic'] + lines.append(f'PUMP {pump} EFFIC {value}') + + return lines + + +def delete_pump_energy_by_pump(name: str, pump: str) -> ChangeSet: + row1 = try_read(name, f"select * from energy_pump_price where pump = '{pump}'") + row2 = try_read(name, f"select * from energy_pump_pattern where pump = '{pump}'") + row3 = try_read(name, f"select * from energy_pump_effic where pump = '{pump}'") + if row1 == None and row2 == None and row3 == None: + return ChangeSet() + return ChangeSet(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': None, 'pattern': None, 'effic': None}) + + +def unset_pump_energy_by_pattern(name: str, pattern: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select * from energy_pump_pattern where pattern = '{pattern}'") + for row in rows: + pump = row['pump'] + row1 = try_read(name, f"select * from energy_pump_price where pump = '{pump}'") + price = float(row1['price']) if row1 != None else None + row2 = try_read(name, f"select * from energy_pump_effic where pump = '{pump}'") + effic = str(row2['effic']) if row2 != None else None + cs.append(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': price, 'pattern': None, 'effic': effic}) + + return cs + + +def unset_pump_energy_by_curve(name: str, curve: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select * from energy_pump_effic where effic = '{curve}'") + for row in rows: + pump = row['pump'] + row1 = try_read(name, f"select * from energy_pump_price where pump = '{pump}'") + price = float(row1['price']) if row1 != None else None + row2 = try_read(name, f"select * from energy_pump_pattern where pump = '{pump}'") + pattern = str(row2['pattern']) if row2 != None else None + cs.append(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': price, 'pattern': pattern, 'effic': None}) + + return cs diff --git a/app/native/api/s16_emitters.py b/app/native/api/s16_emitters.py new file mode 100644 index 0000000..8f510a9 --- /dev/null +++ b/app/native/api/s16_emitters.py @@ -0,0 +1,98 @@ +from .database import * + + +def get_emitter_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'junction' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'coefficient' : {'type': 'float' , 'optional': True , 'readonly': False} } + + +def get_emitter(name: str, junction: str) -> dict[str, Any]: + e = try_read(name, f"select * from emitters where junction = '{junction}'") + if e == None: + return { 'junction': junction, 'coefficient': None } + d = {} + d['junction'] = str(e['junction']) + d['coefficient'] = float(e['coefficient']) if e['coefficient'] != None else None + return d + + +class Emitter(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'emitter' + self.junction = str(input['junction']) + self.coefficient = float(input['coefficient']) if 'coefficient' in input and input['coefficient'] != None else None + + self.f_type = f"'{self.type}'" + self.f_junction = f"'{self.junction}'" + self.f_coefficient = self.coefficient if self.coefficient != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'junction': self.junction, 'coefficient': self.coefficient } + + +def _set_emitter(name: str, cs: ChangeSet) -> DbChangeSet: + old = Emitter(get_emitter(name, cs.operations[0]['junction'])) + raw_new = get_emitter(name, cs.operations[0]['junction']) + + new_dict = cs.operations[0] + schema = get_emitter_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Emitter(raw_new) + + redo_sql = f"delete from emitters where junction = {new.f_junction};" + if new.coefficient != None: + redo_sql += f"\ninsert into emitters (junction, coefficient) values ({new.f_junction}, {new.f_coefficient});" + + undo_sql = f"delete from emitters where junction = {old.f_junction};" + if old.coefficient != None: + undo_sql += f"\ninsert into emitters (junction, coefficient) values ({old.f_junction}, {old.f_coefficient});" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_emitter(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_emitter(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][IN][OUT] +# node Ke +#-------------------------------------------------------------- +# [EPA3][IN][OUT] +# node Ke (exponent pattern) +#-------------------------------------------------------------- + + +def inp_in_emitter(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + junction = str(tokens[0]) + coefficient = float(tokens[1]) + + return str(f"insert into emitters (junction, coefficient) values ('{junction}', {coefficient});") + + +def inp_out_emitter(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from emitters') + for obj in objs: + junction = obj['junction'] + coefficient = obj['coefficient'] + lines.append(f'{junction} {coefficient}') + return lines + + +def delete_emitter_by_junction(name: str, junction: str) -> ChangeSet: + row = try_read(name, f"select * from emitters where junction = '{junction}'") + if row == None: + return ChangeSet() + return ChangeSet(g_update_prefix | {'type' : 'emitter', 'junction': junction, 'coefficient': None}) diff --git a/app/native/api/s17_quality.py b/app/native/api/s17_quality.py new file mode 100644 index 0000000..539b205 --- /dev/null +++ b/app/native/api/s17_quality.py @@ -0,0 +1,95 @@ +from .database import * + + +def get_quality_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'node' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'quality' : {'type': 'float' , 'optional': True , 'readonly': False} } + + +def get_quality(name: str, node: str) -> dict[str, Any]: + e = try_read(name, f"select * from quality where node = '{node}'") + if e == None: + return { 'node': node, 'quality': None } + d = {} + d['node'] = str(e['node']) + d['quality'] = float(e['quality']) if e['quality'] != None else None + return d + + +class Quality(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'quality' + self.node = str(input['node']) + self.quality = float(input['quality']) if 'quality' in input and input['quality'] != None else None + + self.f_type = f"'{self.type}'" + self.f_node = f"'{self.node}'" + self.f_quality = self.quality if self.quality != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'node': self.node, 'quality': self.quality } + + +def _set_quality(name: str, cs: ChangeSet) -> DbChangeSet: + old = Quality(get_quality(name, cs.operations[0]['node'])) + raw_new = get_quality(name, cs.operations[0]['node']) + + new_dict = cs.operations[0] + schema = get_quality_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Quality(raw_new) + + redo_sql = f"delete from quality where node = {new.f_node};" + if new.quality != None: + redo_sql += f"\ninsert into quality (node, quality) values ({new.f_node}, {new.f_quality});" + + undo_sql = f"delete from quality where node = {old.f_node};" + if old.quality != None: + undo_sql += f"\ninsert into quality (node, quality) values ({old.f_node}, {old.f_quality});" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_quality(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_quality(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# node initqual +#-------------------------------------------------------------- + + +def inp_in_quality(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + node = str(tokens[0]) + quality = float(tokens[1]) + + return str(f"insert into quality (node, quality) values ('{node}', {quality});") + + +def inp_out_quality(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from quality') + for obj in objs: + node = obj['node'] + quality = obj['quality'] + lines.append(f'{node} {quality}') + return lines + + +def delete_quality_by_node(name: str, node: str) -> ChangeSet: + row = try_read(name, f"select * from quality where node = '{node}'") + if row == None: + return ChangeSet() + return ChangeSet(g_update_prefix | {'type' : 'quality', 'node': node, 'quality': None}) diff --git a/app/native/api/s18_sources.py b/app/native/api/s18_sources.py new file mode 100644 index 0000000..0abaeda --- /dev/null +++ b/app/native/api/s18_sources.py @@ -0,0 +1,153 @@ +from .database import * +from .s0_base import * + +SOURCE_TYPE_CONCEN = 'CONCEN' +SOURCE_TYPE_MASS = 'MASS' +SOURCE_TYPE_FLOWPACED = 'FLOWPACED' +SOURCE_TYPE_SETPOINT = 'SETPOINT' + +def get_source_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'node' : {'type': 'str' , 'optional': False , 'readonly': True }, + 's_type' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'strength' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'pattern' : {'type': 'str' , 'optional': True , 'readonly': False} } + + +def get_source(name: str, node: str) -> dict[str, Any]: + s = try_read(name, f"select * from sources where node = '{node}'") + if s == None: + return {} + d = {} + d['node'] = str(s['node']) + d['s_type'] = str(s['s_type']) + d['strength'] = float(s['strength']) + d['pattern'] = str(s['pattern']) if s['pattern'] != None else None + return d + + +class Source(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'source' + self.node = str(input['node']) + self.s_type = str(input['s_type']) + self.strength = float(input['strength']) + self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None + + self.f_type = f"'{self.type}'" + self.f_node = f"'{self.node}'" + self.f_s_type = f"'{self.s_type}'" + self.f_strength = self.strength + self.f_pattern = f"'{self.pattern}'" if self.pattern != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'node': self.node, 's_type': self.s_type, 'strength': self.strength, 'pattern': self.pattern } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'node': self.node } + + +def _set_source(name: str, cs: ChangeSet) -> DbChangeSet: + old = Source(get_source(name, cs.operations[0]['node'])) + raw_new = get_source(name, cs.operations[0]['node']) + + new_dict = cs.operations[0] + schema = get_source_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Source(raw_new) + + redo_sql = f"update sources set s_type = {new.f_s_type}, strength = {new.f_strength}, pattern = {new.f_pattern} where node = {new.f_node};" + undo_sql = f"update sources set s_type = {old.f_s_type}, strength = {old.f_strength}, pattern = {old.f_pattern} where node = {old.f_node};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_source(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_source(name, cs)) + + +def _add_source(name: str, cs: ChangeSet) -> DbChangeSet: + new = Source(cs.operations[0]) + + redo_sql = f"insert into sources (node, s_type, strength, pattern) values ({new.f_node}, {new.f_s_type}, {new.f_strength}, {new.f_pattern});" + undo_sql = f"delete from sources where node = {new.f_node};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_id_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_source(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _add_source(name, cs)) + + +def _delete_source(name: str, cs: ChangeSet) -> DbChangeSet: + old = Source(get_source(name, cs.operations[0]['node'])) + + redo_sql = f"delete from sources where node = {old.f_node};" + undo_sql = f"insert into sources (node, s_type, strength, pattern) values ({old.f_node}, {old.f_s_type}, {old.f_strength}, {old.f_pattern});" + + redo_cs = g_delete_prefix | old.as_id_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_source(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _delete_source(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# node sourcetype quality (pattern) +#-------------------------------------------------------------- + + +def inp_in_source(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + node = str(tokens[0]) + s_type = str(tokens[1].upper()) + strength = float(tokens[2]) + pattern = str(tokens[3]) if num_without_desc >= 4 else None + pattern = f"'{pattern}'" if pattern != None else 'null' + + return str(f"insert into sources (node, s_type, strength, pattern) values ('{node}', '{s_type}', {strength}, {pattern});") + + +def inp_out_source(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from sources') + for obj in objs: + node = obj['node'] + s_type = obj['s_type'] + strength = obj['strength'] + pattern = obj['pattern'] if obj['pattern'] != None else '' + lines.append(f'{node} {s_type} {strength} {pattern}') + return lines + + +def delete_source_by_node(name: str, node: str) -> ChangeSet: + row = try_read(name, f"select * from sources where node = '{node}'") + if row == None: + return ChangeSet() + return ChangeSet(g_delete_prefix | {'type' : 'source', 'node': node}) + + +def unset_source_by_pattern(name: str, pattern: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select node from sources where pattern = '{pattern}'") + for row in rows: + cs.append(g_update_prefix | {'type': 'source', 'node': row['node'], 'pattern': None}) + + return cs diff --git a/app/native/api/s19_reactions.py b/app/native/api/s19_reactions.py new file mode 100644 index 0000000..bf0faf5 --- /dev/null +++ b/app/native/api/s19_reactions.py @@ -0,0 +1,263 @@ +from .database import * + + +element_schema = {'type': 'str' , 'optional': True , 'readonly': False} + + +def get_reaction_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'ORDER BULK' : element_schema, + 'ORDER WALL' : element_schema, + 'ORDER TANK' : element_schema, + 'GLOBAL BULK' : element_schema, + 'GLOBAL WALL' : element_schema, + 'LIMITING POTENTIAL' : element_schema, + 'ROUGHNESS CORRELATION' : element_schema } + + +def get_reaction(name: str) -> dict[str, Any]: + ts = read_all(name, f"select * from reactions") + d = {} + for e in ts: + d[e['key']] = str(e['value']) + return d + + +def _set_reaction(name: str, cs: ChangeSet) -> DbChangeSet: + raw_old = get_reaction(name) + + old = {} + new = {} + + new_dict = cs.operations[0] + schema = get_reaction_schema(name) + for key in schema.keys(): + if key in new_dict: + old[key] = str(raw_old[key]) + new[key] = str(new_dict[key]) + + redo_cs = g_update_prefix | { 'type' : 'reaction' } + + redo_sql = '' + for key, value in new.items(): + if redo_sql != '': + redo_sql += '\n' + redo_sql += f"update reactions set value = '{value}' where key = '{key}';" + redo_cs |= { key: value } + + undo_cs = g_update_prefix | { 'type' : 'reaction' } + + undo_sql = '' + for key, value in old.items(): + if undo_sql != '': + undo_sql += '\n' + undo_sql += f"update reactions set value = '{value}' where key = '{key}';" + undo_cs |= { key: value } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_reaction(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_reaction(name, cs)) + + +def get_pipe_reaction_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'pipe' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'bulk' : {'type': 'float' , 'optional': True , 'readonly': False}, + 'wall' : {'type': 'float' , 'optional': True , 'readonly': False} } + + +def get_pipe_reaction(name: str, pipe: str) -> dict[str, Any]: + d = {} + d['pipe'] = pipe + pr = try_read(name, f"select * from reactions_pipe_bulk where pipe = '{pipe}'") + d['bulk'] = float(pr['value']) if pr != None else None + pr = try_read(name, f"select * from reactions_pipe_wall where pipe = '{pipe}'") + d['wall'] = float(pr['value']) if pr != None else None + return d + + +class PipeReaction(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'pipe_reaction' + self.pipe = str(input['pipe']) + self.bulk = float(input['bulk']) if 'bulk' in input and input['bulk'] != None else None + self.wall = float(input['wall']) if 'wall' in input and input['wall'] != None else None + + self.f_type = f"'{self.type}'" + self.f_pipe = f"'{self.pipe}'" + self.f_bulk = self.bulk if self.bulk != None else 'null' + self.f_wall = self.wall if self.wall != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'pipe': self.pipe, 'bulk': self.bulk, 'wall': self.wall } + + +def _set_pipe_reaction(name: str, cs: ChangeSet) -> DbChangeSet: + old = PipeReaction(get_pipe_reaction(name, cs.operations[0]['pipe'])) + raw_new = get_pipe_reaction(name, cs.operations[0]['pipe']) + + new_dict = cs.operations[0] + schema = get_pipe_reaction_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = PipeReaction(raw_new) + + redo_sql = f"delete from reactions_pipe_bulk where pipe = {new.f_pipe};\ndelete from reactions_pipe_wall where pipe = {new.f_pipe};" + if new.bulk != None: + redo_sql += f"\ninsert into reactions_pipe_bulk (pipe, value) values ({new.f_pipe}, {new.f_bulk});" + if new.wall != None: + redo_sql += f"\ninsert into reactions_pipe_wall (pipe, value) values ({new.f_pipe}, {new.f_wall});" + + undo_sql = f"delete from reactions_pipe_bulk where pipe = {old.f_pipe};\ndelete from reactions_pipe_wall where pipe = {old.f_pipe};" + if old.bulk != None: + undo_sql += f"\ninsert into reactions_pipe_bulk (pipe, value) values ({old.f_pipe}, {old.f_bulk});" + if old.wall != None: + undo_sql += f"\ninsert into reactions_pipe_wall (pipe, value) values ({old.f_pipe}, {old.f_wall});" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_pipe_reaction(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_pipe_reaction(name, cs)) + + +def get_tank_reaction_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'tank' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'value' : {'type': 'float' , 'optional': True , 'readonly': False} } + + +def get_tank_reaction(name: str, tank: str) -> dict[str, Any]: + d = {} + d['tank'] = tank + pr = try_read(name, f"select * from reactions_tank where tank = '{tank}'") + d['value'] = float(pr['value']) if pr != None else None + return d + + +class TankReaction(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'tank_reaction' + self.tank = str(input['tank']) + self.value = float(input['value']) if 'value' in input and input['value'] != None else None + + self.f_type = f"'{self.type}'" + self.f_tank = f"'{self.tank}'" + self.f_value = self.value if self.value != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'tank': self.tank, 'value': self.value } + + +def _set_tank_reaction(name: str, cs: ChangeSet) -> DbChangeSet: + old = TankReaction(get_tank_reaction(name, cs.operations[0]['tank'])) + raw_new = get_tank_reaction(name, cs.operations[0]['tank']) + + new_dict = cs.operations[0] + schema = get_tank_reaction_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = TankReaction(raw_new) + + redo_sql = f"delete from reactions_tank where tank = {new.f_tank};" + if new.value != None: + redo_sql += f"\ninsert into reactions_tank (tank, value) values ({new.f_tank}, {new.f_value});" + + undo_sql = f"delete from reactions_tank where tank = {old.f_tank};" + if old.value != None: + undo_sql += f"\ninsert into reactions_tank (tank, value) values ({old.f_tank}, {old.f_value});" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_tank_reaction(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_tank_reaction(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# ORDER {BULK/WALL/TANK} value +# GLOBAL BULK coeff +# GLOBAL WALL coeff +# BULK link1 (link2) coeff +# WALL link1 (link2) coeff +# TANK node1 (node2) coeff +# LIMITING POTENTIAL value +# ROUGHNESS CORRELATION value +#-------------------------------------------------------------- + + +def inp_in_reaction(line: str) -> str: + tokens = line.split() + token0 = tokens[0].upper() + if token0 == 'BULK' or token0 == 'WALL': + pipe = tokens[1] + key = token0.lower() + value = tokens[2] + return str(f"insert into reactions_pipe_{key} (pipe, value) values ('{pipe}', {value});") + + elif token0 == 'TANK': + tank = tokens[1] + value = tokens[2] + return str(f"insert into reactions_tank (tank, value) values ('{tank}', {value});") + + else: + line = line.upper().strip() + for key in get_reaction_schema('').keys(): + if line.startswith(key): + value = line.removeprefix(key).strip() + return str(f"update reactions set value = '{value}' where key = '{key}';") + + return str('') + + +def inp_out_reaction(name: str) -> list[str]: + lines = [] + + objs = read_all(name, f"select * from reactions") + for obj in objs: + key = obj['key'] + value = obj['value'] + lines.append(f'{key} {value}') + + objs = read_all(name, f"select * from reactions_pipe_bulk") + for obj in objs: + pipe = obj['pipe'] + value = obj['value'] + lines.append(f'BULK {pipe} {value}') + + objs = read_all(name, f"select * from reactions_pipe_wall") + for obj in objs: + pipe = obj['pipe'] + value = obj['value'] + lines.append(f'WALL {pipe} {value}') + + objs = read_all(name, f"select * from reactions_tank") + for obj in objs: + tank = obj['tank'] + value = obj['value'] + lines.append(f'TANK {tank} {value}') + + return lines + + +def delete_pipe_reaction_by_pipe(name: str, pipe: str) -> ChangeSet: + row1 = try_read(name, f"select * from reactions_pipe_bulk where pipe = '{pipe}'") + row2 = try_read(name, f"select * from reactions_pipe_wall where pipe = '{pipe}'") + if row1 == None and row2 == None: + return ChangeSet() + return ChangeSet(g_update_prefix | {'type': 'pipe_reaction', 'pipe': pipe, 'bulk': None, 'wall': None}) + + +def delete_tank_reaction_by_tank(name: str, tank: str) -> ChangeSet: + row = try_read(name, f"select * from reactions_tank where tank = '{tank}'") + if row == None: + return ChangeSet() + return ChangeSet(g_update_prefix | {'type': 'tank_reaction', 'tank': tank, 'value': None}) diff --git a/app/native/api/s1_title.py b/app/native/api/s1_title.py new file mode 100644 index 0000000..3ec5d6a --- /dev/null +++ b/app/native/api/s1_title.py @@ -0,0 +1,40 @@ +from .database import * + + +def get_title_schema(name: str) -> dict[str, dict[str, Any]]: + return {'value': {'type': 'float', 'optional': False, 'readonly': False}} + + +def get_title(name: str) -> dict[str, Any]: + title = read(name, 'select * from title') + return { 'value': title['value'] } + + +def _set_title(name: str, cs: ChangeSet) -> DbChangeSet: + new = cs.operations[0]['value'] + old = get_title(name)['value'] + + redo_sql = f"update title set value = '{new}';" + undo_sql = f"update title set value = '{old}';" + + redo_cs = g_update_prefix | { 'type': 'title', 'value': new } + undo_cs = g_update_prefix | { 'type': 'title', 'value': old } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_title(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_title(name ,cs)) + + +def inp_in_title(section: list[str]) -> str: + if section == []: + return str('') + + title = '\n'.join(section) + return str(f"update title set value = '{title}';") + + +def inp_out_title(name: str) -> list[str]: + obj = str(get_title(name)['value']) + return obj.split('\n') diff --git a/app/native/api/s20_mixing.py b/app/native/api/s20_mixing.py new file mode 100644 index 0000000..5bef359 --- /dev/null +++ b/app/native/api/s20_mixing.py @@ -0,0 +1,150 @@ +from .database import * +from .s0_base import * + +MIXING_MODEL_MIXED = 'MIXED' +MIXING_MODEL_2COMP = '2COMP' +MIXING_MODEL_FIFO = 'FIFO' +MIXING_MODEL_LIFO = 'LIFO' + +def get_mixing_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'tank' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'model' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'value' : {'type': 'float' , 'optional': True , 'readonly': False} } + + +def get_mixing(name: str, tank: str) -> dict[str, Any]: + m = try_read(name, f"select * from mixing where tank = '{tank}'") + if m == None: + return {} + d = {} + d['tank'] = str(m['tank']) + d['model'] = str(m['model']) + d['value'] = float(m['value']) if m['value'] != None else None + return d + + +class Mixing(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'mixing' + self.tank = str(input['tank']) + self.model = str(input['model']) + self.value = float(input['value']) if 'value' in input and input['value'] != None else None + + self.f_type = f"'{self.type}'" + self.f_tank = f"'{self.tank}'" + self.f_model = f"'{self.model}'" + self.f_value = self.value if self.value != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'tank': self.tank, 'model': self.model, 'value': self.value } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'tank': self.tank } + + +def _set_mixing(name: str, cs: ChangeSet) -> DbChangeSet: + old = Mixing(get_mixing(name, cs.operations[0]['tank'])) + raw_new = get_mixing(name, cs.operations[0]['tank']) + + new_dict = cs.operations[0] + schema = get_mixing_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Mixing(raw_new) + + redo_sql = f"update mixing set model = {new.f_model}, value = {new.f_value} where tank = {new.f_tank};" + undo_sql = f"update mixing set model = {old.f_model}, value = {old.f_value} where tank = {old.f_tank};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_mixing(name: str, cs: ChangeSet) -> ChangeSet: + if 'tank' not in cs.operations[0]: + return ChangeSet() + if get_mixing(name, cs.operations[0]['tank']) == {}: + return ChangeSet() + return execute_command(name, _set_mixing(name, cs)) + + +def _add_mixing(name: str, cs: ChangeSet) -> DbChangeSet: + new = Mixing(cs.operations[0]) + + redo_sql = f"insert into mixing (tank, model, value) values ({new.f_tank}, {new.f_model}, {new.f_value});" + undo_sql = f"delete from mixing where tank = {new.f_tank};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_id_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_mixing(name: str, cs: ChangeSet) -> ChangeSet: + if 'tank' not in cs.operations[0]: + return ChangeSet() + if get_mixing(name, cs.operations[0]['tank']) != {}: + return ChangeSet() + return execute_command(name, _add_mixing(name, cs)) + + +def _delete_mixing(name: str, cs: ChangeSet) -> DbChangeSet: + old = Mixing(get_mixing(name, cs.operations[0]['tank'])) + + redo_sql = f"delete from mixing where tank = {old.f_tank};" + undo_sql = f"insert into mixing (tank, model, value) values ({old.f_tank}, {old.f_model}, {old.f_value});" + + redo_cs = g_delete_prefix | old.as_id_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_mixing(name: str, cs: ChangeSet) -> ChangeSet: + if 'tank' not in cs.operations[0]: + return ChangeSet() + if get_mixing(name, cs.operations[0]['tank']) == {}: + return ChangeSet() + return execute_command(name, _delete_mixing(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# TankID MixModel FractVolume +# FractVolume if type == MIX2 +#-------------------------------------------------------------- + + +def inp_in_mixing(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + tank = str(tokens[0]) + model = str(tokens[1].upper()) + value = float(tokens[3]) if num_without_desc >= 4 else None + value = value if value != None else 'null' + + return str(f"insert into mixing (tank, model, value) values ('{tank}', '{model}', {value});") + + +def inp_out_mixing(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from mixing') + for obj in objs: + tank = obj['tank'] + model = obj['model'] + value = obj['value'] if obj['value'] != None else '' + lines.append(f'{tank} {model} {value}') + return lines + + +def delete_mixing_by_tank(name: str, tank: str) -> ChangeSet: + row = try_read(name, f"select * from mixing where tank = '{tank}'") + if row == None: + return ChangeSet() + return ChangeSet(g_delete_prefix | {'type' : 'mixing', 'tank': tank}) diff --git a/app/native/api/s21_times.py b/app/native/api/s21_times.py new file mode 100644 index 0000000..44520d8 --- /dev/null +++ b/app/native/api/s21_times.py @@ -0,0 +1,112 @@ +from .database import * + +TIME_STATISTIC_NONE = 'NONE' +TIME_STATISTIC_AVERAGED = 'AVERAGED' +TIME_STATISTIC_MINIMUM = 'MINIMUM' +TIME_STATISTIC_MAXIMUM = 'MAXIMUM' +TIME_STATISTIC_RANGE = 'RANGE' + +element_schema = {'type': 'str' , 'optional': True , 'readonly': False} + +def get_time_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'DURATION' : element_schema, + 'HYDRAULIC TIMESTEP' : element_schema, + 'QUALITY TIMESTEP' : element_schema, + 'RULE TIMESTEP' : element_schema, + 'PATTERN TIMESTEP' : element_schema, + 'PATTERN START' : element_schema, + 'REPORT TIMESTEP' : element_schema, + 'REPORT START' : element_schema, + 'START CLOCKTIME' : element_schema, + 'STATISTIC' : element_schema} + + +def get_time(name: str) -> dict[str, Any]: + ts = read_all(name, f"select * from times") + d = {} + for e in ts: + d[e['key']] = str(e['value']) + return d + + +def _set_time(name: str, cs: ChangeSet) -> DbChangeSet: + raw_old = get_time(name) + + old = {} + new = {} + + new_dict = cs.operations[0] + schema = get_time_schema(name) + for key in schema.keys(): + if key in new_dict: + old[key] = str(raw_old[key]) + new[key] = str(new_dict[key]) + + redo_cs = g_update_prefix | { 'type' : 'time' } + + redo_sql = '' + for key, value in new.items(): + if redo_sql != '': + redo_sql += '\n' + redo_sql += f"update times set value = '{value}' where key = '{key}';" + redo_cs |= { key: value } + + undo_cs = g_update_prefix | { 'type' : 'time' } + + undo_sql = '' + for key, value in old.items(): + if undo_sql != '': + undo_sql += '\n' + undo_sql += f"update times set value = '{value}' where key = '{key}';" + undo_cs |= { key: value } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_time(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_time(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3] +# STATISTIC {NONE/AVERAGE/MIN/MAX/RANGE} +# DURATION value (units) +# HYDRAULIC TIMESTEP value (units) +# QUALITY TIMESTEP value (units) +# RULE TIMESTEP value (units) +# PATTERN TIMESTEP value (units) +# PATTERN START value (units) +# REPORT TIMESTEP value (units) +# REPORT START value (units) +# START CLOCKTIME value (AM PM) +# [EPA3] supports [EPA2] keyword +#-------------------------------------------------------------- + + +def inp_in_time(section: list[str]) -> str: + sql = '' + for s in section: + if s.startswith(';'): + continue + + line = s.upper().strip() + + # TOTAL DURATION => DURATION + if line.startswith('TOTAL DURATION'): + line = line.replace('TOTAL DURATION', 'DURATION') + + for key in get_time_schema('').keys(): + if line.startswith(key): + value = line.removeprefix(key).strip() + sql += f"update times set value = '{value}' where key = '{key}';" + return sql + + +def inp_out_time(name: str) -> list[str]: + lines = [] + objs = read_all(name, f"select * from times") + for obj in objs: + key = obj['key'] + value = obj['value'] + lines.append(f'{key} {value}') + return lines diff --git a/app/native/api/s22_report.py b/app/native/api/s22_report.py new file mode 100644 index 0000000..02b675b --- /dev/null +++ b/app/native/api/s22_report.py @@ -0,0 +1,34 @@ +from .database import * + + +#-------------------------------------------------------------- +# [EPA2] +# PAGE linesperpage +# STATUS {NONE/YES/FULL} +# SUMMARY {YES/NO} +# MESSAGES {YES/NO} +# ENERGY {NO/YES} +# NODES {NONE/ALL} +# NODES node1 node2 ... +# LINKS {NONE/ALL} +# LINKS link1 link2 ... +# FILE filename +# variable {YES/NO} +# variable {BELOW/ABOVE/PRECISION} value +# [EPA3][NOT SUPPORT] +# TRIALS {YES/NO} +#-------------------------------------------------------------- + + +def inp_in_report(section: list[str]) -> str: + return '' + + +def inp_out_report(name: str) -> list[str]: + lines = [] + objs = read_all(name, f"select * from report") + for obj in objs: + key = obj['key'] + value = obj['value'] + lines.append(f'{key} {value}') + return lines diff --git a/app/native/api/s23_options.py b/app/native/api/s23_options.py new file mode 100644 index 0000000..f1a372c --- /dev/null +++ b/app/native/api/s23_options.py @@ -0,0 +1,81 @@ +from .database import * +from .s23_options_util import get_option_schema, generate_v3 + + +def _inp_in_option(section: list[str]) -> ChangeSet: + if len(section) <= 0: + return ChangeSet() + + cs = g_update_prefix | { 'type' : 'option' } + for s in section: + if s.startswith(';'): + continue + + tokens = s.strip().split() + if tokens[0].upper() == 'PATTERN': # can not upper id + value = tokens[1] if len(tokens) > 1 else '' + cs |= { 'PATTERN' : value } + elif tokens[0].upper() == 'QUALITY': # can not upper trace node + value = tokens[1] if len(tokens) > 1 else '' + if len(tokens) > 2: + value += f' {tokens[2]}' + cs |= { 'QUALITY' : value } + else: + line = s.upper().strip() + for key in get_option_schema('').keys(): + if line.startswith(key): + value = line.removeprefix(key).strip() + cs |= { key : value } + + result = ChangeSet(cs) + result.merge(generate_v3(result)) + return result + + +def inp_in_option(section: list[str]) -> str: + sql = '' + result = _inp_in_option(section) + for op in result.operations: + for key in op.keys(): + if key == 'operation' or key == 'type': + continue + if op['type'] == 'option': + sql += f"update options set value = '{op[key]}' where key = '{key}';" + else: + sql += f"update options_v3 set value = '{op[key]}' where key = '{key}';" + return sql + + +def inp_out_option(name: str) -> list[str]: + lines = [] + objs = read_all(name, f"select * from options") + + is_dda = False + + for obj in objs: + if obj['key'] == 'DEMAND MODEL': + is_dda = obj['value'] == 'DDA' + + dda_ignore = [ + 'HEADERROR', # TODO: default is 0 which is conflict with PDA + 'FLOWCHANGE', # TODO: default is 0 which is conflict with PDA + 'MINIMUM PRESSURE', + 'REQUIRED PRESSURE', + 'PRESSURE EXPONENT' + ] + + for obj in objs: + key = obj['key'] + # why write this ? + if key == 'PRESSURE': + continue + # release version does not support new keys and has error message + if key == 'HTOL' or key == 'QTOL' or key == 'RQTOL': + continue + # ignore some weird settings for DDA + if is_dda and key in dda_ignore: + continue + value = obj['value'] + if str(value).strip() != '': + lines.append(f'{key} {value}') + return lines diff --git a/app/native/api/s23_options_util.py b/app/native/api/s23_options_util.py new file mode 100644 index 0000000..f273125 --- /dev/null +++ b/app/native/api/s23_options_util.py @@ -0,0 +1,401 @@ +from .database import * + + +#-------------------------------------------------------------- +# [EPANET2][IN][OUT] +# UNITS CFS/GPM/MGD/IMGD/AFD/LPS/LPM/MLD/CMH/CMD/SI +# PRESSURE PSI/KPA/M +# HEADLOSS H-W/D-W/C-M +# QUALITY NONE/AGE/TRACE/CHEMICAL (TraceNode) +# UNBALANCED STOP/CONTINUE {Niter} +# PATTERN id +# DEMAND MODEL DDA/PDA +# DEMAND MULTIPLIER value +# EMITTER EXPONENT value +# VISCOSITY value +# DIFFUSIVITY value +# SPECIFIC GRAVITY value +# TRIALS value +# ACCURACY value# +# HEADERROR value +# FLOWCHANGE value +# MINIMUM PRESSURE value +# REQUIRED PRESSURE value +# PRESSURE EXPONENT value# +# TOLERANCE value +# HTOL value +# QTOL value +# RQTOL value +# CHECKFREQ value +# MAXCHECK value +# DAMPLIMIT value +# ---- Unsupported Options ----- +# HYDRAULICS USE/SAVE filename +# MAP filename +#-------------------------------------------------------------- + + +element_schema = {'type': 'str' , 'optional': True , 'readonly': False} + + +OPTION_UNITS_CFS = 'CFS' +OPTION_UNITS_GPM = 'GPM' +OPTION_UNITS_MGD = 'MGD' +OPTION_UNITS_IMGD = 'IMGD' +OPTION_UNITS_AFD = 'AFD' +OPTION_UNITS_LPS = 'LPS' +OPTION_UNITS_LPM = 'LPM' +OPTION_UNITS_MLD = 'MLD' +OPTION_UNITS_CMH = 'CMH' +OPTION_UNITS_CMD = 'CMD' + +OPTION_PRESSURE_PSI = 'PSI' +OPTION_PRESSURE_KPA = 'KPA' +OPTION_PRESSURE_METERS = 'METERS' + +OPTION_HEADLOSS_HW = 'H-W' +OPTION_HEADLOSS_DW = 'D-W' +OPTION_HEADLOSS_CM = 'C-M' + +OPTION_UNBALANCED_STOP = 'STOP' +OPTION_UNBALANCED_CONTINUE = 'CONTINUE' + +OPTION_DEMAND_MODEL_DDA = 'DDA' +OPTION_DEMAND_MODEL_PDA = 'PDA' + +OPTION_QUALITY_NONE = 'NONE' +OPTION_QUALITY_CHEMICAL = 'CHEMICAL' +OPTION_QUALITY_AGE = 'AGE' +OPTION_QUALITY_TRACE = 'TRACE' + + +def get_option_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'UNITS' : element_schema, + 'PRESSURE' : element_schema, + 'HEADLOSS' : element_schema, + 'QUALITY' : element_schema, + 'UNBALANCED' : element_schema, + 'PATTERN' : element_schema, + 'DEMAND MODEL' : element_schema, + 'DEMAND MULTIPLIER' : element_schema, + 'EMITTER EXPONENT' : element_schema, + 'VISCOSITY' : element_schema, + 'DIFFUSIVITY' : element_schema, + 'SPECIFIC GRAVITY' : element_schema, + 'TRIALS' : element_schema, + 'ACCURACY' : element_schema, + 'HEADERROR' : element_schema, + 'FLOWCHANGE' : element_schema, + 'MINIMUM PRESSURE' : element_schema, + 'REQUIRED PRESSURE' : element_schema, + 'PRESSURE EXPONENT' : element_schema, + 'TOLERANCE' : element_schema, + 'HTOL' : element_schema, + 'QTOL' : element_schema, + 'RQTOL' : element_schema, + 'CHECKFREQ' : element_schema, + 'MAXCHECK' : element_schema, + 'DAMPLIMIT' : element_schema } + + +def get_option(name: str) -> dict[str, Any]: + ts = read_all(name, f"select * from options") + d = {} + for e in ts: + d[e['key']] = str(e['value']) + return d + + +def _set_option(name: str, cs: ChangeSet) -> DbChangeSet: + raw_old = get_option(name) + + old = {} + new = {} + + new_dict = cs.operations[0] + schema = get_option_schema(name) + for key in schema.keys(): + if key in new_dict: + old[key] = str(raw_old[key]) + new[key] = str(new_dict[key]) + + redo_cs = g_update_prefix | { 'type' : 'option' } + + redo_sql = '' + for key, value in new.items(): + if redo_sql != '': + redo_sql += '\n' + redo_sql += f"update options set value = '{value}' where key = '{key}';" + redo_cs |= { key: value } + + undo_cs = g_update_prefix | { 'type' : 'option' } + + undo_sql = '' + for key, value in old.items(): + if undo_sql != '': + undo_sql += '\n' + undo_sql += f"update options set value = '{value}' where key = '{key}';" + undo_cs |= { key: value } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_option(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_option(name, cs)) + + +OPTION_V3_FLOW_UNITS_CFS = OPTION_UNITS_CFS +OPTION_V3_FLOW_UNITS_GPM = OPTION_UNITS_GPM +OPTION_V3_FLOW_UNITS_MGD = OPTION_UNITS_MGD +OPTION_V3_FLOW_UNITS_IMGD = OPTION_UNITS_IMGD +OPTION_V3_FLOW_UNITS_AFD = OPTION_UNITS_AFD +OPTION_V3_FLOW_UNITS_LPS = OPTION_UNITS_LPS +OPTION_V3_FLOW_UNITS_LPM = OPTION_UNITS_LPM +OPTION_V3_FLOW_UNITS_MLD = OPTION_UNITS_MLD +OPTION_V3_FLOW_UNITS_CMH = OPTION_UNITS_CMH +OPTION_V3_FLOW_UNITS_CMD = OPTION_UNITS_CMD + +OPTION_V3_PRESSURE_UNITS_PSI = OPTION_PRESSURE_PSI +OPTION_V3_PRESSURE_UNITS_KPA = OPTION_PRESSURE_KPA +OPTION_V3_PRESSURE_UNITS_METERS = OPTION_PRESSURE_METERS + +OPTION_V3_HEADLOSS_MODEL_HW = OPTION_HEADLOSS_HW +OPTION_V3_HEADLOSS_MODEL_DW = OPTION_HEADLOSS_DW +OPTION_V3_HEADLOSS_MODEL_CM = OPTION_HEADLOSS_CM + +OPTION_V3_STEP_SIZING_FULL = 'FULL' +OPTION_V3_STEP_SIZING_RELAXATION = 'RELAXATION' +OPTION_V3_STEP_SIZING_LINESEARCH = 'LINESEARCH' + +OPTION_V3_IF_UNBALANCED_STOP = OPTION_UNBALANCED_STOP +OPTION_V3_IF_UNBALANCED_CONTINUE = OPTION_UNBALANCED_CONTINUE + +OPTION_V3_DEMAND_MODEL_FIXED = 'FIXED' +OPTION_V3_DEMAND_MODEL_CONSTRAINED = 'CONSTRAINED' +OPTION_V3_DEMAND_MODEL_POWER = 'POWER' +OPTION_V3_DEMAND_MODEL_LOGISTIC = 'LOGISTIC' + +OPTION_V3_LEAKAGE_MODEL_NONE = 'NONE' +OPTION_V3_LEAKAGE_MODEL_POWER = 'POWER' +OPTION_V3_LEAKAGE_MODEL_FAVAD = 'FAVAD' + +OPTION_V3_QUALITY_MODEL_NONE = OPTION_QUALITY_NONE +OPTION_V3_QUALITY_MODEL_CHEMICAL = OPTION_QUALITY_CHEMICAL +OPTION_V3_QUALITY_MODEL_AGE = OPTION_QUALITY_AGE +OPTION_V3_QUALITY_MODEL_TRACE = OPTION_QUALITY_TRACE + +OPTION_V3_QUALITY_UNITS_HRS = 'HRS' +OPTION_V3_QUALITY_UNITS_PCNT = 'PCNT' +OPTION_V3_QUALITY_UNITS_MGL = 'MG/L' +OPTION_V3_QUALITY_UNITS_UGL = 'UG/L' + + +def get_option_v3_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'FLOW_UNITS' : element_schema, + 'PRESSURE_UNITS' : element_schema, + 'HEADLOSS_MODEL' : element_schema, + 'SPECIFIC_GRAVITY' : element_schema, + 'SPECIFIC_VISCOSITY' : element_schema, + 'MAXIMUM_TRIALS' : element_schema, + 'HEAD_TOLERANCE' : element_schema, + 'FLOW_TOLERANCE' : element_schema, + 'FLOW_CHANGE_LIMIT' : element_schema, + 'RELATIVE_ACCURACY' : element_schema, + 'TIME_WEIGHT' : element_schema, + 'STEP_SIZING' : element_schema, + 'IF_UNBALANCED' : element_schema, + 'DEMAND_MODEL' : element_schema, + 'DEMAND_PATTERN' : element_schema, + 'DEMAND_MULTIPLIER' : element_schema, + 'MINIMUM_PRESSURE' : element_schema, + 'SERVICE_PRESSURE' : element_schema, + 'PRESSURE_EXPONENT' : element_schema, + 'LEAKAGE_MODEL' : element_schema, + 'LEAKAGE_COEFF1' : element_schema, + 'LEAKAGE_COEFF2' : element_schema, + 'EMITTER_EXPONENT' : element_schema, + 'QUALITY_MODEL' : element_schema, + 'QUALITY_NAME' : element_schema, + 'QUALITY_UNITS' : element_schema, + 'TRACE_NODE' : element_schema, + 'SPECIFIC_DIFFUSIVITY' : element_schema, + 'QUALITY_TOLERANCE' : element_schema } + + +def get_option_v3(name: str) -> dict[str, Any]: + ts = read_all(name, f"select * from options_v3") + d = {} + for e in ts: + d[e['key']] = str(e['value']) + return d + + +def _set_option_v3(name: str, cs: ChangeSet) -> DbChangeSet: + raw_old = get_option_v3(name) + + old = {} + new = {} + + new_dict = cs.operations[0] + schema = get_option_v3_schema(name) + for key in schema.keys(): + if key in new_dict: + old[key] = str(raw_old[key]) + new[key] = str(new_dict[key]) + + redo_cs = g_update_prefix | { 'type' : 'option_v3' } + + redo_sql = '' + for key, value in new.items(): + if redo_sql != '': + redo_sql += '\n' + redo_sql += f"update options_v3 set value = '{value}' where key = '{key}';" + redo_cs |= { key: value } + + undo_cs = g_update_prefix | { 'type' : 'option_v3' } + + undo_sql = '' + for key, value in old.items(): + if undo_sql != '': + undo_sql += '\n' + undo_sql += f"update options_v3 set value = '{value}' where key = '{key}';" + undo_cs |= { key: value } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_option_v3(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_option_v3(name, cs)) + + +_key_map_23 = { + 'UNITS' : 'FLOW_UNITS', + 'PRESSURE' : 'PRESSURE_UNITS', + 'HEADLOSS' : 'HEADLOSS_MODEL', + 'QUALITY' : 'QUALITY_MODEL', + 'UNBALANCED' : 'IF_UNBALANCED', + 'PATTERN' : 'DEMAND_PATTERN', + 'DEMAND MODEL' : 'DEMAND_MODEL', + 'DEMAND MULTIPLIER' : 'DEMAND_MULTIPLIER', + 'EMITTER EXPONENT' : 'EMITTER_EXPONENT', + 'VISCOSITY' : 'SPECIFIC_VISCOSITY', + 'DIFFUSIVITY' : 'SPECIFIC_DIFFUSIVITY', + 'SPECIFIC GRAVITY' : 'SPECIFIC_GRAVITY', + 'TRIALS' : 'MAXIMUM_TRIALS', + 'ACCURACY' : 'RELATIVE_ACCURACY', + #'HEADERROR' : '', + 'FLOWCHANGE' : 'FLOW_CHANGE_LIMIT', + 'MINIMUM PRESSURE' : 'MINIMUM_PRESSURE', + 'REQUIRED PRESSURE' : 'SERVICE_PRESSURE', + 'PRESSURE EXPONENT' : 'PRESSURE_EXPONENT', + 'TOLERANCE' : 'QUALITY_TOLERANCE', + 'HTOL' : 'HEAD_TOLERANCE', + 'QTOL' : 'FLOW_TOLERANCE', + #'RQTOL' : '', + #'CHECKFREQ' : '', + #'MAXCHECK' : '', + #'DAMPLIMIT' : '', +} + + +_key_map_32 = { + 'FLOW_UNITS' : 'UNITS', + 'PRESSURE_UNITS' : 'PRESSURE', + 'HEADLOSS_MODEL' : 'HEADLOSS', + 'SPECIFIC_GRAVITY' : 'SPECIFIC GRAVITY', + 'SPECIFIC_VISCOSITY' : 'VISCOSITY', + 'MAXIMUM_TRIALS' : 'TRIALS', + 'HEAD_TOLERANCE' : 'HTOL', + 'FLOW_TOLERANCE' : 'QTOL', + 'FLOW_CHANGE_LIMIT' : 'FLOWCHANGE', + 'RELATIVE_ACCURACY' : 'ACCURACY', + #'TIME_WEIGHT' : '', + #'STEP_SIZING' : '', + 'IF_UNBALANCED' : 'UNBALANCED', + 'DEMAND_MODEL' : 'DEMAND MODEL', + 'DEMAND_PATTERN' : 'PATTERN', + 'DEMAND_MULTIPLIER' : 'DEMAND MULTIPLIER', + 'MINIMUM_PRESSURE' : 'MINIMUM PRESSURE', + 'SERVICE_PRESSURE' : 'REQUIRED PRESSURE', + 'PRESSURE_EXPONENT' : 'PRESSURE EXPONENT', + #'LEAKAGE_MODEL' : '', + #'LEAKAGE_COEFF1' : '', + #'LEAKAGE_COEFF2' : '', + 'EMITTER_EXPONENT' : 'EMITTER EXPONENT', + 'QUALITY_MODEL' : 'QUALITY', + #'QUALITY_NAME' : '', + #'QUALITY_UNITS' : '', + #'TRACE_NODE' : '', + 'SPECIFIC_DIFFUSIVITY' : 'DIFFUSIVITY', + 'QUALITY_TOLERANCE' : 'TOLERANCE' +} + + +def generate_v2(cs: ChangeSet) -> ChangeSet: + op = cs.operations[0] + + if op['type'] == 'option': + return cs + + map = _key_map_32 + + cs_v2 = {} + for key in op: + if key == 'operation' or key == 'type': + continue + + if key in map.keys(): + if key != 'QUALITY_MODEL' and key != 'DEMAND_MODEL': + cs_v2 |= { map[key] : op[key] } + elif key == 'QUALITY_MODEL': + if str(op[key]).upper() == OPTION_QUALITY_TRACE and 'TRACE_NODE' in op.keys(): + cs_v2 |= { map[key] : f"{OPTION_QUALITY_TRACE} {op['TRACE_NODE']}" } + else: + cs_v2 |= { map[key] : str(op[key]).upper() } + elif key == 'DEMAND_MODEL': + if op[key] == OPTION_V3_DEMAND_MODEL_FIXED: + cs_v2 |= { map[key] : OPTION_DEMAND_MODEL_DDA } + else: + cs_v2 |= { map[key] : OPTION_DEMAND_MODEL_PDA } + + if len(cs_v2) > 0: + cs_v2 |= g_update_prefix | { 'type' : 'option' } + return ChangeSet(cs_v2) + + return ChangeSet() + + +def generate_v3(cs: ChangeSet) -> ChangeSet: + op = cs.operations[0] + + if op['type'] == 'option_v3': + return cs + + map = _key_map_23 + + cs_v3 = {} + for key in op: + if key == 'operation' or key == 'type': + continue + + if key in map.keys(): + if key != 'QUALITY' and key != 'DEMAND MODEL': + cs_v3 |= { map[key] : op[key] } + elif key == 'QUALITY': + tokens = str(op[key]).split() + if len(tokens) >= 1: + cs_v3 |= { map[key] : tokens[0].upper() } + if tokens[0].upper() == OPTION_QUALITY_TRACE and len(tokens) >= 2: + cs_v3 |= { 'TRACE_NODE' : tokens[1] } + elif key == 'DEMAND MODEL': + if op[key] == OPTION_DEMAND_MODEL_DDA: + cs_v3 |= { map[key] : OPTION_V3_DEMAND_MODEL_FIXED } + else: + cs_v3 |= { map[key] : OPTION_V3_DEMAND_MODEL_POWER } + + if len(cs_v3) > 0: + cs_v3 |= g_update_prefix | { 'type' : 'option_v3' } + return ChangeSet(cs_v3) + + return ChangeSet() + diff --git a/app/native/api/s23_options_v3.py b/app/native/api/s23_options_v3.py new file mode 100644 index 0000000..0a7738b --- /dev/null +++ b/app/native/api/s23_options_v3.py @@ -0,0 +1,79 @@ +from .database import * +from .s23_options_util import get_option_schema, get_option_v3_schema, generate_v2, generate_v3 + + +def _parse_v2(v2_lines: list[str]) -> dict[str, str]: + cs_v2 = g_update_prefix | { 'type' : 'option' } + for s in v2_lines: + tokens = s.split() + if tokens[0].upper() == 'PATTERN': # can not upper id + value = tokens[1] if len(tokens) > 1 else '' + cs_v2 |= { 'PATTERN' : value } + elif tokens[0].upper() == 'QUALITY': # can not upper trace node + value = tokens[1] + if len(tokens) > 2: + value += f' {tokens[2]}' + cs_v2 |= { 'QUALITY' : value } + else: + line = s.upper().strip() + for key in get_option_schema('').keys(): + if line.startswith(key): + value = line.removeprefix(key).strip() + cs_v2 |= { key : value } + return cs_v2 + + +def _inp_in_option_v3(section: list[str]) -> ChangeSet: + if len(section) <= 0: + return ChangeSet() + + cs_v3 = g_update_prefix | { 'type' : 'option_v3' } + v2_lines = [] + for s in section: + if s.startswith(';'): + continue + + tokens = s.strip().split() + key = tokens[0] + if key in get_option_v3_schema('').keys(): + value = '' + if len(tokens) == 2: + value = tokens[1] + elif len(tokens) > 2: + value = ' '.join(tokens[1:]) + cs_v3 |= { key : value } + else: + v2_lines.append(s.strip()) + + # unlikely... + cs_v2 = _parse_v2(v2_lines) + + result = ChangeSet(cs_v3) + result.merge(generate_v3(ChangeSet(cs_v2))) + result.merge(generate_v2(result)) + return result + + +def inp_in_option_v3(section: list[str]) -> str: + sql = '' + result = _inp_in_option_v3(section) + for op in result.operations: + for key in op.keys(): + if key == 'operation' or key == 'type': + continue + if op['type'] == 'option_v3': + sql += f"update options_v3 set value = '{op[key]}' where key = '{key}';" + else: + sql += f"update options set value = '{op[key]}' where key = '{key}';" + return sql + + +def inp_out_option_v3(name: str) -> list[str]: + lines = [] + objs = read_all(name, f"select * from options_v3") + for obj in objs: + key = obj['key'] + value = obj['value'] + if str(value).strip() != '': + lines.append(f'{key} {value}') + return lines diff --git a/app/native/api/s24_coordinates.py b/app/native/api/s24_coordinates.py new file mode 100644 index 0000000..d038a96 --- /dev/null +++ b/app/native/api/s24_coordinates.py @@ -0,0 +1,92 @@ +from .database import * +from .s0_base import get_link_nodes + +def sql_update_coord(node: str, x: float, y: float) -> str: + coord = f"st_geomfromtext('point({x} {y})')" + return str(f"update coordinates set coord = {coord} where node = '{node}';") + + +def sql_insert_coord(node: str, x: float, y: float) -> str: + coord = f"st_geomfromtext('point({x} {y})')" + return str(f"insert into coordinates (node, coord) values ('{node}', {coord});") + + +def sql_delete_coord(node: str) -> str: + return str(f"delete from coordinates where node = '{node}';") + + +def from_postgis_point(coord: str) -> dict[str, float]: + xy = coord.lower().removeprefix('point(').removesuffix(')').split(' ') + return { 'x': float(xy[0]), 'y': float(xy[1]) } + + +def get_node_coord(name: str, node: str) -> dict[str, float]: + row = try_read(name, f"select st_astext(coord) as coord_geom from coordinates where node = '{node}'") + if row == None: + write(name, sql_insert_coord(node, 0.0, 0.0)) + return {'x': 0.0, 'y': 0.0} + return from_postgis_point(row['coord_geom']) + +# DingZQ 2025-01-03, get nodes in extent +# return node id list +# node_id:junction:x:y +def get_nodes_in_extent(name: str, x1: float, y1: float, x2: float, y2: float) -> list[str]: + nodes = [] + objs = read_all(name, 'select node, st_astext(coord) as coord_geom from coordinates') + for obj in objs: + node_id = obj['node'] + coord = from_postgis_point(obj['coord_geom']) + x = coord['x'] + y = coord['y'] + if x1 <= x <= x2 and y1 <= y <= y2: + nodes.append(f"{node_id}:junction:{x}:{y}") + return nodes + +# DingZQ 2025-01-03, get links in extent +# return link id list +# link_id:pipe:node_id1:node_id2 +def get_links_in_extent(name: str, x1: float, y1: float, x2: float, y2: float) -> list[str]: + node_ids = set([s.split(':')[0] for s in get_nodes_in_extent(name, x1, y1, x2, y2)]) + + all_link_ids = [] + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select id from pipes") + for record in cur: + all_link_ids.append(record['id']) + + links = [] + for link_id in all_link_ids: + nodes = get_link_nodes(name, link_id) + if nodes[0] in node_ids and nodes[1] in node_ids: + links.append(f"{link_id}:pipe:{nodes[0]}:{nodes[1]}") + return links + + +def node_has_coord(name: str, node: str) -> bool: + return try_read(name, f"select node from coordinates where node = '{node}'") != None + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# id x y +#-------------------------------------------------------------- +# exception ! need merge to node change set ! + + +def inp_in_coord(line: str) -> str: + tokens = line.split() + node = tokens[0] + coord = f"st_geomfromtext('point({tokens[1]} {tokens[2]})')" + return str(f"insert into coordinates (node, coord) values ('{node}', {coord});") + + +def inp_out_coord(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select node, st_astext(coord) as coord_geom from coordinates') + for obj in objs: + node = obj['node'] + coord = from_postgis_point(obj['coord_geom']) + x = coord['x'] + y = coord['y'] + lines.append(f'{node} {x} {y}') + return lines diff --git a/app/native/api/s25_vertices.py b/app/native/api/s25_vertices.py new file mode 100644 index 0000000..0bd7647 --- /dev/null +++ b/app/native/api/s25_vertices.py @@ -0,0 +1,120 @@ +from .database import * + + +def get_vertex_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'link' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'coords' : {'type': 'list' , 'optional': False , 'readonly': False, + 'element': { 'x' : {'type': 'float' , 'optional': False , 'readonly': False }, + 'y' : {'type': 'float' , 'optional': False , 'readonly': False } }}} + + +def get_vertex(name: str, link: str) -> dict[str, Any]: + cus = read_all(name, f"select * from vertices where link = '{link}' order by _order") + cs = [] + for r in cus: + cs.append({ 'x': float(r['x']), 'y': float(r['y']) }) + return { 'link': link, 'coords': cs } + + +def _set_vertex(name: str, cs: ChangeSet) -> DbChangeSet: + link = cs.operations[0]['link'] + + old = get_vertex(name, link) + new = { 'link': link, 'coords': [] } + + f_link = f"'{link}'" + + # TODO: transaction ? + redo_sql = f"delete from vertices where link = {f_link};" + for xy in cs.operations[0]['coords']: + x, y = float(xy['x']), float(xy['y']) + f_x, f_y = x, y + redo_sql += f"\ninsert into vertices (link, x, y) values ({f_link}, {f_x}, {f_y});" + new['coords'].append({ 'x': x, 'y': y }) + + undo_sql = f"delete from vertices where link = {f_link};" + for xy in old['coords']: + f_x, f_y = xy['x'], xy['y'] + undo_sql += f"\ninsert into vertices (link, x, y) values ({f_link}, {f_x}, {f_y});" + + redo_cs = { 'type': 'vertex' } | new + undo_cs = { 'type': 'vertex' } | old + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_vertex(name: str, cs: ChangeSet) -> ChangeSet: + result = _set_vertex(name, cs) + result.redo_cs[0] |= g_update_prefix + result.undo_cs[0] |= g_update_prefix + return execute_command(name, result) + + +def _add_vertex(name: str, cs: ChangeSet) -> DbChangeSet: + result = _set_vertex(name, cs) + result.redo_cs[0] |= g_add_prefix + result.undo_cs[0] |= g_delete_prefix + return result + + +def _delete_vertex(name: str, cs: ChangeSet) -> DbChangeSet: + cs.operations[0]['coords'] = [] + result = _set_vertex(name, cs) + result.redo_cs[0] |= g_delete_prefix + result.undo_cs[0] |= g_add_prefix + return result + + +def add_vertex(name: str, cs: ChangeSet) -> ChangeSet: + result = _add_vertex(name, cs) + return execute_command(name, result) + + +def delete_vertex(name: str, cs: ChangeSet) -> ChangeSet: + result = _delete_vertex(name, cs) + return execute_command(name, result) + + +def get_all_vertex_links(name: str) -> list[str]: + result : list[str] = [] + rows = read_all(name, 'select link from vertices order by link') + for row in rows: + result.append(str(row['link'])) + return result + + +def get_all_vertices(name: str) -> list[dict[str, Any]]: + return read_all(name, 'select * from vertices order by link') + + +#-------------------------------------------------------------- +# [EPA2][IN][OUT] +# id x y +# [EPA3][NOT SUPPORT] +#-------------------------------------------------------------- + + +def inp_in_vertex(line: str) -> str: + tokens = line.split() + link = tokens[0] + x = float(tokens[1]) + y = float(tokens[2]) + return str(f"insert into vertices (link, x, y) values ('{link}', {x}, {y});") + + +def inp_out_vertex(name: str) -> list[str]: + lines = [] + objs = read_all(name, f"select * from vertices order by _order") + for obj in objs: + link = obj['link'] + x = obj['x'] + y = obj['y'] + lines.append(f"{link} {x} {y}") + return lines + + +def delete_vertex_by_link(name: str, link: str) -> ChangeSet: + row = try_read(name, f"select * from vertices where link = '{link}'") + if row == None: + return ChangeSet() + return ChangeSet(g_delete_prefix | {'type': 'vertex', 'link' : link}) diff --git a/app/native/api/s26_labels.py b/app/native/api/s26_labels.py new file mode 100644 index 0000000..3bb0596 --- /dev/null +++ b/app/native/api/s26_labels.py @@ -0,0 +1,137 @@ +from .database import * + + +def get_label_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'x' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'y' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'label' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'node' : {'type': 'str' , 'optional': True , 'readonly': False} } + + +def get_label(name: str, x: float, y: float) -> dict[str, Any]: + d = {} + d['x'] = x + d['y'] = y + l = try_read(name, f'select * from labels where x = {x} and y = {y}') + if l == None: + d['label'] = None + d['node'] = None + else: + d['label'] = str(l['label']) + d['node'] = str(l['node']) if l['node'] != None else None + return d + + +class Label(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'label' + self.x = float(input['x']) + self.y = float(input['y']) + self.label = str(input['label']) + self.node = str(input['node']) if 'node' in input and input['node'] != None else None + + self.f_type = f"'{self.type}'" + self.f_x = self.x + self.f_y = self.y + self.f_label = f"'{self.label}'" + self.f_node = f"'{self.node}'" if self.node != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'x': self.x, 'y': self.y, 'label': self.label, 'node': self.node } + + def as_xy_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'x': self.x, 'y': self.y } + + +def _set_label(name: str, cs: ChangeSet) -> DbChangeSet: + old = Label(get_label(name, cs.operations[0]['x'], cs.operations[0]['y'])) + raw_new = get_label(name, cs.operations[0]['x'], cs.operations[0]['y']) + + new_dict = cs.operations[0] + schema = get_label_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Label(raw_new) + + redo_sql = f"update labels set label = {new.f_label}, node = {new.f_node} where x = {new.f_x} and y = {new.f_y};" + undo_sql = f"update labels set label = {old.f_label}, node = {old.f_node} where x = {old.f_x} and y = {old.f_y};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_label(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_label(name, cs)) + + +def _add_label(name: str, cs: ChangeSet) -> DbChangeSet: + new = Label(cs.operations[0]) + + redo_sql = f"insert into labels (x, y, label, node) values ({new.f_x}, {new.f_y}, {new.f_label}, {new.f_node});" + undo_sql = f"delete from labels where x = {new.f_x} and y = {new.f_y};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_xy_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_label(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _add_label(name, cs)) + + +def _delete_label(name: str, cs: ChangeSet) -> DbChangeSet: + old = Label(get_label(name, cs.operations[0]['x'], cs.operations[0]['y'])) + + redo_sql = f"delete from labels where x = {old.f_x} and y = {old.f_y};" + undo_sql = f"insert into labels (x, y, label, node) values ({old.f_x}, {old.f_y}, {old.f_label}, {old.f_node});" + + redo_cs = g_delete_prefix | old.as_xy_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_label(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _delete_label(name, cs)) + + +def inp_in_label(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + x = float(tokens[0]) + y = float(tokens[1]) + label = str(tokens[2]) + node = str(tokens[3]) if num >= 4 else None + node = f"'{node}'" if node != None else 'null' + + return str(f"insert into labels (x, y, label, node) values ({x}, {y}, '{label}', {node});") + + +def inp_out_label(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from labels') + for obj in objs: + x = obj['x'] + y = obj['y'] + label = obj['label'] + node = obj['node'] if obj['node'] != None else '' + lines.append(f'{x} {y} {label} {node}') + return lines + + +def unset_label_by_node(name: str, node: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select x, y from labels where node = '{node}'") + for row in rows: + cs.append(g_update_prefix | {'type': 'label', 'x': row['x'], 'y': row['y'], 'node': None}) + + return cs diff --git a/app/native/api/s27_backdrop.py b/app/native/api/s27_backdrop.py new file mode 100644 index 0000000..bedc8fa --- /dev/null +++ b/app/native/api/s27_backdrop.py @@ -0,0 +1,39 @@ +from .database import * + + +def get_backdrop_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'content' : {'type': 'str' , 'optional': False , 'readonly': False} } + + +def get_backdrop(name: str) -> dict[str, Any]: + e = read(name, f"select * from backdrop") + return { 'content': e['content'] } + + +def _set_backdrop(name: str, cs: ChangeSet) -> DbChangeSet: + old = get_backdrop(name) + + redo_sql = f"update backdrop set content = '{cs.operations[0]['content']}' where content = '{old['content']}';" + undo_sql = f"update backdrop set content = '{old['content']}' where content = '{cs.operations[0]['content']}';" + + redo_cs = g_update_prefix | { 'type': 'backdrop', 'content': cs.operations[0]['content'] } + undo_cs = g_update_prefix | { 'type': 'backdrop', 'content': old['content'] } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_backdrop(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_backdrop(name, cs)) + + +def inp_in_backdrop(section: list[str]) -> str: + if section == []: + return str('') + + content = '\n'.join(section) + return str(f"update backdrop set content = '{content}';") + + +def inp_out_backdrop(name: str) -> list[str]: + obj = str(get_backdrop(name)['content']) + return obj.split('\n') \ No newline at end of file diff --git a/app/native/api/s28_end.py b/app/native/api/s28_end.py new file mode 100644 index 0000000..e69de29 diff --git a/app/native/api/s29_scada_device.py b/app/native/api/s29_scada_device.py new file mode 100644 index 0000000..7af8ee0 --- /dev/null +++ b/app/native/api/s29_scada_device.py @@ -0,0 +1,123 @@ +from .database import * + + +SCADA_DEVICE_TYPE_PRESSURE = 'PRESSURE' +SCADA_DEVICE_TYPE_DEMAND = 'DEMAND' +SCADA_DEVICE_TYPE_QUALITY = 'QUALITY' +SCADA_DEVICE_TYPE_LEVEL = 'LEVEL' +SCADA_DEVICE_TYPE_FLOW = 'FLOW' +SCADA_DEVICE_TYPE_UNKNOWN = 'UNKNOWN' + + +def get_scada_device_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str', 'optional': False, 'readonly': True }, + 'name' : {'type': 'str', 'optional': True , 'readonly': False}, + 'address': {'type': 'str', 'optional': True , 'readonly': False}, + 'sd_type': {'type': 'str', 'optional': True , 'readonly': False}} + + +def get_scada_device(name: str, id: str) -> dict[str, Any]: + sm = try_read(name, f"select * from scada_device where id = '{id}'") + if sm == None: + return {} + d = {} + d['id'] = str(sm['id']) + d['name'] = str(sm['name']) if sm['name'] != None else None + d['address'] = str(sm['address']) if sm['address'] != None else None + d['sd_type'] = str(sm['sd_type']) if sm['sd_type'] != None else None + return d + + +class ScadaDevice(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'scada_device' + self.id = str(input['id']) + self.name = str(input['name']) if 'name' in input and input['name'] != None else None + self.address = str(input['address']) if 'address' in input and input['address'] != None else None + self.sd_type = str(input['sd_type']) if 'sd_type' in input and input['sd_type'] != None else None + + self.f_type = f"'{self.type}'" + self.f_id = f"'{self.id}'" + self.f_name = f"'{self.name}'" if self.name != None else 'null' + self.f_address = f"'{self.address}'" if self.address != None else 'null' + self.f_sd_type = f"'{self.sd_type}'" if self.sd_type != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id, 'name': self.name, 'address': self.address, 'sd_type': self.sd_type } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id } + + +def _set_scada_device(name: str, cs: ChangeSet) -> DbChangeSet: + old = ScadaDevice(get_scada_device(name, cs.operations[0]['id'])) + raw_new = get_scada_device(name, cs.operations[0]['id']) + + new_dict = cs.operations[0] + schema = get_scada_device_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = ScadaDevice(raw_new) + + redo_sql = f"update scada_device set name = {new.f_name}, address = {new.f_address}, sd_type = {new.f_sd_type} where id = {new.f_id};" + undo_sql = f"update scada_device set name = {old.f_name}, address = {old.f_address}, sd_type = {old.f_sd_type} where id = {old.f_id};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_scada_device(name: str, cs: ChangeSet) -> ChangeSet: + if get_scada_device(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _set_scada_device(name, cs), False) + + +def _add_scada_device(name: str, cs: ChangeSet) -> DbChangeSet: + new = ScadaDevice(cs.operations[0]) + + redo_sql = f"insert into scada_device (id, name, address, sd_type) values ({new.f_id}, {new.f_name}, {new.f_address}, {new.f_sd_type});" + undo_sql = f"delete from scada_device where id = {new.f_id};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_id_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_scada_device(name: str, cs: ChangeSet) -> ChangeSet: + if get_scada_device(name, cs.operations[0]['id']) != {}: + return ChangeSet() + return execute_command(name, _add_scada_device(name, cs), False) + + +def _delete_scada_device(name: str, cs: ChangeSet) -> DbChangeSet: + old = ScadaDevice(get_scada_device(name, cs.operations[0]['id'])) + + redo_sql = f"delete from scada_device where id = {old.f_id};" + undo_sql = f"insert into scada_device (id, name, address, sd_type) values ({old.f_id}, {old.f_name}, {old.f_address}, {old.f_sd_type});" + + redo_cs = g_delete_prefix | old.as_id_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_scada_device(name: str, cs: ChangeSet) -> ChangeSet: + if get_scada_device(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_scada_device(name, cs), False) + + +def get_all_scada_device_ids(name: str) -> list[str]: + result : list[str] = [] + rows = read_all(name, 'select id from scada_device order by id') + for row in rows: + result.append(str(row['id'])) + return result + + +def get_all_scada_devices(name: str) -> list[dict[str, Any]]: + return read_all(name, 'select * from scada_device order by id') diff --git a/app/native/api/s2_junctions.py b/app/native/api/s2_junctions.py new file mode 100644 index 0000000..9007229 --- /dev/null +++ b/app/native/api/s2_junctions.py @@ -0,0 +1,191 @@ +from .database import * +from .s0_base import * +from .s24_coordinates import * + + +def get_junction_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'x' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'y' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'elevation' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'links' : {'type': 'str_list' , 'optional': False , 'readonly': True } } + + +def get_junction(name: str, id: str) -> dict[str, Any]: + j = try_read(name, f"select * from junctions where id = '{id}'") + if j == None: + return {} + xy = get_node_coord(name, id) + d = {} + d['id'] = str(j['id']) + d['x'] = float(xy['x']) + d['y'] = float(xy['y']) + d['elevation'] = float(j['elevation']) + d['links'] = get_node_links(name, id) + return d + +# DingZQ, 2025-03-29 +def get_all_junctions(name: str) -> list[dict[str, Any]]: + rows = read_all(name, f"select * from junctions") + if rows == None: + return [] + + result = [] + for row in rows: + d = {} + id = str(row['id']) + xy = get_node_coord(name, id) + d['id'] = id + d['x'] = float(xy['x']) + d['y'] = float(xy['y']) + d['elevation'] = float(row['elevation']) + d['links'] = get_node_links(name, id) + result.append(d) + + return result + +class Junction(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'junction' + self.id = str(input['id']) + self.x = float(input['x']) + self.y = float(input['y']) + self.elevation = float(input['elevation']) + + self.f_type = f"'{self.type}'" + self.f_id = f"'{self.id}'" + self.f_elevation = self.elevation + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'elevation': self.elevation } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id } + + +def _set_junction(name: str, cs: ChangeSet) -> DbChangeSet: + old = Junction(get_junction(name, cs.operations[0]['id'])) + raw_new = get_junction(name, cs.operations[0]['id']) + + new_dict = cs.operations[0] + schema = get_junction_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Junction(raw_new) + + redo_sql = f"update junctions set elevation = {new.f_elevation} where id = {new.f_id};" + redo_sql += f"\n{sql_update_coord(new.id, new.x, new.y)}" + + undo_sql = sql_update_coord(old.id, old.x, old.y) + undo_sql += f"\nupdate junctions set elevation = {old.f_elevation} where id = {old.f_id};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_junction(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_junction(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _set_junction(name, cs)) + + +def _add_junction(name: str, cs: ChangeSet) -> DbChangeSet: + new = Junction(cs.operations[0]) + + redo_sql = f"insert into _node (id, type) values ({new.f_id}, {new.f_type});" + redo_sql += f"\ninsert into junctions (id, elevation) values ({new.f_id}, {new.f_elevation});" + redo_sql += f"\n{sql_insert_coord(new.id, new.x, new.y)}" + + undo_sql = sql_delete_coord(new.id) + undo_sql += f"\ndelete from junctions where id = {new.f_id};" + undo_sql += f"\ndelete from _node where id = {new.f_id};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_id_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_junction(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_junction(name, cs.operations[0]['id']) != {}: + return ChangeSet() + return execute_command(name, _add_junction(name, cs)) + + +def _delete_junction(name: str, cs: ChangeSet) -> DbChangeSet: + old = Junction(get_junction(name, cs.operations[0]['id'])) + + redo_sql = sql_delete_coord(old.id) + redo_sql += f"\ndelete from junctions where id = {old.f_id};" + redo_sql += f"\ndelete from _node where id = {old.f_id};" + + undo_sql = f"insert into _node (id, type) values ({old.f_id}, {old.f_type});" + undo_sql += f"\ninsert into junctions (id, elevation) values ({old.f_id}, {old.f_elevation});" + undo_sql += f"\n{sql_insert_coord(old.id, old.x, old.y)}" + + redo_cs = g_delete_prefix | old.as_id_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_junction(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_junction(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_junction(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2] +# [IN] +# id elev. (demand) (demand pattern) ;desc +# [OUT] +# id elev. ;desc +#-------------------------------------------------------------- +# [EPA3] +# [IN] +# id elev. (demand) (demand pattern) +# [OUT] +# id elev. * * minpressure fullpressure +#-------------------------------------------------------------- + + +def inp_in_junction(line: str, demand_outside: bool) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + id = str(tokens[0]) + elevation = float(tokens[1]) + demand = float(tokens[2]) if num_without_desc >= 3 and tokens[2] != '*' else None + pattern = str(tokens[3]) if num_without_desc >= 4 and tokens[3] != '*' else None + pattern = f"'{pattern}'" if pattern != None else 'null' + desc = str(tokens[-1]) if has_desc else None + + sql = f"insert into _node (id, type) values ('{id}', 'junction');insert into junctions (id, elevation) values ('{id}', {elevation});" + if demand != None and demand_outside == False: + sql += f"insert into demands (junction, demand, pattern) values ('{id}', {demand}, {pattern});" + + return str(sql) + + +def inp_out_junction(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from junctions') + for obj in objs: + id = obj['id'] + elev = obj['elevation'] + desc = ';' + lines.append(f'{id} {elev} {desc}') + return lines diff --git a/app/native/api/s30_scada_device_data.py b/app/native/api/s30_scada_device_data.py new file mode 100644 index 0000000..d1a6043 --- /dev/null +++ b/app/native/api/s30_scada_device_data.py @@ -0,0 +1,90 @@ +from .database import * + + +def get_scada_device_data_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'device_id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'data' : {'type': 'list' , 'optional': False , 'readonly': False, + 'element': { 'time' : {'type': 'str' , 'optional': False , 'readonly': False }, + 'value' : {'type': 'float' , 'optional': False , 'readonly': False } }}} + + +def get_scada_device_data(name: str, device_id: str) -> dict[str, Any]: + sds = read_all(name, f"select * from scada_device_data where device_id = '{device_id}' order by time") + ds = [] + for r in sds: + ds.append({ 'time': str(r['time']), 'value': float(r['value']) }) + return { 'device_id': device_id, 'data': ds } + + +def _set_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet: + device_id = cs.operations[0]['device_id'] + + old = get_scada_device_data(name, device_id) + new = { 'device_id': device_id, 'data': [] } + + f_device_id = f"'{device_id}'" + + # TODO: transaction ? + redo_sql = f"delete from scada_device_data where device_id = {f_device_id};" + for tv in cs.operations[0]['data']: + time, value = str(tv['time']), float(tv['value']) + f_time, f_value = f"'{time}'", value + redo_sql += f"\ninsert into scada_device_data (device_id, time, value) values ({f_device_id}, {f_time}, {f_value});" + new['data'].append({ 'time': time, 'value': value }) + + undo_sql = f"delete from scada_device_data where device_id = {f_device_id};" + for tv in old['data']: + time, value = str(tv['time']), float(tv['value']) + f_time, f_value = f"'{time}'", value + undo_sql += f"\ninsert into scada_device_data (device_id, time, value) values ({f_device_id}, {f_time}, {f_value});" + + redo_cs = g_update_prefix | { 'type': 'scada_device_data' } | new + undo_cs = g_update_prefix | { 'type': 'scada_device_data' } | old + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_scada_device_data(name, cs), False) + + +def _add_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet: + values = cs.operations[0] + device_id = values['device_id'] + time = values['time'] + value = float(values['value']) + + redo_sql = f"insert into scada_device_data (device_id, time, value) values ('{device_id}', '{time}', {value});" + undo_sql = f"delete from scada_device_data where device_id = '{device_id}' and time = '{time}';" + redo_cs = g_add_prefix | { 'type': 'scada_device_data', 'device_id': device_id, 'time': time, 'value': value } + undo_cs = g_delete_prefix | { 'type': 'scada_device_data', 'device_id': device_id, 'time': time } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet: + row = try_read(name, f"select * from scada_device_data where device_id = '{cs.operations[0]['device_id']}' and time = '{cs.operations[0]['time']}'") + if row != None: + return ChangeSet() + return execute_command(name, _add_scada_device_data(name, cs), False) + + +def _delete_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet: + values = cs.operations[0] + device_id = values['device_id'] + time = values['time'] + value = float(read(name, f"select * from scada_device_data where device_id = '{device_id}' and time = '{time}'")['value']) + + redo_sql = f"delete from scada_device_data where device_id = '{device_id}' and time = '{time}';" + undo_sql = f"insert into scada_device_data (device_id, time, value) values ('{device_id}', '{time}', {value});" + redo_cs = g_delete_prefix | { 'type': 'scada_device_data', 'device_id': device_id, 'time': time } + undo_cs = g_add_prefix | { 'type': 'scada_device_data', 'device_id': device_id, 'time': time, 'value': value } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet: + row = try_read(name, f"select * from scada_device_data where device_id = '{cs.operations[0]['device_id']}' and time = '{cs.operations[0]['time']}'") + if row == None: + return ChangeSet() + return execute_command(name, _delete_scada_device_data(name, cs), False) diff --git a/app/native/api/s31_scada_element.py b/app/native/api/s31_scada_element.py new file mode 100644 index 0000000..457def3 --- /dev/null +++ b/app/native/api/s31_scada_element.py @@ -0,0 +1,197 @@ +from .database import * +from .s0_base import * + + +SCADA_TYPE_PRESSURE = 'PRESSURE' +SCADA_TYPE_DEMAND = 'DEMAND' +SCADA_TYPE_QUALITY = 'QUALITY' +SCADA_TYPE_LEVEL = 'LEVEL' +SCADA_TYPE_FLOW = 'FLOW' + + +SCADA_MODEL_TYPE_JUNCTION = 'JUNCTION' +SCADA_MODEL_TYPE_RESERVOIR = 'RESERVOIR' +SCADA_MODEL_TYPE_TANK = 'TANK' +SCADA_MODEL_TYPE_PIPE = 'PIPE' +SCADA_MODEL_TYPE_PUMP = 'PUMP' +SCADA_MODEL_TYPE_VALVE = 'VALVE' + + +SCADA_ELEMENT_STATUS_OFFLINE = 'OFF' +SCADA_ELEMENT_STATUS_ONLINE = 'ON' + + +_scada_model_types = [SCADA_MODEL_TYPE_JUNCTION, SCADA_MODEL_TYPE_RESERVOIR, SCADA_MODEL_TYPE_TANK, SCADA_MODEL_TYPE_PIPE, SCADA_MODEL_TYPE_PUMP, SCADA_MODEL_TYPE_VALVE] + + +def _check_model(name: str, cs: ChangeSet) -> bool: + has_model_id = 'model_id' in cs.operations[0] + has_model_type = 'model_type' in cs.operations[0] + + if has_model_id and has_model_type: + pass + elif has_model_id and not has_model_type: + return False + elif not has_model_id and has_model_type: + return False + elif not has_model_id and not has_model_type: + return True + + _model_id = cs.operations[0]['model_id'] + _model_type = cs.operations[0]['model_type'] + if _model_type == SCADA_MODEL_TYPE_JUNCTION: + return is_junction(name, _model_id) + elif _model_type == SCADA_MODEL_TYPE_RESERVOIR: + return is_reservoir(name, _model_id) + elif _model_type == SCADA_MODEL_TYPE_TANK: + return is_tank(name, _model_id) + elif _model_type == SCADA_MODEL_TYPE_PIPE: + return is_pipe(name, _model_id) + elif _model_type == SCADA_MODEL_TYPE_PUMP: + return is_pump(name, _model_id) + elif _model_type == SCADA_MODEL_TYPE_VALVE: + return is_valve(name, _model_id) + return False + + +def get_scada_element_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'x' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'y' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'device_id' : {'type': 'str' , 'optional': True , 'readonly': False}, + 'model_id' : {'type': 'str' , 'optional': True , 'readonly': False}, + 'model_type' : {'type': 'str' , 'optional': True , 'readonly': False}, + 'status' : {'type': 'str' , 'optional': True , 'readonly': False} } + + +def get_scada_element(name: str, id: str) -> dict[str, Any]: + sm = try_read(name, f"select * from scada_element where id = '{id}'") + if sm == None: + return {} + d = {} + d['id'] = str(sm['id']) + d['x'] = float(sm['x']) + d['y'] = float(sm['y']) + d['device_id'] = str(sm['device_id']) if sm['device_id'] != None else None + d['model_id'] = str(sm['model_id']) if sm['model_id'] != None else None + d['model_type'] = str(sm['model_type']) if sm['model_type'] != None else None + d['status'] = str(sm['status']) + return d + + +class ScadaModel(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'scada_element' + self.id = str(input['id']) + self.x = float(input['x']) + self.y = float(input['y']) + self.device_id = str(input['device_id']) if 'device_id' in input and input['device_id'] != None else None + self.model_id = str(input['model_id']) if 'model_id' in input and input['model_id'] != None else None + self.model_type = str(input['model_type']) if 'model_type' in input and input['model_type'] != None else None + self.status = str(input['status']) if 'status' in input and input['status'] != None else SCADA_ELEMENT_STATUS_OFFLINE + + self.f_type = f"'{self.type}'" + self.f_id = f"'{self.id}'" + self.f_x = self.x + self.f_y = self.y + self.f_device_id = f"'{self.device_id}'" if self.device_id != None else 'null' + self.f_model_id = f"'{self.model_id}'" if self.model_id != None else 'null' + self.f_model_type = f"'{self.model_type}'" if self.model_type != None else 'null' + self.f_status = f"'{self.status}'" + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'device_id': self.device_id, 'model_id': self.model_id, 'model_type': self.model_type, 'status': self.status } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id } + + +def _set_scada_element(name: str, cs: ChangeSet) -> DbChangeSet: + old = ScadaModel(get_scada_element(name, cs.operations[0]['id'])) + raw_new = get_scada_element(name, cs.operations[0]['id']) + + new_dict = cs.operations[0] + schema = get_scada_element_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = ScadaModel(raw_new) + + redo_sql = f"update scada_element set x = {new.f_x}, y = {new.f_y}, device_id = {new.f_device_id}, model_id = {new.f_model_id}, model_type = {new.f_model_type}, status = {new.f_status} where id = {new.f_id};" + undo_sql = f"update scada_element set x = {old.f_x}, y = {old.f_y}, device_id = {old.f_device_id}, model_id = {old.f_model_id}, model_type = {old.f_model_type}, status = {old.f_status} where id = {old.f_id};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_scada_element(name: str, cs: ChangeSet) -> ChangeSet: + if get_scada_element(name, cs.operations[0]['id']) == {}: + return ChangeSet() + if _check_model(name, cs) == False: + return ChangeSet() + return execute_command(name, _set_scada_element(name, cs)) + + +def _add_scada_element(name: str, cs: ChangeSet) -> DbChangeSet: + new = ScadaModel(cs.operations[0]) + + redo_sql = f"insert into scada_element (id, x, y, device_id, model_id, model_type, status) values ({new.f_id}, {new.f_x}, {new.f_y}, {new.f_device_id}, {new.f_model_id}, {new.f_model_type}, {new.f_status});" + undo_sql = f"delete from scada_element where id = {new.f_id};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_id_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_scada_element(name: str, cs: ChangeSet) -> ChangeSet: + if get_scada_element(name, cs.operations[0]['id']) != {}: + return ChangeSet() + if _check_model(name, cs) == False: + return ChangeSet() + return execute_command(name, _add_scada_element(name, cs)) + + +def _delete_scada_element(name: str, cs: ChangeSet) -> DbChangeSet: + old = ScadaModel(get_scada_element(name, cs.operations[0]['id'])) + + redo_sql = f"delete from scada_element where id = {old.f_id};" + undo_sql = f"insert into scada_element (id, x, y, device_id, model_id, model_type, status) values ({old.f_id}, {old.f_x}, {old.f_y}, {old.f_device_id}, {old.f_model_id}, {old.f_model_type}, {old.f_status});" + + redo_cs = g_delete_prefix | old.as_id_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_scada_element(name: str, cs: ChangeSet) -> ChangeSet: + if get_scada_element(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_scada_element(name, cs)) + + +def get_all_scada_element_ids(name: str) -> list[str]: + result : list[str] = [] + rows = read_all(name, 'select id from scada_element order by id') + for row in rows: + result.append(str(row['id'])) + return result + +# +# create table scada_element +# ( +# id text primary key +# , x float8 not null +# , y float8 not null +# , device_id text references scada_device(id) +# , model_id varchar(32) -- add constraint in API +# , model_type scada_model_type +# , status scada_element_status not null default 'OFF' +# ); +# +# 返回list,list里每个item是dict,内容是 'id':'abc' 这样 +# scada_model type 是类似pressure,flow之类的,是由Device 决定 的 +def get_all_scada_elements(name: str) -> list[dict[str, Any]]: + return read_all(name, 'select * from scada_element order by id') diff --git a/app/native/api/s32_region.py b/app/native/api/s32_region.py new file mode 100644 index 0000000..95fd67f --- /dev/null +++ b/app/native/api/s32_region.py @@ -0,0 +1,95 @@ +from .database import * +from .s32_region_util import from_postgis_polygon, to_postgis_polygon + +def get_region_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'boundary' : {'type': 'tuple_list' , 'optional': False , 'readonly': False} } + + +def get_region(name: str, id: str) -> dict[str, Any]: + r = try_read(name, f"select id, st_astext(boundary) as boundary_geom from region where id = '{id}'") + if r == None: + return {} + d = {} + d['id'] = str(r['id']) + d['boundary'] = from_postgis_polygon(str(r['boundary_geom'])) + return d + + +def _set_region(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + new = cs.operations[0]['boundary'] + old = get_region(name, id)['boundary'] + + redo_sql = f"update region set boundary = st_geomfromtext('{to_postgis_polygon(new)}') where id = '{id}';" + undo_sql = f"update region set boundary = st_geomfromtext('{to_postgis_polygon(old)}') where id = '{id}';" + redo_cs = g_update_prefix | { 'type': 'region', 'id': id, 'boundary': new } + undo_cs = g_update_prefix | { 'type': 'region', 'id': id, 'boundary': old } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_region(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0] or 'boundary' not in cs.operations[0]: + return ChangeSet() + b = cs.operations[0]['boundary'] + if len(b) < 4 or b[0] != b[-1]: + return ChangeSet() + if get_region(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _set_region(name, cs)) + + +def _add_region(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + new = cs.operations[0]['boundary'] + + redo_sql = f"insert into region (id, boundary) values ('{id}', '{to_postgis_polygon(new)}');" + undo_sql = f"delete from region where id = '{id}';" + redo_cs = g_add_prefix | { 'type': 'region', 'id': id, 'boundary': new } + undo_cs = g_delete_prefix | { 'type': 'region', 'id': id } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_region(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0] or 'boundary' not in cs.operations[0]: + return ChangeSet() + b = cs.operations[0]['boundary'] + if len(b) < 4 or b[0] != b[-1]: + return ChangeSet() + if get_region(name, cs.operations[0]['id']) != {}: + return ChangeSet() + return execute_command(name, _add_region(name, cs)) + + +def _delete_region(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + old = get_region(name, id)['boundary'] + + redo_sql = f"delete from region where id = '{id}';" + undo_sql = f"insert into region (id, boundary) values ('{id}', '{to_postgis_polygon(old)}');" + redo_cs = g_delete_prefix | { 'type': 'region', 'id': id } + undo_cs = g_add_prefix | { 'type': 'region', 'id': id, 'boundary': old } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_region(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_region(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_region(name, cs)) + +def inp_in_region(line: str) -> str: + tokens = line.split() + return str(f"insert into _region (id, type) values ('{tokens[0]}', '{tokens[1]}');") + +def inp_in_bound(line: str) -> str: + tokens = line.split() + return tokens[0] + +def inp_in_regionnodes(line: str)->str: + tokens = line.split() + return tokens[0] \ No newline at end of file diff --git a/app/native/api/s32_region_util.py b/app/native/api/s32_region_util.py new file mode 100644 index 0000000..4fdb46d --- /dev/null +++ b/app/native/api/s32_region_util.py @@ -0,0 +1,463 @@ +import ctypes +import platform +import os +import math +from typing import Any +from .s0_base import get_node_links, get_link_nodes, is_pipe +from .s5_pipes import get_pipe +from .database import read, try_read, read_all, write +from .s24_coordinates import node_has_coord, get_node_coord + + +def from_postgis_polygon(polygon: str) -> list[tuple[float, float]]: + boundary = polygon.lower().removeprefix('polygon((').removesuffix('))').split(',') + xys = [] + for pt in boundary: + xy = pt.split(' ') + xys.append((float(xy[0]), float(xy[1]))) + return xys + + +def to_postgis_polygon(boundary: list[tuple[float, float]]) -> str: + polygon = '' + for pt in boundary: + polygon += f'{pt[0]} {pt[1]},' + return str(f'polygon(({polygon[:-1]}))') + + +def to_postgis_linestring(boundary: list[tuple[float, float]]) -> str: + line = '' + for pt in boundary: + line += f'{pt[0]} {pt[1]},' + return str(f'linestring({line[:-1]})') + + +def get_nodes_in_boundary(name: str, boundary: list[tuple[float, float]]) -> list[str]: + api = 'get_nodes_in_boundary' + write(name, f"delete from temp_region where id = '{api}'") + write(name, f"insert into temp_region (id, boundary) values ('{api}', '{to_postgis_polygon(boundary)}')") + + nodes: list[str] = [] + for row in read_all(name, f"select c.node from coordinates as c, temp_region as r where ST_Intersects(c.coord, r.boundary) and r.id = '{api}'"): + nodes.append(row['node']) + + write(name, f"delete from temp_region where id = '{api}'") + + return nodes + + +def _get_links_on_boundary(name: str, nodes: list[str]) -> list[str]: + links: list[str] = [] + + for node in nodes: + node_links = get_node_links(name, node) + for link in node_links: + if link in links: + continue + + link_nodes = get_link_nodes(name, link) + if link_nodes[0] in nodes and link_nodes[1] not in nodes: + links.append(link) + elif link_nodes[0] not in nodes and link_nodes[1] in nodes: + links.append(link) + + return links + + +# if region is general or wda => get_nodes_in_boundary +# if region is dma, sa or vd => get stored nodes in table +def get_nodes_in_region(name: str, region_id: str) -> list[str]: + nodes: list[str] = [] + + row = try_read(name, f"select r_type from region where id = '{region_id}'") + if row == None: + return nodes + + r_type = str(row['r_type']) + + if r_type == 'DMA' or r_type == 'SA' or r_type == 'VD': + table = '' + if r_type == 'DMA': + table = 'region_dma' + elif r_type == 'SA': + table = 'region_sa' + elif r_type == 'VD': + table = 'region_vd' + + if table != '': + row = try_read(name, f"select nodes from {table} where id = '{region_id}'") + if row != None: + nodes = eval(str(row['nodes'])) + + if nodes == []: + for row in read_all(name, f"select c.node from coordinates as c, region as r where ST_Intersects(c.coord, r.boundary) and r.id = '{region_id}'"): + nodes.append(row['node']) + + return nodes + + +def get_links_on_region_boundary(name: str, region_id: str) -> list[str]: + nodes = get_nodes_in_region(name, region_id) + print(nodes) + return _get_links_on_boundary(name, nodes) + + +def calculate_convex_hull(name: str, nodes: list[str]) -> list[tuple[float, float]]: + write(name, f'delete from temp_node') + for node in nodes: + write(name, f"insert into temp_node values ('{node}')") + + # TODO: check none + polygon = read(name, f'select st_astext(st_convexhull(st_collect(array(select coord from coordinates where node in (select * from temp_node))))) as boundary' )['boundary'] + write(name, f'delete from temp_node') + + return from_postgis_polygon(polygon) + + +def _verify_platform(): + _platform = platform.system() + if _platform != "Windows": + raise Exception(f'Platform {_platform} unsupported (not yet)') + + +def _normal(v: tuple[float, float]) -> tuple[float, float]: + l = math.sqrt(v[0] * v[0] + v[1] * v[1]) + return (v[0] / l, v[1] / l) + + +def _angle(v: tuple[float, float]) -> float: + if v[0] >= 0 and v[1] >= 0: + return math.asin(v[1]) + elif v[0] <= 0 and v[1] >= 0: + return math.pi - math.asin(v[1]) + elif v[0] <= 0 and v[1] <= 0: + return math.asin(-v[1]) + math.pi + elif v[0] >= 0 and v[1] <= 0: + return math.pi * 2 - math.asin(-v[1]) + return 0 + + +def _angle_of_node_link(node: str, link: str, nodes, links) -> float: + n1 = node + n2 = links[link]['node1'] if n1 == links[link]['node2'] else links[link]['node2'] + x1, y1 = nodes[n1]['x'], nodes[n1]['y'] + x2, y2 = nodes[n2]['x'], nodes[n2]['y'] + if y1 == y2: + v = ((x2 - x1) / abs(x2 - x1), 0.0) + else: + v = _normal((x2 - x1, y2 - y1)) + return _angle(v) + + +class Topology: + def __init__(self, db: str, nodes: list[str]) -> None: + self._nodes: dict[str, Any] = {} + self._max_x_node = '' + self._node_list: list[str] = [] + for node in nodes: + if not node_has_coord(db, node): + continue + if get_node_links(db, node) == 0: + continue + self._nodes[node] = get_node_coord(db, node) | { 'links': [] } + self._node_list.append(node) + if self._max_x_node == '' or self._nodes[node]['x'] > self._nodes[self._max_x_node]['x']: + self._max_x_node = node + + self._links: dict[str, Any] = {} + self._link_list: list[str] = [] + for node in self._nodes: + for link in get_node_links(db, node): + candidate = True + link_nodes = get_link_nodes(db, link) + for link_node in link_nodes: + if link_node not in self._nodes: + candidate = False + break + if candidate: + length = get_pipe(db, link)['length'] if is_pipe(db, link) else 0.0 + self._links[link] = { 'node1' : link_nodes[0], 'node2' : link_nodes[1], 'length' : length } + self._link_list.append(link) + if link not in self._nodes[link_nodes[0]]['links']: + self._nodes[link_nodes[0]]['links'].append(link) + if link not in self._nodes[link_nodes[1]]['links']: + self._nodes[link_nodes[1]]['links'].append(link) + + def nodes(self): + return self._nodes + + def node_list(self): + return self._node_list + + def max_x_node(self): + return self._max_x_node + + def links(self): + return self._links + + def link_list(self): + return self._link_list + + +def _calculate_boundary(cursor: str, t_nodes: dict[str, Any], t_links: dict[str, Any]) -> tuple[list[str], dict[str, list[str]], list[tuple[float, float]]]: + in_angle = 0 + + vertices: list[str] = [] + path: dict[str, list[str]] = {} + while True: + # prevent duplicated node + if len(vertices) > 0 and cursor == vertices[-1]: + break + + # prevent duplicated path + if len(vertices) >= 3 and vertices[0] == vertices[-1] and vertices[1] == cursor: + break + + vertices.append(cursor) + + sorted_links = [] + overlapped_link = '' + for link in t_nodes[cursor]['links']: + angle = _angle_of_node_link(cursor, link, t_nodes, t_links) + if angle == in_angle: + overlapped_link = link + continue + sorted_links.append((angle, link)) + + # work into a branch, return + if len(sorted_links) == 0: + path[overlapped_link] = [] + cursor = vertices[-2] + in_angle = _angle_of_node_link(cursor, overlapped_link, t_nodes, t_links) + continue + + sorted_links = sorted(sorted_links, key=lambda s:s[0]) + out_link = sorted_links[0][1] + for angle, link in sorted_links: + if angle > in_angle: + out_link = link + break + + path[out_link] = [] + cursor = t_links[out_link]['node1'] if cursor == t_links[out_link]['node2'] else t_links[out_link]['node2'] + in_angle = _angle_of_node_link(cursor, out_link, t_nodes, t_links) + + boundary: list[tuple[float, float]] = [] + for node in vertices: + boundary.append((t_nodes[node]['x'], t_nodes[node]['y'])) + + return (vertices, path, boundary) + + +def _collect_new_links(in_links: dict[str, list[str]], t_nodes: dict[str, Any], t_links: dict[str, Any], new_nodes: dict[str, Any], new_links: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: + for link, pts in in_links.items(): + node1 = t_links[link]['node1'] + node2 = t_links[link]['node2'] + x1, x2 = t_nodes[node1]['x'], t_nodes[node2]['x'] + y1, y2 = t_nodes[node1]['y'], t_nodes[node2]['y'] + + if node1 not in new_nodes: + new_nodes[node1] = { 'x': x1, 'y': y1, 'links': [] } + if node2 not in new_nodes: + new_nodes[node2] = { 'x': x2, 'y': y2, 'links': [] } + + x_delta = x2 - x1 + y_delta = y2 - y1 + use_x = abs(x_delta) > abs(y_delta) + + if len(pts) == 0: + new_links[link] = t_links[link] + else: + sorted_nodes: list[tuple[float, str]] = [] + sorted_nodes.append((0.0, node1)) + sorted_nodes.append((1.0, node2)) + i = 0 + for pt in pts: + x, y = new_nodes[pt]['x'], new_nodes[pt]['y'] + percent = ((x - x1) / x_delta) if use_x else ((y - y1) / y_delta) + sorted_nodes.append((percent, pt)) + i += 1 + sorted_nodes = sorted(sorted_nodes, key=lambda s:s[0]) + + for i in range(1, len(sorted_nodes)): + l = sorted_nodes[i - 1][1] + r = sorted_nodes[i][1] + new_link = f'LINK_[{l}]_[{r}]' + new_links[new_link] = { 'node1': l, 'node2': r } + + return (new_nodes, new_links) + + +def calculate_boundary(name: str, nodes: list[str], accurate = False) -> list[tuple[float, float]]: + topology = Topology(name, nodes) + t_nodes = topology.nodes() + t_links = topology.links() + + vertices, path, boundary = _calculate_boundary(topology.max_x_node(), t_nodes, t_links) + + if not accurate: + return boundary + + api = 'calculate_boundary' + write(name, f"delete from temp_region where id = '{api}'") + # use linestring instead of polygon to reduce strict limitation + # TODO: linestring can not work well + write(name, f"insert into temp_region (id, boundary) values ('{api}', '{to_postgis_polygon(boundary)}')") + + write(name, f'delete from temp_node') + for node in nodes: + write(name, f"insert into temp_node values ('{node}')") + + for row in read_all(name, f"select n.node from coordinates as c, temp_node as n, temp_region as r where c.node = n.node and ST_Intersects(c.coord, r.boundary) and r.id = '{api}'"): + node = row['node'] + write(name, f"delete from temp_node where node = '{node}'") + + outside_nodes: list[str] = [] + for row in read_all(name, "select node from temp_node"): + outside_nodes.append(row['node']) + + # no outside nodes, return + if len(outside_nodes) == 0: + write(name, f'delete from temp_node') + write(name, f"delete from temp_region where id = '{api}'") + return boundary + + new_nodes: dict[str, Any] = {} + new_links: dict[str, Any] = {} + + boundary_links: dict[str, list[str]] = {} + write(name, "delete from temp_link_2") + for node in outside_nodes: + for link in t_nodes[node]['links']: + node1 = t_links[link]['node1'] + node2 = t_links[link]['node2'] + if node1 in outside_nodes and node2 not in outside_nodes and node2 not in vertices and link: + if link not in boundary: + boundary_links[link] = [] + line = f"LINESTRING({t_nodes[node1]['x']} {t_nodes[node1]['y']}, {t_nodes[node2]['x']} {t_nodes[node2]['y']})" + write(name, f"insert into temp_link_2 values ('{link}', '{line}')") + if node2 in outside_nodes and node1 not in outside_nodes and node1 not in vertices: + if link not in boundary: + boundary_links[link] = [] + line = f"LINESTRING({t_nodes[node1]['x']} {t_nodes[node1]['y']}, {t_nodes[node2]['x']} {t_nodes[node2]['y']})" + write(name, f"insert into temp_link_2 values ('{link}', '{line}')") + if node1 in outside_nodes and node2 in outside_nodes: + x1, x2 = t_nodes[node1]['x'], t_nodes[node2]['x'] + y1, y2 = t_nodes[node1]['y'], t_nodes[node2]['y'] + if node1 not in new_nodes: + new_nodes[node1] = { 'x': x1, 'y': y1, 'links': [] } + if node2 not in new_nodes: + new_nodes[node2] = { 'x': x2, 'y': y2, 'links': [] } + if link not in new_links: + new_links[link] = t_links[link] + + # no boundary links, return + if len(boundary_links) == 0: + write(name, "delete from temp_link_2") + write(name, f'delete from temp_node') + write(name, f"delete from temp_region where id = '{api}'") + return boundary + + write(name, "delete from temp_link_1") + for link, _ in path.items(): + node1 = t_links[link]['node1'] + node2 = t_links[link]['node2'] + line = f"LINESTRING({t_nodes[node1]['x']} {t_nodes[node1]['y']}, {t_nodes[node2]['x']} {t_nodes[node2]['y']})" + write(name, f"insert into temp_link_1 (link, geom) values ('{link}', '{line}')") + + has_intersection = False + for row in read_all(name, f"select l1.link as l, l2.link as r, st_astext(st_intersection(l1.geom, l2.geom)) as p from temp_link_1 as l1, temp_link_2 as l2 where st_intersects(l1.geom, l2.geom)"): + has_intersection = True + + link1, link2, pt = str(row['l']), str(row['r']), str(row['p']) + pts = pt.lower().removeprefix('point(').removesuffix(')').split(' ') + xy = (float(pts[0]), float(pts[1])) + + new_node = f'NODE_[{link1}]_[{link2}]' + new_nodes[new_node] = { 'x': xy[0], 'y': xy[1], 'links': [] } + + path[link1].append(new_node) + boundary_links[link2].append(new_node) + + # no intersection, return + if not has_intersection: + write(name, "delete from temp_link_1") + write(name, "delete from temp_link_2") + write(name, 'delete from temp_node') + write(name, f"delete from temp_region where id = '{api}'") + return boundary + + new_nodes, new_links = _collect_new_links(path, t_nodes, t_links, new_nodes, new_links) + new_nodes, new_links = _collect_new_links(boundary_links, t_nodes, t_links, new_nodes, new_links) + + for link, values in new_links.items(): + new_nodes[values['node1']]['links'].append(link) + new_nodes[values['node2']]['links'].append(link) + + _, _, boundary = _calculate_boundary(topology.max_x_node(), new_nodes, new_links) + + write(name, "delete from temp_link_1") + write(name, "delete from temp_link_2") + write(name, 'delete from temp_node') + write(name, f"delete from temp_region where id = '{api}'") + + return boundary + + +''' +# CClipper2.dll +# int inflate_paths(double* path, size_t size, double delta, int jt, int et, double miter_limit, int precision, double arc_tolerance, double** out_path, size_t* out_size); +# int simplify_paths(double* path, size_t size, double epsilon, int is_closed_path, double** out_path, size_t* out_size); +# void free_paths(double** paths); +''' +def inflate_boundary(name: str, boundary: list[tuple[float, float]], delta: float = 0.5) -> list[tuple[float, float]]: + if boundary[0] == boundary[-1]: + del(boundary[-1]) + + lib = ctypes.CDLL(os.path.join(os.getcwd(), 'api', 'CClipper2.dll')) + + c_size = ctypes.c_size_t(len(boundary) * 2) + c_path = (ctypes.c_double * c_size.value)() + i = 0 + for xy in boundary: + c_path[i] = xy[0] + i += 1 + c_path[i] = xy[1] + i += 1 + c_delta = ctypes.c_double(delta) + JoinType_Square, JoinType_Round, JoinType_Miter = 0, 1, 2 + c_jt = ctypes.c_int(JoinType_Square) + EndType_Polygon, EndType_Joined, EndType_Butt, EndType_Square, EndType_Round = 0, 1, 2, 3, 4 + c_et = ctypes.c_int(EndType_Polygon) + c_miter_limit = ctypes.c_double(2.0) + c_precision = ctypes.c_int(2) + c_arc_tolerance = ctypes.c_double(0.0) + c_out_path = ctypes.POINTER(ctypes.c_double)() + c_out_size = ctypes.c_size_t(0) + + lib.inflate_paths(c_path, c_size, c_delta, c_jt, c_et, c_miter_limit, c_precision, c_arc_tolerance, ctypes.byref(c_out_path), ctypes.byref(c_out_size)) + if c_out_size.value == 0: + lib.free_paths(ctypes.byref(c_out_path)) + return [] + + # TODO: simplify_paths :) + + result: list[tuple[float, float]] = [] + for i in range(0, c_out_size.value, 2): + result.append((c_out_path[i], c_out_path[i + 1])) + result.append(result[0]) + + lib.free_paths(ctypes.byref(c_out_path)) + return result + + +def inflate_region(name: str, region_id: str, delta: float = 0.5) -> list[tuple[float, float]]: + r = try_read(name, f"select id, st_astext(boundary) as boundary_geom from region where id = '{region_id}'") + if r == None: + return [] + boundary = from_postgis_polygon(str(r['boundary_geom'])) + return inflate_boundary(name, boundary, delta) + + +if __name__ == '__main__': + _verify_platform() diff --git a/app/native/api/s33_dma.py b/app/native/api/s33_dma.py new file mode 100644 index 0000000..49198c9 --- /dev/null +++ b/app/native/api/s33_dma.py @@ -0,0 +1,230 @@ +from .database import * +from .s0_base import is_node +from .s32_region_util import to_postgis_polygon +from .s32_region import get_region + +def get_district_metering_area_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'boundary' : {'type': 'tuple_list' , 'optional': False , 'readonly': False }, + 'parent' : {'type': 'str' , 'optional': True , 'readonly': False }, + 'level' : {'type': 'int' , 'optional': False , 'readonly': True } } + + +def get_district_metering_area(name: str, id: str) -> dict[str, Any]: + dma = get_region(name, id) + if dma == {}: + return {} + r = try_read(name, f"select * from region_dma where id = '{id}'") + if r == None: + return {} + dma['parent'] = r['parent'] + dma['nodes'] = list(eval(r['nodes'])) + dma['level'] = 1 + + if dma['parent'] != None: + parent = dma['parent'] + while parent != None: + parent = read(name, f"select parent from region_dma where id = '{parent}'")['parent'] + dma['level'] += 1 + + return dma + + +def _set_district_metering_area(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + + new_boundary = cs.operations[0]['boundary'] + old_boundary = get_region(name, id)['boundary'] + + new_parent = cs.operations[0]['parent'] + f_new_parent = f"'{new_parent}'" if new_parent != None else 'null' + + new_nodes = cs.operations[0]['nodes'] + str_new_nodes = str(new_nodes).replace("'", "''") + + old = get_district_metering_area(name, id) + old_parent = old['parent'] + f_old_parent = f"'{old_parent}'" if old_parent != None else 'null' + + old_nodes = old['nodes'] + str_old_nodes = str(old_nodes).replace("'", "''") + + redo_sql = f"update region set boundary = st_geomfromtext('{to_postgis_polygon(new_boundary)}') where id = '{id}';" + redo_sql += f"update region_dma set parent = {f_new_parent}, nodes = '{str_new_nodes}' where id = '{id}';" + + undo_sql = f"update region_dma set parent = {f_old_parent}, nodes = '{str_old_nodes}' where id = '{id}';" + undo_sql += f"update region set boundary = st_geomfromtext('{to_postgis_polygon(old_boundary)}') where id = '{id}';" + + redo_cs = g_update_prefix | { 'type': 'district_metering_area', 'id': id, 'boundary': new_boundary, 'parent': new_parent, 'nodes': new_nodes } + undo_cs = g_update_prefix | { 'type': 'district_metering_area', 'id': id, 'boundary': old_boundary, 'parent': old_parent, 'nodes': old_nodes } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_district_metering_area(name: str, cs: ChangeSet) -> ChangeSet: + ops = cs.operations + + if len(cs.operations) == 0: + return ChangeSet() + + op = ops[0] + + if 'id' not in op: + return ChangeSet() + + dma = get_district_metering_area(name, op['id']) + if dma == {}: + return ChangeSet() + + if 'boundary' not in op: + op['boundary'] = dma['boundary'] + else: + b = op['boundary'] + if len(b) < 4 or b[0] != b[-1]: + return ChangeSet() + + if 'parent' not in op: + op['parent'] = dma['parent'] + + if op['parent'] != None and get_district_metering_area(name, op['parent']) == {}: + return ChangeSet() + + if 'nodes' not in op: + op['nodes'] = dma['nodes'] + else: + for node in op['nodes']: + if not is_node(name, node): + return ChangeSet() + + return execute_command(name, _set_district_metering_area(name, cs)) + + +def _add_district_metering_area(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + + boundary = cs.operations[0]['boundary'] + + parent = cs.operations[0]['parent'] + f_parent = f"'{parent}'" if parent != None else 'null' + + nodes = cs.operations[0]['nodes'] + str_nodes = str(nodes).replace("'", "''") + + redo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'DMA');" + redo_sql += f"insert into region_dma (id, parent, nodes) values ('{id}', {f_parent}, '{str_nodes}');" + + undo_sql = f"delete from region_dma where id = '{id}';" + undo_sql += f"delete from region where id = '{id}';" + + redo_cs = g_add_prefix | { 'type': 'district_metering_area', 'id': id, 'boundary': boundary, 'parent': parent, 'nodes': nodes } + undo_cs = g_delete_prefix | { 'type': 'district_metering_area', 'id': id } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_district_metering_area(name: str, cs: ChangeSet) -> ChangeSet: + ops = cs.operations + + if len(cs.operations) == 0: + return ChangeSet() + + op = ops[0] + + if 'id' not in op: + return ChangeSet() + + dma = get_district_metering_area(name, op['id']) + if dma != {}: + return ChangeSet() + + if 'boundary' not in op: + return ChangeSet() + else: + b = op['boundary'] + if len(b) < 4 or b[0] != b[-1]: + return ChangeSet() + + if 'parent' not in op: + op['parent'] = None + + if op['parent'] != None and get_district_metering_area(name, op['parent']) == {}: + return ChangeSet() + + if 'nodes' not in op: + op['nodes'] = [] + else: + for node in op['nodes']: + if not is_node(name, node): + return ChangeSet() + + return execute_command(name, _add_district_metering_area(name, cs)) + + +def _delete_district_metering_area(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + dma = get_district_metering_area(name, id) + boundary = dma['boundary'] + parent = dma['parent'] + f_parent = f"'{parent}'" if parent != None else 'null' + nodes = dma['nodes'] + str_nodes = str(nodes).replace("'", "''") + + redo_sql = f"delete from region_dma where id = '{id}';" + redo_sql += f"delete from region where id = '{id}';" + + undo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'DMA');" + undo_sql += f"insert into region_dma (id, parent, nodes) values ('{id}', {f_parent}, '{str_nodes}');" + + redo_cs = g_delete_prefix | { 'type': 'district_metering_area', 'id': id } + undo_cs = g_add_prefix | { 'type': 'district_metering_area', 'id': id, 'boundary': boundary, 'parent': parent, 'nodes': nodes } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def _has_child(name: str, parent: str) -> bool: + return try_read(name, f"select * from region_dma where parent = '{parent}'") != None + + +def is_descendant_of(name: str, descendant: str, ancestor: str) -> bool: + parent = descendant + while parent != None: + parent = read(name, f"select parent from region_dma where id = '{parent}'")['parent'] + if parent == ancestor: + return True + return False + + +def delete_district_metering_area(name: str, cs: ChangeSet) -> ChangeSet: + ops = cs.operations + + if len(cs.operations) == 0: + return ChangeSet() + + op = ops[0] + + if 'id' not in op: + return ChangeSet() + + dma = get_district_metering_area(name, op['id']) + if dma == {}: + return ChangeSet() + + #TODO: cascade ? + if _has_child(name, dma['id']): + return ChangeSet() + + return execute_command(name, _delete_district_metering_area(name, cs)) + + +def get_all_district_metering_area_ids(name: str) -> list[str]: + ids = [] + for row in read_all(name, f"select id from region_dma"): + ids.append(row['id']) + return ids + + +def get_all_district_metering_areas(name: str) -> list[dict[str, Any]]: + result = [] + for id in get_all_district_metering_area_ids(name): + result.append(get_district_metering_area(name, id)) + return result diff --git a/app/native/api/s33_dma_cal.py b/app/native/api/s33_dma_cal.py new file mode 100644 index 0000000..e2d7ae9 --- /dev/null +++ b/app/native/api/s33_dma_cal.py @@ -0,0 +1,152 @@ +import ctypes +import os +import numpy as np +import pymetis +from .database import * +from .s0_base import get_nodes +from .s32_region_util import get_nodes_in_region +from .s32_region_util import Topology + + +PARTITION_TYPE_RB = 0 +PARTITION_TYPE_KWAY = 1 + +''' +adjacency_list = [np.array([4, 2, 1]), + np.array([0, 2, 3]), + np.array([4, 3, 1, 0]), + np.array([1, 2, 5, 6]), + np.array([0, 2, 5]), + np.array([4, 3, 6]), + np.array([5, 3])] +n_cuts, membership = pymetis.part_graph(2, adjacency=adjacency_list) +# n_cuts = 3 +# membership = [1, 1, 1, 0, 1, 0, 0] + +nodes_part_0 = np.argwhere(np.array(membership) == 0).ravel() # [3, 5, 6] +nodes_part_1 = np.argwhere(np.array(membership) == 1).ravel() # [0, 1, 2, 4] + +print(nodes_part_0) +print(nodes_part_1) +''' + + +def calculate_district_metering_area_for_nodes(name: str, nodes: list[str], part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]: + topology = Topology(name, nodes) + t_nodes = topology.nodes() + t_links = topology.links() + t_node_list = topology.node_list() + + adjacency_list = [] + + for node in t_node_list: + links: list[str] = t_nodes[node]['links'] + a_nodes: list[int] = [] + for link in links: + if t_links[link]['node1'] == node: + i = t_node_list.index(t_links[link]['node2']) + a_nodes.append(i) + elif t_links[link]['node2'] == node: + i = t_node_list.index(t_links[link]['node1']) + a_nodes.append(i) + adjacency_list.append(np.array(a_nodes)) + + recursive = part_type == PARTITION_TYPE_RB + n_cuts, membership = pymetis.part_graph(nparts=part_count, adjacency=adjacency_list, recursive=recursive, contiguous=True) + + result: list[list[str]] = [] + for i in range(0, part_count): + indices: list[int] = list(np.argwhere(np.array(membership) == i).ravel()) + index_strs: list[str] = [] + for index in indices: + index_strs.append(t_node_list[index]) + result.append(index_strs) + + return result + + +def _calculate_district_metering_area_for_nodes(name: str, nodes: list[str], part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]: + if part_type != PARTITION_TYPE_RB and part_type != PARTITION_TYPE_KWAY: + return [] + if part_count <= 0: + return [] + elif part_count == 1: + return [nodes] + + lib = ctypes.CDLL(os.path.join(os.getcwd(), 'api', 'CMetis.dll')) + + METIS_NOPTIONS = 40 + c_options = (ctypes.c_int64 * METIS_NOPTIONS)() + + METIS_OK = 1 + result = lib.set_default_options(c_options) + if result != METIS_OK: + return [] + + METIS_OPTION_PTYPE , METIS_OPTION_CONTIG = 0, 13 + c_options[METIS_OPTION_PTYPE] = part_type + c_options[METIS_OPTION_CONTIG] = 1 + + topology = Topology(name, nodes) + t_nodes = topology.nodes() + t_links = topology.links() + t_node_list = topology.node_list() + t_link_list = topology.link_list() + + nedges = len(t_link_list) * 2 + + c_nvtxs = ctypes.c_int64(len(t_node_list)) + c_ncon = ctypes.c_int64(1) + c_xadj = (ctypes.c_int64 * (c_nvtxs.value + 1))() + c_adjncy = (ctypes.c_int64 * nedges)() + c_vwgt = (ctypes.c_int64 * (c_ncon.value * c_nvtxs.value))() + c_adjwgt = (ctypes.c_int64 * nedges)() + c_vsize = (ctypes.c_int64 * c_nvtxs.value)() + + c_xadj[0] = 0 + + l, n = 0, 0 + c_xadj_i = 1 + for node in t_node_list: + links = t_nodes[node]['links'] + for link in links: + node1 = t_links[link]['node1'] + node2 = t_links[link]['node2'] + c_adjncy[l] = t_node_list.index(node2) if node2 != node else t_node_list.index(node1) + c_adjwgt[l] = 1 + l += 1 + if len(links) > 0: + c_xadj[c_xadj_i] = l # adjncy.size() + c_xadj_i += 1 + c_vwgt[n] = 1 + c_vsize[n] = 1 + n += 1 + + part_func = lib.part_graph_recursive if part_type == PARTITION_TYPE_RB else lib.part_graph_kway + + c_nparts = ctypes.c_int64(part_count) + c_tpwgts = ctypes.POINTER(ctypes.c_double)() + c_ubvec = ctypes.POINTER(ctypes.c_double)() + c_out_edgecut = ctypes.c_int64(0) + c_out_part = (ctypes.c_int64 * c_nvtxs.value)() + result = part_func(ctypes.byref(c_nvtxs), ctypes.byref(c_ncon), c_xadj, c_adjncy, c_vwgt, c_vsize, c_adjwgt, ctypes.byref(c_nparts), c_tpwgts, c_ubvec, c_options, ctypes.byref(c_out_edgecut), c_out_part) + if result != METIS_OK: + return [] + + dmas : list[list[str]]= [] + for i in range(part_count): + dmas.append([]) + for i in range(c_nvtxs.value): + dmas[c_out_part[i]].append(t_node_list[i]) + + return dmas + + +def calculate_district_metering_area_for_region(name: str, region: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]: + nodes = get_nodes_in_region(name, region) + return calculate_district_metering_area_for_nodes(name, nodes, part_count, part_type) + + +def calculate_district_metering_area_for_network(name: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]: + nodes = get_nodes(name) + return calculate_district_metering_area_for_nodes(name, nodes, part_count, part_type) diff --git a/app/native/api/s33_dma_gen.py b/app/native/api/s33_dma_gen.py new file mode 100644 index 0000000..4a88fdc --- /dev/null +++ b/app/native/api/s33_dma_gen.py @@ -0,0 +1,47 @@ +from .s32_region_util import calculate_boundary, inflate_boundary +from .s33_dma_cal import * +from .s33_dma import get_all_district_metering_area_ids, get_all_district_metering_areas, get_district_metering_area, is_descendant_of +from .batch_exe import execute_batch_command + + +def generate_district_metering_area(name: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB, inflate_delta: float = 0.5) -> ChangeSet: + cs = ChangeSet() + + dmas = get_all_district_metering_areas(name) + max_level = 0 + for dma in dmas: + if dma['level'] > max_level: + max_level = dma['level'] + while max_level > 0: + for dma in dmas: + if dma['level'] == max_level: + cs.delete({ 'type': 'district_metering_area', 'id': dma['id'] }) + max_level -= 1 + + i = 1 + for nodes in calculate_district_metering_area_for_network(name, part_count, part_type): + boundary = calculate_boundary(name, nodes) + boundary = inflate_boundary(name, boundary, inflate_delta) + cs.add({ 'type': 'district_metering_area', 'id': f"DMA_1_{i}", 'boundary': boundary, 'parent': None, 'nodes': nodes }) + i += 1 + + return execute_batch_command(name, cs) + + +def generate_sub_district_metering_area(name: str, dma: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB, inflate_delta: float = 0.5) -> ChangeSet: + cs = ChangeSet() + + for id in get_all_district_metering_area_ids(name): + if is_descendant_of(name, id, dma): + cs.delete({ 'type': 'district_metering_area', 'id': id }) + + level = get_district_metering_area(name, dma)['level'] + 1 + + i = 1 + for nodes in calculate_district_metering_area_for_region(name, dma, part_count, part_type): + boundary = calculate_boundary(name, nodes) + boundary = inflate_boundary(name, boundary, inflate_delta) + cs.add({ 'type': 'district_metering_area', 'id': f"DMA_[{dma}]_{level}_{i}", 'boundary': boundary, 'parent': dma, 'nodes': nodes }) + i += 1 + + return execute_batch_command(name, cs) diff --git a/app/native/api/s34_sa.py b/app/native/api/s34_sa.py new file mode 100644 index 0000000..ec54c2c --- /dev/null +++ b/app/native/api/s34_sa.py @@ -0,0 +1,217 @@ +from .database import * +from .s0_base import is_node +from .s32_region_util import to_postgis_polygon +from .s32_region import get_region + +def get_service_area_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'boundary' : {'type': 'tuple_list' , 'optional': False , 'readonly': False }, + 'source' : {'type': 'str' , 'optional': False , 'readonly': False }, + 'time_index' : {'type': 'int' , 'optional': False , 'readonly': False } } + +def get_service_area(name: str, id: str) -> dict[str, Any]: + sa = get_region(name, id) + if sa == {}: + return {} + r = try_read(name, f"select * from region_sa where id = '{id}'") + if r == None: + return {} + sa['source'] = r['source'] + sa['nodes'] = list(eval(r['nodes'])) + sa['time_index'] = r['time_index'] + return sa + +def _set_service_area(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + + new_boundary = cs.operations[0]['boundary'] + old_boundary = get_region(name, id)['boundary'] + + new_source = cs.operations[0]['source'] + f_new_source = f"'{new_source}'" + + new_nodes = cs.operations[0]['nodes'] + str_new_nodes = str(new_nodes).replace("'", "''") + + new_time_index = cs.operations[0]['time_index'] + + old = get_service_area(name, id) + old_source = old['source'] + f_old_source = f"'{old_source}'" + + old_nodes = old['nodes'] + str_old_nodes = str(old_nodes).replace("'", "''") + + old_time_index = old['time_index'] + + redo_sql = f"update region set boundary = st_geomfromtext('{to_postgis_polygon(new_boundary)}') where id = '{id}';" + redo_sql += f"update region_sa set time_index = {new_time_index}, source = {f_new_source}, nodes = '{str_new_nodes}' where id = '{id}';" + + undo_sql = f"update region_sa set time_index = {old_time_index}, source = {f_old_source}, nodes = '{str_old_nodes}' where id = '{id}';" + undo_sql += f"update region set boundary = st_geomfromtext('{to_postgis_polygon(old_boundary)}') where id = '{id}';" + + redo_cs = g_update_prefix | { 'type': 'service_area', 'id': id, 'boundary': new_boundary, 'time_index': new_time_index, 'source': new_source, 'nodes': new_nodes } + undo_cs = g_update_prefix | { 'type': 'service_area', 'id': id, 'boundary': old_boundary, 'time_index': old_time_index, 'source': old_source, 'nodes': old_nodes } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_service_area(name: str, cs: ChangeSet) -> ChangeSet: + ops = cs.operations + + if len(cs.operations) == 0: + return ChangeSet() + + op = ops[0] + + if 'id' not in op: + return ChangeSet() + + sa = get_service_area(name, op['id']) + if sa == {}: + return ChangeSet() + + if 'boundary' not in op: + op['boundary'] = sa['boundary'] + else: + b = op['boundary'] + if len(b) < 4 or b[0] != b[-1]: + return ChangeSet() + + if 'time_index' not in op: + op['time_index'] = sa['time_index'] + + if 'source' not in op: + op['source'] = sa['source'] + + if not is_node(name, op['source']): + return ChangeSet() + + if 'nodes' not in op: + op['nodes'] = sa['nodes'] + else: + for node in op['nodes']: + if not is_node(name, node): + return ChangeSet() + + return execute_command(name, _set_service_area(name, cs)) + + +def _add_service_area(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + + boundary = cs.operations[0]['boundary'] + + time_index = cs.operations[0]['time_index'] + + source = cs.operations[0]['source'] + f_source = f"'{source}'" + + nodes = cs.operations[0]['nodes'] + str_nodes = str(nodes).replace("'", "''") + + redo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'SA');" + redo_sql += f"insert into region_sa (id, time_index, source, nodes) values ('{id}', {time_index}, {f_source}, '{str_nodes}');" + + undo_sql = f"delete from region_sa where id = '{id}';" + undo_sql += f"delete from region where id = '{id}';" + + redo_cs = g_add_prefix | { 'type': 'service_area', 'id': id, 'boundary': boundary, 'time_index': time_index, 'source': source, 'nodes': nodes } + undo_cs = g_delete_prefix | { 'type': 'service_area', 'id': id } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_service_area(name: str, cs: ChangeSet) -> ChangeSet: + ops = cs.operations + + if len(cs.operations) == 0: + return ChangeSet() + + op = ops[0] + + if 'id' not in op: + return ChangeSet() + + sa = get_service_area(name, op['id']) + if sa != {}: + return ChangeSet() + + if 'boundary' not in op: + return ChangeSet() + else: + b = op['boundary'] + if len(b) < 4 or b[0] != b[-1]: + return ChangeSet() + + if 'time_index' not in op: + return ChangeSet() + + if 'source' not in op: + return ChangeSet() + + if not is_node(name, op['source']): + return ChangeSet() + + if 'nodes' not in op: + op['nodes'] = [] + else: + for node in op['nodes']: + if not is_node(name, node): + return ChangeSet() + + return execute_command(name, _add_service_area(name, cs)) + + +def _delete_service_area(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + sa = get_service_area(name, id) + boundary = sa['boundary'] + time_index = sa['time_index'] + source = sa['source'] + f_source = f"'{source}'" + nodes = sa['nodes'] + str_nodes = str(nodes).replace("'", "''") + + redo_sql = f"delete from region_sa where id = '{id}';" + redo_sql += f"delete from region where id = '{id}';" + + undo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'SA');" + undo_sql += f"insert into region_sa (id, time_index, source, nodes) values ('{id}', {time_index}, {f_source}, '{str_nodes}');" + + redo_cs = g_delete_prefix | { 'type': 'service_area', 'id': id } + undo_cs = g_add_prefix | { 'type': 'service_area', 'id': id, 'boundary': boundary, 'time_index': time_index, 'source': source, 'nodes': nodes } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_service_area(name: str, cs: ChangeSet) -> ChangeSet: + ops = cs.operations + + if len(cs.operations) == 0: + return ChangeSet() + + op = ops[0] + + if 'id' not in op: + return ChangeSet() + + sa = get_service_area(name, op['id']) + if sa == {}: + return ChangeSet() + + return execute_command(name, _delete_service_area(name, cs)) + + +def get_all_service_area_ids(name: str) -> list[str]: + ids = [] + for row in read_all(name, f"select id from region_sa"): + ids.append(row['id']) + return ids + + +def get_all_service_areas(name: str) -> list[dict[str, Any]]: + result = [] + for id in get_all_service_area_ids(name): + result.append(get_service_area(name, id)) + return result diff --git a/app/native/api/s34_sa_cal.py b/app/native/api/s34_sa_cal.py new file mode 100644 index 0000000..c72a2a5 --- /dev/null +++ b/app/native/api/s34_sa_cal.py @@ -0,0 +1,198 @@ +import os +import ctypes +from .project_backup import have_project +from .inp_out import dump_inp + +def calculate_service_area(name: str) -> list[dict[str, list[str]]]: + if not have_project(name): + raise Exception(f'Not found project [{name}]') + + dir = os.path.abspath(os.getcwd()) + + inp_str = os.path.join(os.path.join(dir, 'db_inp'), name + '.db.inp') + dump_inp(name, inp_str, '2') + + toolkit = ctypes.CDLL(os.path.join(os.path.join(dir, 'api'), 'toolkit.dll')) + + inp = ctypes.c_char_p(inp_str.encode()) + + handle = ctypes.c_ulonglong() + toolkit.TK_ServiceArea_Start(inp, ctypes.byref(handle)) + + c_nodeCount = ctypes.c_size_t() + toolkit.TK_ServiceArea_GetNodeCount(handle, ctypes.byref(c_nodeCount)) + nodeCount = c_nodeCount.value + + nodeIds: list[str] = [] + + for n in range(0, nodeCount): + id = ctypes.c_char_p() + toolkit.TK_ServiceArea_GetNodeId(handle, ctypes.c_size_t(n), ctypes.byref(id)) + nodeIds.append(id.value.decode()) + + c_timeCount = ctypes.c_size_t() + toolkit.TK_ServiceArea_GetTimeCount(handle, ctypes.byref(c_timeCount)) + timeCount = c_timeCount.value + + results: list[dict[str, list[str]]] = [] + + for t in range(0, timeCount): + c_sourceCount = ctypes.c_size_t() + toolkit.TK_ServiceArea_GetSourceCount(handle, ctypes.c_size_t(t), ctypes.byref(c_sourceCount)) + sourceCount = c_sourceCount.value + + sources = ctypes.POINTER(ctypes.c_size_t)() + toolkit.TK_ServiceArea_GetSources(handle, ctypes.c_size_t(t), ctypes.byref(sources)) + + result: dict[str, list[str]] = {} + for s in range(0, sourceCount): + result[nodeIds[sources[s]]] = [] + + for n in range(0, nodeCount): + concentration = ctypes.POINTER(ctypes.c_double)() + toolkit.TK_ServiceArea_GetConcentration(handle, ctypes.c_size_t(t), ctypes.c_size_t(n), ctypes.byref(concentration)) + + maxS = sources[0] + maxC = concentration[0] + for s in range(1, sourceCount): + if concentration[s] > maxC: + maxS = sources[s] + maxC = concentration[s] + + result[nodeIds[maxS]].append(nodeIds[n]) + + results.append(result) + + toolkit.TK_ServiceArea_End(handle) + + return results + +''' +import sys +import json +from queue import Queue +from .database import * +from .s0_base import get_node_links, get_link_nodes + +sys.path.append('..') +from epanet.epanet import run_project + +def _calculate_service_area(name: str, inp, time_index: int = 0) -> dict[str, list[str]]: + sources : dict[str, list[str]] = {} + for node_result in inp['node_results']: + result = node_result['result'][time_index] + if result['demand'] < 0: + sources[node_result['node']] = [] + + link_flows: dict[str, float] = {} + for link_result in inp['link_results']: + result = link_result['result'][time_index] + link_flows[link_result['link']] = float(result['flow']) + + # build source to nodes map + for source in sources: + queue = Queue() + queue.put(source) + + while not queue.empty(): + cursor = queue.get() + if cursor not in sources[source]: + sources[source].append(cursor) + + links = get_node_links(name, cursor) + for link in links: + node1, node2 = get_link_nodes(name, link) + if node1 == cursor and link_flows[link] > 0: + queue.put(node2) + elif node2 == cursor and link_flows[link] < 0: + queue.put(node1) + + #return sources + + # calculation concentration + concentration_map: dict[str, dict[str, float]] = {} + node_wip: list[str] = [] + for source, nodes in sources.items(): + for node in nodes: + if node not in concentration_map: + concentration_map[node] = {} + concentration_map[node][source] = 0.0 + if node not in node_wip: + node_wip.append(node) + + # if only one source, done + for node, concentrations in concentration_map.items(): + if len(concentrations) == 1: + node_wip.remove(node) + for key in concentrations.keys(): + concentration_map[node][key] = 1.0 + + node_upstream : dict[str, list[tuple[str, str]]] = {} + for node in node_wip: + if node not in node_upstream: + node_upstream[node] = [] + + links = get_node_links(name, node) + for link in links: + node1, node2 = get_link_nodes(name, link) + if node2 == node and link_flows[link] > 0: + node_upstream[node].append((link, node1)) + elif node1 == node and link_flows[link] < 0: + node_upstream[node].append((link, node2)) + + while len(node_wip) != 0: + done = [] + for node in node_wip: + up_link_nodes = node_upstream[node] + ready = True + for link_node in up_link_nodes: + if link_node[1] in node_wip: + ready = False + break + if ready: + for link_node in up_link_nodes: + if link_node[1] not in concentration_map.keys(): + continue + for source, concentration in concentration_map[link_node[1]].items(): + concentration_map[node][source] += concentration * abs(link_flows[link_node[0]]) + + # normalize + sum = 0.0 + for source, concentration in concentration_map[node].items(): + sum += concentration + for source in concentration_map[node].keys(): + concentration_map[node][source] /= sum + + done.append(node) + + for node in done: + node_wip.remove(node) + + source_to_main_node: dict[str, list[str]] = {} + for node, value in concentration_map.items(): + max_source = '' + max_concentration = 0.0 + for s, c in value.items(): + if c > max_concentration: + max_concentration = c + max_source = s + if max_source not in source_to_main_node: + source_to_main_node[max_source] = [] + source_to_main_node[max_source].append(node) + + return source_to_main_node + + +def calculate_service_area(name: str) -> list[dict[str, list[str]]]: + inp = json.loads(run_project(name, True)) + + result: list[dict[str, list[str]]] = [] + + time_count = len(inp['node_results'][0]['result']) + + for i in range(time_count): + sas = _calculate_service_area(name, inp, i) + result.append(sas) + + return result +''' diff --git a/app/native/api/s34_sa_gen.py b/app/native/api/s34_sa_gen.py new file mode 100644 index 0000000..7eb7ff8 --- /dev/null +++ b/app/native/api/s34_sa_gen.py @@ -0,0 +1,23 @@ +from .s32_region_util import calculate_boundary, inflate_boundary +from .s34_sa_cal import * +from .s34_sa import get_all_service_area_ids +from .batch_exe import execute_batch_command +from .database import ChangeSet + +def generate_service_area(name: str, inflate_delta: float = 0.5) -> ChangeSet: + cs = ChangeSet() + + for id in get_all_service_area_ids(name): + cs.delete({'type': 'service_area', 'id': id}) + + sass = calculate_service_area(name) + + time_index = 0 + for sas in sass: + for source, nodes in sas.items(): + boundary = calculate_boundary(name, nodes) + boundary = inflate_boundary(name, boundary, inflate_delta) + cs.add({ 'type': 'service_area', 'id': f"SA_{source}_{time_index}", 'boundary': boundary, 'time_index': time_index, 'source': source, 'nodes': nodes }) + time_index += 1 + + return execute_batch_command(name, cs) diff --git a/app/native/api/s35_vd.py b/app/native/api/s35_vd.py new file mode 100644 index 0000000..feddd1d --- /dev/null +++ b/app/native/api/s35_vd.py @@ -0,0 +1,202 @@ +from .database import * +from .s0_base import is_node +from .s32_region_util import to_postgis_polygon +from .s32_region import get_region + +def get_virtual_district_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'boundary' : {'type': 'tuple_list' , 'optional': False , 'readonly': False }, + 'center' : {'type': 'str' , 'optional': False , 'readonly': False } } + +def get_virtual_district(name: str, id: str) -> dict[str, Any]: + vd = get_region(name, id) + if vd == {}: + return {} + r = try_read(name, f"select * from region_vd where id = '{id}'") + if r == None: + return {} + vd['center'] = r['center'] + vd['nodes'] = list(eval(r['nodes'])) + return vd + +def _set_virtual_district(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + + new_boundary = cs.operations[0]['boundary'] + old_boundary = get_region(name, id)['boundary'] + + new_center = cs.operations[0]['center'] + f_new_center = f"'{new_center}'" + + new_nodes = cs.operations[0]['nodes'] + str_new_nodes = str(new_nodes).replace("'", "''") + + old = get_virtual_district(name, id) + old_center = old['center'] + f_old_center = f"'{old_center}'" + + old_nodes = old['nodes'] + str_old_nodes = str(old_nodes).replace("'", "''") + + redo_sql = f"update region set boundary = st_geomfromtext('{to_postgis_polygon(new_boundary)}') where id = '{id}';" + redo_sql += f"update region_vd set center = {f_new_center}, nodes = '{str_new_nodes}' where id = '{id}';" + + undo_sql = f"update region_vd set center = {f_old_center}, nodes = '{str_old_nodes}' where id = '{id}';" + undo_sql += f"update region set boundary = st_geomfromtext('{to_postgis_polygon(old_boundary)}') where id = '{id}';" + + redo_cs = g_update_prefix | { 'type': 'virtual_district', 'id': id, 'boundary': new_boundary, 'center': new_center, 'nodes': new_nodes } + undo_cs = g_update_prefix | { 'type': 'virtual_district', 'id': id, 'boundary': old_boundary, 'center': old_center, 'nodes': old_nodes } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_virtual_district(name: str, cs: ChangeSet) -> ChangeSet: + ops = cs.operations + + if len(cs.operations) == 0: + return ChangeSet() + + op = ops[0] + + if 'id' not in op: + return ChangeSet() + + vd = get_virtual_district(name, op['id']) + if vd == {}: + return ChangeSet() + + if 'boundary' not in op: + op['boundary'] = vd['boundary'] + else: + b = op['boundary'] + if len(b) < 4 or b[0] != b[-1]: + return ChangeSet() + + if 'center' not in op: + op['center'] = vd['center'] + + if not is_node(name, op['center']): + return ChangeSet() + + if 'nodes' not in op: + op['nodes'] = vd['nodes'] + else: + for node in op['nodes']: + if not is_node(name, node): + return ChangeSet() + + return execute_command(name, _set_virtual_district(name, cs)) + + +def _add_virtual_district(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + + boundary = cs.operations[0]['boundary'] + + center = cs.operations[0]['center'] + f_center = f"'{center}'" + + nodes = cs.operations[0]['nodes'] + str_nodes = str(nodes).replace("'", "''") + + redo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'VD');" + redo_sql += f"insert into region_vd (id, center, nodes) values ('{id}', {f_center}, '{str_nodes}');" + + undo_sql = f"delete from region_vd where id = '{id}';" + undo_sql += f"delete from region where id = '{id}';" + + redo_cs = g_add_prefix | { 'type': 'virtual_district', 'id': id, 'boundary': boundary, 'center': center, 'nodes': nodes } + undo_cs = g_delete_prefix | { 'type': 'virtual_district', 'id': id } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_virtual_district(name: str, cs: ChangeSet) -> ChangeSet: + ops = cs.operations + + if len(cs.operations) == 0: + return ChangeSet() + + op = ops[0] + + if 'id' not in op: + return ChangeSet() + + vd = get_virtual_district(name, op['id']) + if vd != {}: + return ChangeSet() + + if 'boundary' not in op: + return ChangeSet() + else: + b = op['boundary'] + if len(b) < 4 or b[0] != b[-1]: + return ChangeSet() + + if 'center' not in op: + return ChangeSet() + + if not is_node(name, op['center']): + return ChangeSet() + + if 'nodes' not in op: + op['nodes'] = [] + else: + for node in op['nodes']: + if not is_node(name, node): + return ChangeSet() + + return execute_command(name, _add_virtual_district(name, cs)) + + +def _delete_virtual_district(name: str, cs: ChangeSet) -> DbChangeSet: + id = cs.operations[0]['id'] + vd = get_virtual_district(name, id) + boundary = vd['boundary'] + center = vd['center'] + f_center = f"'{center}'" + nodes = vd['nodes'] + str_nodes = str(nodes).replace("'", "''") + + redo_sql = f"delete from region_vd where id = '{id}';" + redo_sql += f"delete from region where id = '{id}';" + + undo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'VD');" + undo_sql += f"insert into region_vd (id, center, nodes) values ('{id}', {f_center}, '{str_nodes}');" + + redo_cs = g_delete_prefix | { 'type': 'virtual_district', 'id': id } + undo_cs = g_add_prefix | { 'type': 'virtual_district', 'id': id, 'boundary': boundary, 'center': center, 'nodes': nodes } + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_virtual_district(name: str, cs: ChangeSet) -> ChangeSet: + ops = cs.operations + + if len(cs.operations) == 0: + return ChangeSet() + + op = ops[0] + + if 'id' not in op: + return ChangeSet() + + vd = get_virtual_district(name, op['id']) + if vd == {}: + return ChangeSet() + + return execute_command(name, _delete_virtual_district(name, cs)) + + +def get_all_virtual_district_ids(name: str) -> list[str]: + ids = [] + for row in read_all(name, f"select id from region_vd"): + ids.append(row['id']) + return ids + + +def get_all_virtual_districts(name: str) -> list[dict[str, Any]]: + result = [] + for id in get_all_virtual_district_ids(name): + result.append(get_virtual_district(name, id)) + return result diff --git a/app/native/api/s35_vd_cal.py b/app/native/api/s35_vd_cal.py new file mode 100644 index 0000000..b115814 --- /dev/null +++ b/app/native/api/s35_vd_cal.py @@ -0,0 +1,66 @@ +from .database import * +from .s0_base import get_node_links + + +def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, list[Any]]: + write(name, 'delete from temp_vd_topology') + + # map node name to index + i = 0 + isolated_nodes = [] + node_index: dict[str, int] = {} + for row in read_all(name, 'select id from _node'): + node = str(row['id']) + if get_node_links(name, node) == []: + isolated_nodes.append(node) + continue + i += 1 + node_index[node] = i + + # build topology graph + pipes = read_all(name, 'select node1, node2, length from pipes') + for pipe in pipes: + source = node_index[str(pipe['node1'])] + target = node_index[str(pipe['node2'])] + cost = float(pipe['length']) + write(name, f"insert into temp_vd_topology (source, target, cost) values ({source}, {target}, {cost})") + pumps = read_all(name, 'select node1, node2 from pumps') + for pump in pumps: + source = node_index[str(pump['node1'])] + target = node_index[str(pump['node2'])] + write(name, f"insert into temp_vd_topology (source, target, cost) values ({source}, {target}, 0.0)") + valves = read_all(name, 'select node1, node2 from valves') + for valve in valves: + source = node_index[str(valve['node1'])] + target = node_index[str(valve['node2'])] + write(name, f"insert into temp_vd_topology (source, target, cost) values ({source}, {target}, 0.0)") + + # dijkstra distance + node_distance: dict[str, dict[str, Any]] = {} + for center in centers: + for node, index in node_index.items(): + if node == center: + node_distance[node] = { 'center': center, 'distance' : 0.0 } + continue + # TODO: check none + distance = float(read(name, f"select max(agg_cost) as distance from pgr_dijkstraCost('select id, source, target, cost from temp_vd_topology', {index}, {node_index[center]}, false)")['distance']) + if node not in node_distance: + node_distance[node] = { 'center': center, 'distance' : distance } + elif distance < node_distance[node]['distance']: + node_distance[node] = { 'center': center, 'distance' : distance } + + write(name, 'delete from temp_vd_topology') + + # reorganize the distance result + center_node: dict[str, list[str]] = {} + for node, value in node_distance.items(): + if value['center'] not in center_node: + center_node[value['center']] = [] + center_node[value['center']].append(node) + + vds: list[dict[str, Any]] = [] + + for center, value in center_node.items(): + vds.append({ 'center': center, 'nodes': value }) + + return { 'virtual_districts': vds, 'isolated_nodes': isolated_nodes } diff --git a/app/native/api/s35_vd_gen.py b/app/native/api/s35_vd_gen.py new file mode 100644 index 0000000..6a3bc72 --- /dev/null +++ b/app/native/api/s35_vd_gen.py @@ -0,0 +1,21 @@ +from .s32_region_util import calculate_boundary, inflate_boundary +from .s35_vd_cal import * +from .s35_vd import get_all_virtual_district_ids +from .batch_exe import execute_batch_command + +def generate_virtual_district(name: str, centers: list[str], inflate_delta: float = 0.5) -> ChangeSet: + cs = ChangeSet() + + for id in get_all_virtual_district_ids(name): + cs.delete({'type': 'virtual_district', 'id': id}) + + vds = calculate_virtual_district(name, centers)['virtual_districts'] + + for vd in vds: + center = vd['center'] + nodes = vd['nodes'] + boundary = calculate_boundary(name, nodes) + boundary = inflate_boundary(name, boundary, inflate_delta) + cs.add({ 'type': 'virtual_district', 'id': f"VD_{center}", 'boundary': boundary, 'center': center, 'nodes': nodes }) + + return execute_batch_command(name, cs) diff --git a/app/native/api/s36_wda.py b/app/native/api/s36_wda.py new file mode 100644 index 0000000..e69de29 diff --git a/app/native/api/s36_wda_cal.py b/app/native/api/s36_wda_cal.py new file mode 100644 index 0000000..4583993 --- /dev/null +++ b/app/native/api/s36_wda_cal.py @@ -0,0 +1,104 @@ +from .database import ChangeSet +from .s0_base import is_junction, get_nodes +from .s9_demands import get_demand +from .s32_region_util import Topology, get_nodes_in_region +from .batch_exe import execute_batch_command + + +DISTRIBUTION_TYPE_ADD = 'ADD' +DISTRIBUTION_TYPE_OVERRIDE = 'OVERRIDE' + + +def calculate_demand_to_nodes(name: str, demand: float, nodes: list[str]) -> dict[str, float]: + if len(nodes) == 0 or demand == 0.0: + return {} + + topology = Topology(name, nodes) + t_nodes = topology.nodes() + t_links = topology.links() + + length_sum = 0.0 + for value in t_links.values(): + length_sum += abs(value['length']) + + if length_sum <= 0.0: + return {} + + demand_per_length = demand / length_sum + + result: dict[str, float] = {} + for node, value in t_nodes.items(): + if not is_junction(name, node): + continue + demand_per_node = 0.0 + for link in value['links']: + demand_per_node += abs(t_links[link]['length']) * demand_per_length * 0.5 + result[node] = demand_per_node + + return result + + +def calculate_demand_to_region(name: str, demand: float, region: str) -> dict[str, float]: + nodes = get_nodes_in_region(name, region) + return calculate_demand_to_nodes(name, demand, nodes) + + +def calculate_demand_to_network(name: str, demand: float) -> dict[str, float]: + nodes = get_nodes(name) + return calculate_demand_to_nodes(name, demand, nodes) + + +def distribute_demand_to_nodes(name: str, demand: float, nodes: list[str], type: str = DISTRIBUTION_TYPE_ADD) -> ChangeSet: + if len(nodes) == 0 or demand == 0.0: + return ChangeSet() + if type != DISTRIBUTION_TYPE_ADD and type != DISTRIBUTION_TYPE_OVERRIDE: + return ChangeSet() + + topology = Topology(name, nodes) + t_nodes = topology.nodes() + t_links = topology.links() + + length_sum = 0.0 + for value in t_links.values(): + length_sum += abs(value['length']) + + if length_sum <= 0.0: + return ChangeSet() + + demand_per_length = demand / length_sum + + cs = ChangeSet() + + for node, value in t_nodes.items(): + if not is_junction(name, node): + continue + demand_per_node = 0.0 + for link in value['links']: + demand_per_node += abs(t_links[link]['length']) * demand_per_length * 0.5 + + ds = get_demand(name, node)['demands'] + if len(ds) == 0: + ds = [{'demand': demand_per_node, 'pattern': None, 'category': None}] + elif type == DISTRIBUTION_TYPE_ADD: + ds[0]['demand'] += demand_per_node + else: + ds[0]['demand'] = demand_per_node + cs.update({'type': 'demand', 'junction': node, 'demands': ds}) + + return execute_batch_command(name, cs) + + +def distribute_demand_to_region(name: str, demand: float, region: str, type: str = DISTRIBUTION_TYPE_ADD) -> ChangeSet: + nodes = get_nodes_in_region(name, region) + return distribute_demand_to_nodes(name, demand, nodes, type) + +def get_total_base_demand(name:str,region:str)->float: + nodes = get_nodes_in_region(name, region) + t_demands=0.0 + for node in nodes: + if not is_junction(name, node): + continue + ds = get_demand(name, node)['demands'] + t_demands= t_demands+ds[0]['demand'] + + return t_demands \ No newline at end of file diff --git a/app/native/api/s38_scada_info.py b/app/native/api/s38_scada_info.py new file mode 100644 index 0000000..3e932f9 --- /dev/null +++ b/app/native/api/s38_scada_info.py @@ -0,0 +1,42 @@ +from .database import * + + +def get_scada_info_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'type' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'x' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'y' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'query_api_id' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'associated_element_id' : {'type': 'str' , 'optional': False , 'readonly': True } } + + +def get_scada_info(name: str, id: str) -> dict[str, Any]: + si = try_read(name, f"select * from scada_info where id = '{id}'") + if si is None: + return {} + + d = {} + d['id'] = si['id'] + d['type'] = si['type'] + d['x'] = float(si['x_coor']) + d['y'] = float(si['y_coor']) + d['api_query_id'] = si['api_query_id'] + d['associated_element_id'] = si['associated_element_id'] + + return d + +def get_all_scada_info(name: str) -> list[dict[str, Any]]: + sis = read_all(name, f"select * from scada_info") + if sis is None: + return [] + + d = [] + for si in sis: + d.append({ 'id': si['id'], + 'type': si['type'], + 'x': float(si['x_coor']), + 'y': float(si['y_coor']), + 'api_query_id': si['api_query_id'], + 'associated_element_id': si['associated_element_id'] }) + + return d \ No newline at end of file diff --git a/app/native/api/s39_user.py b/app/native/api/s39_user.py new file mode 100644 index 0000000..3b4c37d --- /dev/null +++ b/app/native/api/s39_user.py @@ -0,0 +1,37 @@ +from .database import * +from .s0_base import * + +class User(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'user' + self.id = str(input['user_id']) + self.name = str(input['username']) + self.password = str(input['password']) + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id, 'name': self.name, 'password': self.password } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id } + + +def get_user_schema(name: str) -> dict[str, dict[Any, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'name' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'password' : {'type': 'str' , 'optional': False , 'readonly': False} } + +def get_user(name: str, user_name: str) -> dict[Any, Any]: + t = try_read(name, f"select * from users where username = '{user_name}'") + if t == None: + return {} + + d = {} + d['id'] = str(t['user_id']) + d['name'] = str(t['username']) + # d['password'] = str(t['password']) + + return d + +def get_all_users(name: str) -> list[dict[Any, Any]]: + return read_all(name, "select * from users") + diff --git a/app/native/api/s3_reservoirs.py b/app/native/api/s3_reservoirs.py new file mode 100644 index 0000000..181b707 --- /dev/null +++ b/app/native/api/s3_reservoirs.py @@ -0,0 +1,193 @@ +from .database import * +from .s0_base import * +from .s24_coordinates import * + + +def get_reservoir_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'x' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'y' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'head' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'pattern' : {'type': 'str' , 'optional': True , 'readonly': False}, + 'links' : {'type': 'str_list' , 'optional': False , 'readonly': True } } + + +def get_reservoir(name: str, id: str) -> dict[str, Any]: + r = try_read(name, f"select * from reservoirs where id = '{id}'") + if r == None: + return {} + xy = get_node_coord(name, id) + d = {} + d['id'] = str(r['id']) + d['x'] = float(xy['x']) + d['y'] = float(xy['y']) + d['head'] = float(r['head']) + d['pattern'] = str(r['pattern']) if r['pattern'] != None else None + d['links'] = get_node_links(name, id) + return d + +# DingZQ, 2025-03-29 +def get_all_reservoirs(name: str) -> list[dict[str, Any]]: + rows = read_all(name, f"select * from reservoirs") + if rows == None: + return [] + + result = [] + for row in rows: + d = {} + id = str(row['id']) + xy = get_node_coord(name, id) + d['id'] = id + d['x'] = float(xy['x']) + d['y'] = float(xy['y']) + d['head'] = float(row['head']) if row['head'] != None else None + d['pattern'] = str(row['pattern']) if row['pattern'] != None else None + d['links'] = get_node_links(name, id) + result.append(d) + + return result + +class Reservoir(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'reservoir' + self.id = str(input['id']) + self.x = float(input['x']) + self.y = float(input['y']) + self.head = float(input['head']) + self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None + + self.f_type = f"'{self.type}'" + self.f_id = f"'{self.id}'" + self.f_head = self.head + self.f_pattern = f"'{self.pattern}'" if self.pattern != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'head': self.head, 'pattern': self.pattern } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id } + + +def _set_reservoir(name: str, cs: ChangeSet) -> DbChangeSet: + old = Reservoir(get_reservoir(name, cs.operations[0]['id'])) + raw_new = get_reservoir(name, cs.operations[0]['id']) + + new_dict = cs.operations[0] + schema = get_reservoir_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Reservoir(raw_new) + + redo_sql = f"update reservoirs set head = {new.f_head}, pattern = {new.f_pattern} where id = {new.f_id};" + redo_sql += f"\n{sql_update_coord(new.id, new.x, new.y)}" + + undo_sql = sql_update_coord(old.id, old.x, old.y) + undo_sql += f"\nupdate reservoirs set head = {old.f_head}, pattern = {old.f_pattern} where id = {old.f_id};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_reservoir(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_reservoir(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _set_reservoir(name, cs)) + + +def _add_reservoir(name: str, cs: ChangeSet) -> DbChangeSet: + new = Reservoir(cs.operations[0]) + + redo_sql = f"insert into _node (id, type) values ({new.f_id}, {new.f_type});" + redo_sql += f"\ninsert into reservoirs (id, head, pattern) values ({new.f_id}, {new.f_head}, {new.f_pattern});" + redo_sql += f"\n{sql_insert_coord(new.id, new.x, new.y)}" + + undo_sql = sql_delete_coord(new.id) + undo_sql += f"\ndelete from reservoirs where id = {new.f_id};" + undo_sql += f"\ndelete from _node where id = {new.f_id};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_id_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_reservoir(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_reservoir(name, cs.operations[0]['id']) != {}: + return ChangeSet() + return execute_command(name, _add_reservoir(name, cs)) + + +def _delete_reservoir(name: str, cs: ChangeSet) -> DbChangeSet: + old = Reservoir(get_reservoir(name, cs.operations[0]['id'])) + + redo_sql = sql_delete_coord(old.id) + redo_sql += f"\ndelete from reservoirs where id = {old.f_id};" + redo_sql += f"\ndelete from _node where id = {old.f_id};" + + undo_sql = f"insert into _node (id, type) values ({old.f_id}, {old.f_type});" + undo_sql += f"\ninsert into reservoirs (id, head, pattern) values ({old.f_id}, {old.f_head}, {old.f_pattern});" + undo_sql += f"\n{sql_insert_coord(old.id, old.x, old.y)}" + + redo_cs = g_delete_prefix | old.as_id_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_reservoir(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_reservoir(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_reservoir(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# id elev (pattern) ;desc +#-------------------------------------------------------------- + + +def inp_in_reservoir(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + id = str(tokens[0]) + head = float(tokens[1]) + pattern = str(tokens[2]) if num_without_desc >= 3 else None + pattern = f"'{pattern}'" if pattern != None else 'null' + desc = str(tokens[-1]) if has_desc else None + + return str(f"insert into _node (id, type) values ('{id}', 'reservoir');insert into reservoirs (id, head, pattern) values ('{id}', {head}, {pattern});") + + +def inp_out_reservoir(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from reservoirs') + for obj in objs: + id = obj['id'] + head = obj['head'] + pattern = obj['pattern'] if obj['pattern'] != None else '' + desc = ';' + lines.append(f'{id} {head} {pattern} {desc}') + return lines + + +def unset_reservoir_by_pattern(name: str, pattern: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select id from reservoirs where pattern = '{pattern}'") + for row in rows: + cs.append(g_update_prefix | {'type': 'reservoir', 'id': row['id'], 'pattern': None}) + + return cs diff --git a/app/native/api/s40_schema.py b/app/native/api/s40_schema.py new file mode 100644 index 0000000..be21ffb --- /dev/null +++ b/app/native/api/s40_schema.py @@ -0,0 +1,30 @@ +from .database import * +from .s0_base import * + +def get_scheme_schema(name: str) -> dict[str, dict[Any, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'name' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'type' : {'type': 'str' , 'optional': False , 'readonly': False}, + "create_time": {'type': 'str' , 'optional': False , 'readonly': True }, + "start_time" : {'type': 'str' , 'optional': False , 'readonly': True }, + "detail" : {'type': 'str' , 'optional': False , 'readonly': True } } + + +def get_scheme(name: str, schema_name: str) -> dict[Any, Any]: + t = try_read(name, f"select * from scheme_list where scheme_name = '{schema_name}'") + if t == None: + return {} + + d = {} + d['id'] = str(t['scheme_id']) + d['name'] = str(t['scheme_name']) + d['type'] = str(t['scheme_type']) + d['create_time'] = str(t['create_time']) + d['start_time'] = str(t['start_time']) + d['detail'] = str(t['detail']) + + return d + +def get_all_schemes(name: str) -> list[dict[Any, Any]]: + return read_all(name, "select * from scheme_list") + diff --git a/app/native/api/s41_pipe_risk_probability.py b/app/native/api/s41_pipe_risk_probability.py new file mode 100644 index 0000000..33f0fe9 --- /dev/null +++ b/app/native/api/s41_pipe_risk_probability.py @@ -0,0 +1,87 @@ +from .database import * +from .s0_base import * +import json + +def get_pipe_risk_probability_now(name: str, pipe_id: str) -> dict[str, Any]: + t = try_read(name, f"select * from pipe_risk_probability where pipeid = '{pipe_id}'") + if t == None: + return {} + + d = {} + d['pipeid'] = str(t['pipeid']) + d['pipeage'] = t['pipeage'] + d['risk_probability_now'] = t['risk_probability_now'] + + return d + +def get_pipe_risk_probability(name: str, pipe_id: str) -> dict[str, Any]: + t = try_read(name, f"select * from pipe_risk_probability where pipeid = '{pipe_id}'") + if t == None: + return {} + + d = {} + d['pipeid'] = t['pipeid'] + d['x'] = t['x'] + d['y'] = t['y'] + + return d + +def get_network_pipe_risk_probability_now(name: str) -> list[dict[str, Any]]: + pipe_risk_probability_list = [] + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select * from pipe_risk_probability") + for record in cur: + #pipe_risk_probability_list.append(record) + t = {} + t['pipeid'] = record['pipeid'] + t['pipeage'] = record['pipeage'] + t['risk_probability_now'] = record['risk_probability_now'] + pipe_risk_probability_list.append(t) + + return pipe_risk_probability_list + +def get_pipes_risk_probability(name: str, pipe_ids: list[str]) -> list[dict[str, Any]]: + pipe_risk_probability_list = [] + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select * from pipe_risk_probability") + for record in cur: + if record['pipeid'] in pipe_ids: + t = {} + t['pipeid'] = record['pipeid'] + t['x'] = record['x'] + t['y'] = record['y'] + pipe_risk_probability_list.append(t) + + return pipe_risk_probability_list + +def get_pipe_risk_probability_geometries(name: str) -> dict[str, Any]: + ''' + 获取管道的几何信息 + 返回一个字典,key 是管道的 id,value 是管道的几何信息 + 几何信息是一个字典,包含 start 和 end 两个 key,value 是管道的起点和终点的坐标 + ''' + pipe_risk_probability_geometries = {} + + key_pipeId = '编码' + # key_startnode = '上游节点' + # key_endnode = '下游节点' + key_geometry = 'geometry' + + with conn[name].cursor(row_factory=dict_row) as cur: + cur.execute(f"select *, ST_AsGeoJSON(geometry) AS {key_geometry} from gis_pipe") + + for record in cur: + id = record[key_pipeId] + geom = json.loads(record[key_geometry]) + + pipe_risk_probability_geometries[id] = { + 'points': geom['coordinates'] + } + + for col in record: + if col != key_geometry: + pipe_risk_probability_geometries[id][col] = record[col] + + # print(len(pipe_risk_probability_geometries)) + + return pipe_risk_probability_geometries \ No newline at end of file diff --git a/app/native/api/s42_sensor_placement.py b/app/native/api/s42_sensor_placement.py new file mode 100644 index 0000000..5ad8b2a --- /dev/null +++ b/app/native/api/s42_sensor_placement.py @@ -0,0 +1,7 @@ +from .database import * +from .s0_base import * +from .s42_sensor_placement import * +import json + +def get_all_sensor_placements(name: str) -> list[dict[Any, Any]]: + return read_all(name, "select * from sensor_placement") \ No newline at end of file diff --git a/app/native/api/s43_burst_locate_result.py b/app/native/api/s43_burst_locate_result.py new file mode 100644 index 0000000..ac26463 --- /dev/null +++ b/app/native/api/s43_burst_locate_result.py @@ -0,0 +1,6 @@ +from .database import * +from .s0_base import * +import json + +def get_all_burst_locate_results(name: str) -> list[dict[Any, Any]]: + return read_all(name, "select * from burst_locate_result") \ No newline at end of file diff --git a/app/native/api/s4_tanks.py b/app/native/api/s4_tanks.py new file mode 100644 index 0000000..306c9d7 --- /dev/null +++ b/app/native/api/s4_tanks.py @@ -0,0 +1,250 @@ +from .database import * +from .s0_base import * +from .s24_coordinates import * + + +OVERFLOW_YES = 'YES' +OVERFLOW_NO = 'NO' + + +def get_tank_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'x' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'y' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'elevation' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'init_level' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'min_level' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'max_level' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'diameter' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'min_vol' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'vol_curve' : {'type': 'str' , 'optional': True , 'readonly': False}, + 'overflow' : {'type': 'str' , 'optional': True , 'readonly': False}, + 'links' : {'type': 'str_list' , 'optional': False , 'readonly': True } } + + +def get_tank(name: str, id: str) -> dict[str, Any]: + t = try_read(name, f"select * from tanks where id = '{id}'") + if t == None: + return {} + xy = get_node_coord(name, id) + d = {} + d['id'] = str(t['id']) + d['x'] = float(xy['x']) + d['y'] = float(xy['y']) + d['elevation'] = float(t['elevation']) + d['init_level'] = float(t['init_level']) + d['min_level'] = float(t['min_level']) + d['max_level'] = float(t['max_level']) + d['diameter'] = float(t['diameter']) + d['min_vol'] = float(t['min_vol']) + d['vol_curve'] = str(t['vol_curve']) if t['vol_curve'] != None else None + d['overflow'] = str(t['overflow']) if t['overflow'] != None else None + d['links'] = get_node_links(name, id) + return d + +# DingZQ, 2025-03-29 +def get_all_tanks(name: str) -> list[dict[str, Any]]: + rows = read_all(name, f"select * from tanks") + if rows == None: + return [] + + result = [] + for row in rows: + d = {} + id = str(row['id']) + xy = get_node_coord(name, id) + d['id'] = id + d['x'] = float(xy['x']) + d['y'] = float(xy['y']) + d['elevation'] = float(row['elevation']) + d['init_level'] = float(row['init_level']) + d['min_level'] = float(row['min_level']) + d['max_level'] = float(row['max_level']) + d['diameter'] = float(row['diameter']) + d['min_vol'] = float(row['min_vol']) + d['vol_curve'] = str(row['vol_curve']) if row['vol_curve'] != None else None + d['overflow'] = str(row['overflow']) if row['overflow'] != None else None + d['links'] = get_node_links(name, id) + result.append(d) + + return result + +class Tank(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'tank' + self.id = str(input['id']) + self.x = float(input['x']) + self.y = float(input['y']) + self.elevation = float(input['elevation']) + self.init_level = float(input['init_level']) + self.min_level = float(input['min_level']) + self.max_level = float(input['max_level']) + self.diameter = float(input['diameter']) + self.min_vol = float(input['min_vol']) + self.vol_curve = str(input['vol_curve']) if 'vol_curve' in input and input['vol_curve'] != None else None + self.overflow = str(input['overflow']) if 'overflow' in input and input['overflow'] != None else None + + self.f_type = f"'{self.type}'" + self.f_id = f"'{self.id}'" + self.f_elevation = self.elevation + self.f_init_level = self.init_level + self.f_min_level = self.min_level + self.f_max_level = self.max_level + self.f_diameter = self.diameter + self.f_min_vol = self.min_vol + self.f_vol_curve = f"'{self.vol_curve}'" if self.vol_curve != None else 'null' + self.f_overflow = f"'{self.overflow}'" if self.overflow != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'elevation': self.elevation, 'init_level': self.init_level, 'min_level': self.min_level, 'max_level': self.max_level, 'diameter': self.diameter, 'min_vol': self.min_vol, 'vol_curve': self.vol_curve, 'overflow': self.overflow } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id } + + +def _set_tank(name: str, cs: ChangeSet) -> DbChangeSet: + old = Tank(get_tank(name, cs.operations[0]['id'])) + raw_new = get_tank(name, cs.operations[0]['id']) + + new_dict = cs.operations[0] + schema = get_tank_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Tank(raw_new) + + redo_sql = f"update tanks set elevation = {new.f_elevation}, init_level = {new.f_init_level}, min_level = {new.f_min_level}, max_level = {new.f_max_level}, diameter = {new.f_diameter}, min_vol = {new.f_min_vol}, vol_curve = {new.f_vol_curve}, overflow = {new.f_overflow} where id = {new.f_id};" + redo_sql += f"\n{sql_update_coord(new.id, new.x, new.y)}" + + undo_sql = sql_update_coord(old.id, old.x, old.y) + undo_sql += f"\nupdate tanks set elevation = {old.f_elevation}, init_level = {old.f_init_level}, min_level = {old.f_min_level}, max_level = {old.f_max_level}, diameter = {old.f_diameter}, min_vol = {old.f_min_vol}, vol_curve = {old.f_vol_curve}, overflow = {old.f_overflow} where id = {old.f_id};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_tank(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_tank(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _set_tank(name, cs)) + + +def _add_tank(name: str, cs: ChangeSet) -> DbChangeSet: + new = Tank(cs.operations[0]) + + redo_sql = f"insert into _node (id, type) values ({new.f_id}, {new.f_type});" + redo_sql += f"\ninsert into tanks (id, elevation, init_level, min_level, max_level, diameter, min_vol, vol_curve, overflow) values ({new.f_id}, {new.f_elevation}, {new.f_init_level}, {new.f_min_level}, {new.f_max_level}, {new.f_diameter}, {new.f_min_vol}, {new.f_vol_curve}, {new.f_overflow});" + redo_sql += f"\n{sql_insert_coord(new.id, new.x, new.y)}" + + undo_sql = sql_delete_coord(new.id) + undo_sql += f"\ndelete from tanks where id = {new.f_id};" + undo_sql += f"\ndelete from _node where id = {new.f_id};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_id_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_tank(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_tank(name, cs.operations[0]['id']) != {}: + return ChangeSet() + return execute_command(name, _add_tank(name, cs)) + + +def _delete_tank(name: str, cs: ChangeSet) -> DbChangeSet: + old = Tank(get_tank(name, cs.operations[0]['id'])) + + redo_sql = sql_delete_coord(old.id) + redo_sql += f"\ndelete from tanks where id = {old.f_id};" + redo_sql += f"\ndelete from _node where id = {old.f_id};" + + undo_sql = f"insert into _node (id, type) values ({old.f_id}, {old.f_type});" + undo_sql += f"\ninsert into tanks (id, elevation, init_level, min_level, max_level, diameter, min_vol, vol_curve, overflow) values ({old.f_id}, {old.f_elevation}, {old.f_init_level}, {old.f_min_level}, {old.f_max_level}, {old.f_diameter}, {old.f_min_vol}, {old.f_vol_curve}, {old.f_overflow});" + undo_sql += f"\n{sql_insert_coord(old.id, old.x, old.y)}" + + redo_cs = g_delete_prefix | old.as_id_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_tank(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_tank(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_tank(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2] +# [IN] +# id elev initlevel minlevel maxlevel diam (minvol vcurve overflow) ;desc +# xxx +# * YES +# [OUT] +# id elev initlevel minlevel maxlevel diam minvol (vcurve overflow) ;desc +#-------------------------------------------------------------- +# [EPA3] +# id elev initlevel minlevel maxlevel diam minvol (vcurve) +#-------------------------------------------------------------- + + +def inp_in_tank(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + id = str(tokens[0]) + elevation = float(tokens[1]) + init_level = float(tokens[2]) + min_level = float(tokens[3]) + max_level = float(tokens[4]) + diameter = float(tokens[5]) + min_vol = float(tokens[6]) if num_without_desc >= 7 else 0.0 + vol_curve = str(tokens[7]) if num_without_desc >= 8 and tokens[7] != '*' else None + vol_curve = f"'{vol_curve}'" if vol_curve != None else 'null' + overflow = str(tokens[8].upper()) if num_without_desc >= 9 else None + overflow = f"'{overflow}'" if overflow != None else 'null' + desc = str(tokens[-1]) if has_desc else None + + return str(f"insert into _node (id, type) values ('{id}', 'tank');insert into tanks (id, elevation, init_level, min_level, max_level, diameter, min_vol, vol_curve, overflow) values ('{id}', {elevation}, {init_level}, {min_level}, {max_level}, {diameter}, {min_vol}, {vol_curve}, {overflow});") + + +def inp_out_tank(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from tanks') + for obj in objs: + id = obj['id'] + elevation = obj['elevation'] + init_level = obj['init_level'] + min_level = obj['min_level'] + max_level = obj['max_level'] + diameter = obj['diameter'] + min_vol = obj['min_vol'] + vol_curve = obj['vol_curve'] if obj['vol_curve'] != None else '' + overflow = obj['overflow'] if obj['overflow'] != None else '' + if vol_curve == '' and overflow != '': + vol_curve = '*' + desc = ';' + lines.append(f'{id} {elevation} {init_level} {min_level} {max_level} {diameter} {min_vol} {vol_curve} {overflow} {desc}') + return lines + + +def unset_tank_by_curve(name: str, curve: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select id from tanks where vol_curve = '{curve}'") + for row in rows: + cs.append(g_update_prefix | {'type': 'tank', 'id': row['id'], 'vol_curve': None}) + + return cs diff --git a/app/native/api/s5_pipes.py b/app/native/api/s5_pipes.py new file mode 100644 index 0000000..2930c3a --- /dev/null +++ b/app/native/api/s5_pipes.py @@ -0,0 +1,214 @@ +from .database import * +from .s0_base import * + + +PIPE_STATUS_OPEN = 'OPEN' +PIPE_STATUS_CLOSED = 'CLOSED' +PIPE_STATUS_CV = 'CV' + + +def get_pipe_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'node1' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'node2' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'length' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'diameter' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'roughness' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'minor_loss' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'status' : {'type': 'str' , 'optional': False , 'readonly': False} } + + +def get_pipe(name: str, id: str) -> dict[str, Any]: + p = try_read(name, f"select * from pipes where id = '{id}'") + if p == None: + return {} + d = {} + d['id'] = str(p['id']) + d['node1'] = str(p['node1']) + d['node2'] = str(p['node2']) + d['length'] = float(p['length']) + d['diameter'] = float(p['diameter']) + d['roughness'] = float(p['roughness']) + d['minor_loss'] = float(p['minor_loss']) + d['status'] = str(p['status']) + return d + +# DingZQ, 2025-03-29 +def get_all_pipes(name: str) -> list[dict[str, Any]]: + rows = read_all(name, f"select * from pipes") + if rows == None: + return [] + + result = [] + for row in rows: + d = {} + d['id'] = str(row['id']) + d['node1'] = str(row['node1']) + d['node2'] = str(row['node2']) + d['length'] = float(row['length']) + d['diameter'] = float(row['diameter']) + d['roughness'] = float(row['roughness']) + d['minor_loss'] = float(row['minor_loss']) + d['status'] = str(row['status']) + result.append(d) + + return result + +class Pipe(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'pipe' + self.id = str(input['id']) + self.node1 = str(input['node1']) + self.node2 = str(input['node2']) + self.length = float(input['length']) + self.diameter = float(input['diameter']) + self.roughness = float(input['roughness']) + self.minor_loss = float(input['minor_loss']) + self.status = str(input['status']) + + self.f_type = f"'{self.type}'" + self.f_id = f"'{self.id}'" + self.f_node1 = f"'{self.node1}'" + self.f_node2 = f"'{self.node2}'" + self.f_length = self.length + self.f_diameter = self.diameter + self.f_roughness = self.roughness + self.f_minor_loss = self.minor_loss + self.f_status = f"'{self.status}'" + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id, 'node1': self.node1, 'node2': self.node2, 'length': self.length, 'diameter': self.diameter, 'roughness': self.roughness, 'minor_loss': self.minor_loss, 'status': self.status } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id } + + +def _set_pipe(name: str, cs: ChangeSet) -> DbChangeSet: + old = Pipe(get_pipe(name, cs.operations[0]['id'])) + raw_new = get_pipe(name, cs.operations[0]['id']) + + new_dict = cs.operations[0] + schema = get_pipe_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Pipe(raw_new) + + redo_sql = f"update pipes set node1 = {new.f_node1}, node2 = {new.f_node2}, length = {new.f_length}, diameter = {new.f_diameter}, roughness = {new.f_roughness}, minor_loss = {new.f_minor_loss}, status = {new.f_status} where id = {new.f_id};" + undo_sql = f"update pipes set node1 = {old.f_node1}, node2 = {old.f_node2}, length = {old.f_length}, diameter = {old.f_diameter}, roughness = {old.f_roughness}, minor_loss = {old.f_minor_loss}, status = {old.f_status} where id = {old.f_id};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_pipe(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_pipe(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _set_pipe(name, cs)) + + +def _add_pipe(name: str, cs: ChangeSet) -> DbChangeSet: + new = Pipe(cs.operations[0]) + + redo_sql = f"insert into _link (id, type) values ({new.f_id}, {new.f_type});" + redo_sql += f"\ninsert into pipes (id, node1, node2, length, diameter, roughness, minor_loss, status) values ({new.f_id}, {new.f_node1}, {new.f_node2}, {new.f_length}, {new.f_diameter}, {new.f_roughness}, {new.f_minor_loss}, {new.f_status});" + + undo_sql = f"delete from pipes where id = {new.f_id};" + undo_sql += f"\ndelete from _link where id = {new.f_id};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_id_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_pipe(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_pipe(name, cs.operations[0]['id']) != {}: + return ChangeSet() + return execute_command(name, _add_pipe(name, cs)) + + +def _delete_pipe(name: str, cs: ChangeSet) -> DbChangeSet: + old = Pipe(get_pipe(name, cs.operations[0]['id'])) + + redo_sql = f"delete from pipes where id = {old.f_id};" + redo_sql += f"\ndelete from _link where id = {old.f_id};" + + undo_sql = f"insert into _link (id, type) values ({old.f_id}, {old.f_type});" + undo_sql += f"\ninsert into pipes (id, node1, node2, length, diameter, roughness, minor_loss, status) values ({old.f_id}, {old.f_node1}, {old.f_node2}, {old.f_length}, {old.f_diameter}, {old.f_roughness}, {old.f_minor_loss}, {old.f_status});" + + redo_cs = g_delete_prefix | old.as_id_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_pipe(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_pipe(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_pipe(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3] +# [IN] +# id node1 node2 length diam rcoeff (lcoeff status) ;desc +# [OUT] +# id node1 node2 length diam rcoeff lcoeff (status) ;desc +#-------------------------------------------------------------- + + +def inp_in_pipe(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + id = str(tokens[0]) + node1 = str(tokens[1]) + node2 = str(tokens[2]) + length = float(tokens[3]) + diameter = float(tokens[4]) + roughness = float(tokens[5]) + minor_loss = float(tokens[6]) + # status is must-have, here fix input + status = str(tokens[7].upper()) if num_without_desc >= 8 else PIPE_STATUS_OPEN + desc = str(tokens[-1]) if has_desc else None + + return str(f"insert into _link (id, type) values ('{id}', 'pipe');insert into pipes (id, node1, node2, length, diameter, roughness, minor_loss, status) values ('{id}', '{node1}', '{node2}', {length}, {diameter}, {roughness}, {minor_loss}, '{status}');") + + +def inp_out_pipe(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from pipes') + for obj in objs: + id = obj['id'] + node1 = obj['node1'] + node2 = obj['node2'] + length = obj['length'] + diameter = obj['diameter'] + roughness = obj['roughness'] + minor_loss = obj['minor_loss'] + status = obj['status'] + desc = ';' + lines.append(f'{id} {node1} {node2} {length} {diameter} {roughness} {minor_loss} {status} {desc}') + return lines + + +'''def delete_pipe_by_node(name: str, node: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select id from pipes where node1 = '{node}' or node2 = '{node}'") + for row in rows: + cs.append(g_delete_prefix | {'type': 'pipe', 'id': row['id']}) + + return cs''' diff --git a/app/native/api/s6_pumps.py b/app/native/api/s6_pumps.py new file mode 100644 index 0000000..8ac4622 --- /dev/null +++ b/app/native/api/s6_pumps.py @@ -0,0 +1,231 @@ +from .database import * +from .s0_base import * + + +def get_pump_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'node1' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'node2' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'power' : {'type': 'float' , 'optional': True , 'readonly': False}, + 'head' : {'type': 'str' , 'optional': True , 'readonly': False}, + 'speed' : {'type': 'float' , 'optional': True , 'readonly': False}, + 'pattern' : {'type': 'str' , 'optional': True , 'readonly': False} } + + +def get_pump(name: str, id: str) -> dict[str, Any]: + p = try_read(name, f"select * from pumps where id = '{id}'") + if p == None: + return {} + d = {} + d['id'] = str(p['id']) + d['node1'] = str(p['node1']) + d['node2'] = str(p['node2']) + d['power'] = float(p['power']) if p['power'] != None else None + d['head'] = str(p['head']) if p['head'] != None else None + d['speed'] = float(p['speed']) if p['speed'] != None else None + d['pattern'] = str(p['pattern']) if p['pattern'] != None else None + return d + +# DingZQ, 2025-03-29 +def get_all_pumps(name: str) -> list[dict[str, Any]]: + rows = read_all(name, f"select * from pumps") + if rows == None: + return [] + + result = [] + for row in rows: + d = {} + d['id'] = str(row['id']) + d['node1'] = str(row['node1']) + d['node2'] = str(row['node2']) + d['power'] = float(row['power']) if row['power'] != None else None + d['head'] = str(row['head']) if row['head'] != None else None + d['speed'] = float(row['speed']) if row['speed'] != None else None + d['pattern'] = str(row['pattern']) if row['pattern'] != None else None + result.append(d) + + return result + + + +class Pump(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'pump' + self.id = str(input['id']) + self.node1 = str(input['node1']) + self.node2 = str(input['node2']) + self.power = float(input['power']) if 'power' in input and input['power'] != None else None + self.head = str(input['head']) if 'head' in input and input['head'] != None else None + self.speed = float(input['speed']) if 'speed' in input and input['speed'] != None else None + self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None + + self.f_type = f"'{self.type}'" + self.f_id = f"'{self.id}'" + self.f_node1 = f"'{self.node1}'" + self.f_node2 = f"'{self.node2}'" + self.f_power = self.power if self.power != None else 'null' + self.f_head = f"'{self.head}'" if self.head != None else 'null' + self.f_speed = self.speed if self.speed != None else 'null' + self.f_pattern = f"'{self.pattern}'" if self.pattern != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id, 'node1': self.node1, 'node2': self.node2, 'power': self.power, 'head': self.head, 'speed': self.speed, 'pattern': self.pattern } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id } + + +def _set_pump(name: str, cs: ChangeSet) -> DbChangeSet: + old = Pump(get_pump(name, cs.operations[0]['id'])) + raw_new = get_pump(name, cs.operations[0]['id']) + + new_dict = cs.operations[0] + schema = get_pump_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Pump(raw_new) + + redo_sql = f"update pumps set node1 = {new.f_node1}, node2 = {new.f_node2}, power = {new.f_power}, head = {new.f_head}, speed = {new.f_speed}, pattern = {new.f_pattern} where id = {new.f_id};" + undo_sql = f"update pumps set node1 = {old.f_node1}, node2 = {old.f_node2}, power = {old.f_power}, head = {old.f_head}, speed = {old.f_speed}, pattern = {old.f_pattern} where id = {old.f_id};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_pump(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_pump(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _set_pump(name, cs)) + + +def _add_pump(name: str, cs: ChangeSet) -> DbChangeSet: + new = Pump(cs.operations[0]) + + redo_sql = f"insert into _link (id, type) values ({new.f_id}, {new.f_type});" + redo_sql += f"\ninsert into pumps (id, node1, node2, power, head, speed, pattern) values ({new.f_id}, {new.f_node1}, {new.f_node2}, {new.f_power}, {new.f_head}, {new.f_speed}, {new.f_pattern});" + + undo_sql = f"delete from pumps where id = {new.f_id};" + undo_sql += f"\ndelete from _link where id = {new.f_id};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_id_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_pump(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_pump(name, cs.operations[0]['id']) != {}: + return ChangeSet() + return execute_command(name, _add_pump(name, cs)) + + +def _delete_pump(name: str, cs: ChangeSet) -> DbChangeSet: + old = Pump(get_pump(name, cs.operations[0]['id'])) + + redo_sql = f"delete from pumps where id = {old.f_id};" + redo_sql += f"\ndelete from _link where id = {old.f_id};" + + undo_sql = f"insert into _link (id, type) values ({old.f_id}, {old.f_type});" + undo_sql += f"\ninsert into pumps (id, node1, node2, power, head, speed, pattern) values ({old.f_id}, {old.f_node1}, {old.f_node2}, {old.f_power}, {old.f_head}, {old.f_speed}, {old.f_pattern});" + + redo_cs = g_delete_prefix | old.as_id_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_pump(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_pump(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_pump(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# id node1 node2 KEYWORD value {KEYWORD value ...} ;desc +# where KEYWORD = [POWER,HEAD,PATTERN,SPEED] +#-------------------------------------------------------------- + + +def inp_in_pump(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + id = str(tokens[0]) + node1 = str(tokens[1]) + node2 = str(tokens[2]) + props = {} + for i in range(3, num_without_desc, 2): + props |= { tokens[i].lower(): tokens[i + 1] } + power = float(props['power']) if 'power' in props else None + power = power if power != None else 'null' + head = str(props['head']) if 'head' in props else None + head = f"'{head}'" if head != None else 'null' + speed = float(props['speed']) if 'speed' in props else None + speed = speed if speed != None else 'null' + pattern = str(props['pattern']) if 'pattern' in props else None + pattern = f"'{pattern}'" if pattern != None else 'null' + desc = str(tokens[-1]) if has_desc else None + + return str(f"insert into _link (id, type) values ('{id}', 'pump');insert into pumps (id, node1, node2, power, head, speed, pattern) values ('{id}', '{node1}', '{node2}', {power}, {head}, {speed}, {pattern});") + + +def inp_out_pump(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from pumps') + for obj in objs: + id = obj['id'] + node1 = obj['node1'] + node2 = obj['node2'] + power = f"POWER {obj['power']}" if obj['power'] != None else '' + head = f"HEAD {obj['head']}" if obj['head'] != None else '' + speed = f"SPEED {obj['speed']}" if obj['speed'] != None else '' + pattern = f"PATTERN {obj['pattern']}" if obj['pattern'] != None else '' + desc = ';' + lines.append(f'{id} {node1} {node2} {power} {head} {speed} {pattern} {desc}') + return lines + + +'''def delete_pump_by_node(name: str, node: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select id from pumps where node1 = '{node}' or node2 = '{node}'") + for row in rows: + cs.append(g_delete_prefix | {'type': 'pump', 'id': row['id']}) + + return cs''' + + +def unset_pump_by_curve(name: str, curve: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select * from pumps where head = '{curve}'") + for row in rows: + if row['power'] != None: + cs.append(g_update_prefix | {'type': 'pump', 'id': row['id'], 'head': None}) + else: # workaround to prevent pump deletion... and I don't want to remove constraint... + cs.append(g_update_prefix | {'type': 'pump', 'id': row['id'], 'head': None, 'power': 0.0}) + + return cs + + +def unset_pump_by_pattern(name: str, pattern: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select id from pumps where pattern = '{pattern}'") + for row in rows: + cs.append(g_update_prefix | {'type': 'pump', 'id': row['id'], 'pattern': None}) + + return cs diff --git a/app/native/api/s7_valves.py b/app/native/api/s7_valves.py new file mode 100644 index 0000000..3192088 --- /dev/null +++ b/app/native/api/s7_valves.py @@ -0,0 +1,210 @@ +from .database import * +from .s0_base import * + + +VALVES_TYPE_PRV = 'PRV' +VALVES_TYPE_PSV = 'PSV' +VALVES_TYPE_PBV = 'PBV' +VALVES_TYPE_FCV = 'FCV' +VALVES_TYPE_TCV = 'TCV' +VALVES_TYPE_GPV = 'GPV' + + +def get_valve_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'node1' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'node2' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'diameter' : {'type': 'float' , 'optional': False , 'readonly': False}, + 'v_type' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'setting' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'minor_loss' : {'type': 'float' , 'optional': False , 'readonly': False} } + + +def get_valve(name: str, id: str) -> dict[str, Any]: + p = try_read(name, f"select * from valves where id = '{id}'") + if p == None: + return {} + d = {} + d['id'] = str(p['id']) + d['node1'] = str(p['node1']) + d['node2'] = str(p['node2']) + d['diameter'] = float(p['diameter']) + d['v_type'] = str(p['v_type']) + d['setting'] = str(p['setting']) + d['minor_loss'] = float(p['minor_loss']) + return d + +def get_all_valves(name: str) -> list[dict[str, Any]]: + rows = read_all(name, f"select * from valves") + if rows == None: + return [] + + result = [] + for row in rows: + d = {} + d['id'] = str(row['id']) + d['node1'] = str(row['node1']) + d['node2'] = str(row['node2']) + d['diameter'] = float(row['diameter']) + d['v_type'] = str(row['v_type']) + d['setting'] = str(row['setting']) + d['minor_loss'] = float(row['minor_loss']) + result.append(d) + + return result + + + + +class Valve(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'valve' + self.id = str(input['id']) + self.node1 = str(input['node1']) + self.node2 = str(input['node2']) + self.diameter = float(input['diameter']) + self.v_type = str(input['v_type']) + self.setting = str(input['setting']) + self.minor_loss = float(input['minor_loss']) + + self.f_type = f"'{self.type}'" + self.f_id = f"'{self.id}'" + self.f_node1 = f"'{self.node1}'" + self.f_node2 = f"'{self.node2}'" + self.f_diameter = self.diameter + self.f_v_type = f"'{self.v_type}'" + self.f_setting = f"'{self.setting}'" + self.f_minor_loss = self.minor_loss + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id, 'node1': self.node1, 'node2': self.node2, 'diameter': self.diameter, 'v_type': self.v_type, 'setting': self.setting, 'minor_loss': self.minor_loss } + + def as_id_dict(self) -> dict[str, Any]: + return { 'type': self.type, 'id': self.id } + + +def _set_valve(name: str, cs: ChangeSet) -> DbChangeSet: + old = Valve(get_valve(name, cs.operations[0]['id'])) + raw_new = get_valve(name, cs.operations[0]['id']) + + new_dict = cs.operations[0] + schema = get_valve_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Valve(raw_new) + + redo_sql = f"update valves set node1 = {new.f_node1}, node2 = {new.f_node2}, diameter = {new.f_diameter}, v_type = {new.f_v_type}, setting = {new.f_setting}, minor_loss = {new.f_minor_loss} where id = {new.f_id};" + undo_sql = f"update valves set node1 = {old.f_node1}, node2 = {old.f_node2}, diameter = {old.f_diameter}, v_type = {old.f_v_type}, setting = {old.f_setting}, minor_loss = {old.f_minor_loss} where id = {old.f_id};" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_valve(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_valve(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _set_valve(name, cs)) + + +def _add_valve(name: str, cs: ChangeSet) -> DbChangeSet: + new = Valve(cs.operations[0]) + + redo_sql = f"insert into _link (id, type) values ({new.f_id}, {new.f_type});" + redo_sql += f"\ninsert into valves (id, node1, node2, diameter, v_type, setting, minor_loss) values ({new.f_id}, {new.f_node1}, {new.f_node2}, {new.f_diameter}, {new.f_v_type}, {new.f_setting}, {new.f_minor_loss});" + + undo_sql = f"delete from valves where id = {new.f_id};" + undo_sql += f"\ndelete from _link where id = {new.f_id};" + + redo_cs = g_add_prefix | new.as_dict() + undo_cs = g_delete_prefix | new.as_id_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def add_valve(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_valve(name, cs.operations[0]['id']) != {}: + return ChangeSet() + return execute_command(name, _add_valve(name, cs)) + + +def _delete_valve(name: str, cs: ChangeSet) -> DbChangeSet: + old = Valve(get_valve(name, cs.operations[0]['id'])) + + redo_sql = f"delete from valves where id = {old.f_id};" + redo_sql += f"\ndelete from _link where id = {old.f_id};" + + undo_sql = f"insert into _link (id, type) values ({old.f_id}, {old.f_type});" + undo_sql += f"\ninsert into valves (id, node1, node2, diameter, v_type, setting, minor_loss) values ({old.f_id}, {old.f_node1}, {old.f_node2}, {old.f_diameter}, {old.f_v_type}, {old.f_setting}, {old.f_minor_loss});" + + redo_cs = g_delete_prefix | old.as_id_dict() + undo_cs = g_add_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def delete_valve(name: str, cs: ChangeSet) -> ChangeSet: + if 'id' not in cs.operations[0]: + return ChangeSet() + if get_valve(name, cs.operations[0]['id']) == {}: + return ChangeSet() + return execute_command(name, _delete_valve(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# id node1 node2 diam type setting (lcoeff lcurve) +# for GPV, setting is string = head curve id +# [NOT SUPPORT] for PCV, add loss curve if present +#-------------------------------------------------------------- + + +def inp_in_valve(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + id = str(tokens[0]) + node1 = str(tokens[1]) + node2 = str(tokens[2]) + diameter = float(tokens[3]) + v_type = str(tokens[4].upper()) + setting = str(tokens[5]) + minor_loss = float(tokens[6]) if len(tokens) >= 7 else 0.0 + desc = str(tokens[-1]) if has_desc else None + + return str(f"insert into _link (id, type) values ('{id}', 'valve');insert into valves (id, node1, node2, diameter, v_type, setting, minor_loss) values ('{id}', '{node1}', '{node2}', {diameter}, '{v_type}', '{setting}', {minor_loss});") + + +def inp_out_valve(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from valves') + for obj in objs: + id = obj['id'] + node1 = obj['node1'] + node2 = obj['node2'] + diameter = obj['diameter'] + v_type = obj['v_type'] + setting = obj['setting'] + minor_loss = obj['minor_loss'] + desc = ';' + lines.append(f'{id} {node1} {node2} {diameter} {v_type} {setting} {minor_loss} {desc}') + return lines + + +'''def delete_valve_by_node(name: str, node: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select id from valves where node1 = '{node}' or node2 = '{node}'") + for row in rows: + cs.append(g_delete_prefix | {'type': 'valve', 'id': row['id']}) + + return cs''' diff --git a/app/native/api/s8_tags.py b/app/native/api/s8_tags.py new file mode 100644 index 0000000..3a77576 --- /dev/null +++ b/app/native/api/s8_tags.py @@ -0,0 +1,142 @@ +from typing import Any +from .database import ChangeSet, execute_command, try_read, read_all, DbChangeSet, g_update_prefix + +TAG_TYPE_NODE = 'NODE' +TAG_TYPE_LINK = 'LINK' + +def get_tag_schema(name: str) -> dict[str, dict[str, Any]]: + return { 't_type' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'id' : {'type': 'str' , 'optional': False , 'readonly': False}, + 'tag' : {'type': 'str' , 'optional': True , 'readonly': False},} + + +def get_tags(name: str) -> list[dict[str, Any]]: + results: list[dict[str, Any]] = [] + rows = read_all(name, "select * from tags_node") + for row in rows: + tag = str(row['tag']) if row['tag'] != None else None + results.append({ 't_type': TAG_TYPE_NODE, 'id': str(row['id']), 'tag': tag }) + rows = read_all(name, "select * from tags_link") + for row in rows: + tag = str(row['tag']) if row['tag'] != None else None + results.append({ 't_type': TAG_TYPE_LINK, 'id': str(row['id']), 'tag': tag }) + return results + + +def get_tag(name: str, t_type: str, id: str) -> dict[str, Any]: + t = None + if t_type == TAG_TYPE_NODE: + t = try_read(name, f"select * from tags_node where id = '{id}'") + elif t_type == TAG_TYPE_LINK: + t = try_read(name, f"select * from tags_link where id = '{id}'") + if t is None: + return { 't_type': t_type, 'id': id, 'tag': None } + d = {} + d['t_type'] = t_type + d['id'] = str(t['id']) + d['tag'] = str(t['tag']) if t['tag'] is not None else None + return d + + +class Tag(object): + def __init__(self, input: dict[str, Any]) -> None: + self.type = 'tag' + self.t_type = str(input['t_type']) + self.id = str(input['id']) + self.tag = str(input['tag']) if 'tag' in input and input['tag'] != None else None + + self.f_type = f"'{self.type}'" + self.f_t_type = f"'{self.t_type}'" + self.f_id = f"'{self.id}'" + self.f_tag = f"'{self.tag}'" if self.tag != None else 'null' + + def as_dict(self) -> dict[str, Any]: + return { 'type': self.type, 't_type': self.t_type, 'id': self.id, 'tag': self.tag } + + +def _set_tag(name: str, cs: ChangeSet) -> DbChangeSet: + old = Tag(get_tag(name, cs.operations[0]['t_type'], cs.operations[0]['id'])) + raw_new = get_tag(name, cs.operations[0]['t_type'], cs.operations[0]['id']) + + new_dict = cs.operations[0] + schema = get_tag_schema(name) + for key, value in schema.items(): + if key in new_dict and not value['readonly']: + raw_new[key] = new_dict[key] + new = Tag(raw_new) + + table = '' + if cs.operations[0]['t_type'] == TAG_TYPE_NODE: + table = 'tags_node' + elif cs.operations[0]['t_type'] == TAG_TYPE_LINK: + table = 'tags_link' + else: + raise Exception('Only support NODE and Link') + + redo_sql = f"delete from {table} where id = {new.f_id};" + if new.tag is not None: + redo_sql += f"\ninsert into {table} (id, tag) values ({new.f_id}, {new.f_tag});" + + undo_sql = f"delete from {table} where id = {old.f_id};" + if old.tag is not None: + undo_sql += f"\ninsert into {table} (id, tag) values ({old.f_id}, {old.f_tag});" + + redo_cs = g_update_prefix | new.as_dict() + undo_cs = g_update_prefix | old.as_dict() + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_tag(name: str, cs: ChangeSet) -> ChangeSet: + if 't_type' not in cs.operations[0] or 'id' not in cs.operations[0] or 'tag' not in cs.operations[0]: + return ChangeSet() + return execute_command(name, _set_tag(name, cs)) + + +def inp_in_tag(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + t_type = str(tokens[0].upper()) + id = str(tokens[1]) + tag = str(tokens[2]) + + if t_type == TAG_TYPE_NODE: + return str(f"insert into tags_node (id, tag) values ('{id}', '{tag}');") + elif t_type == TAG_TYPE_LINK: + return str(f"insert into tags_link (id, tag) values ('{id}', '{tag}');") + return str('') + + +def inp_out_tag(name: str) -> list[str]: + lines = [] + objs = read_all(name, 'select * from tags_node') + for obj in objs: + t_type = TAG_TYPE_NODE + id = obj['id'] + tag = obj['tag'] + lines.append(f'{t_type} {id} {tag}') + objs = read_all(name, 'select * from tags_link') + for obj in objs: + t_type = TAG_TYPE_LINK + id = obj['id'] + tag = obj['tag'] + lines.append(f'{t_type} {id} {tag}') + return lines + + +def delete_tag_by_node(name: str, node: str) -> ChangeSet: + row = try_read(name, f"select * from tags_node where id = '{node}'") + if row is None: + return ChangeSet() + return ChangeSet(g_update_prefix | {'type': 'tag', 't_type': TAG_TYPE_NODE, 'id': node, 'tag': None }) + + +def delete_tag_by_link(name: str, link: str) -> ChangeSet: + row = try_read(name, f"select * from tags_link where id = '{link}'") + if row is None: + return ChangeSet() + return ChangeSet(g_update_prefix | {'type': 'tag', 't_type': TAG_TYPE_LINK, 'id': link, 'tag': None }) diff --git a/app/native/api/s9_demands.py b/app/native/api/s9_demands.py new file mode 100644 index 0000000..e1b8126 --- /dev/null +++ b/app/native/api/s9_demands.py @@ -0,0 +1,115 @@ +from .database import read_all, ChangeSet, DbChangeSet, g_update_prefix, execute_command, try_read +from typing import Any + +def get_demand_schema(name: str) -> dict[str, dict[str, Any]]: + return { 'junction' : {'type': 'str' , 'optional': False , 'readonly': True }, + 'demands' : {'type': 'list' , 'optional': False , 'readonly': False, + 'element': { 'demand' : {'type': 'float' , 'optional': False , 'readonly': False }, + 'pattern' : {'type': 'str' , 'optional': True , 'readonly': False }, + 'category': {'type': 'str' , 'optional': True , 'readonly': False }}}} + + +def get_demand(name: str, junction: str) -> dict[str, Any]: + des = read_all(name, f"select * from demands where junction = '{junction}' order by _order") + ds = [] + for r in des: + d = {} + d['demand'] = float(r['demand']) + d['pattern'] = str(r['pattern']) if r['pattern'] != None else None + d['category'] = str(r['category']) if r['category'] != None else None + ds.append(d) + return { 'junction': junction, 'demands': ds } + + +def _set_demand(name: str, cs: ChangeSet) -> DbChangeSet: + junction = cs.operations[0]['junction'] + old = get_demand(name, junction) + new = { 'junction': junction, 'demands': [] } + + f_junction = f"'{junction}'" + + # TODO: transaction ? + redo_sql = f"delete from demands where junction = {f_junction};" + for r in cs.operations[0]['demands']: + demand = float(r['demand']) + pattern = str(r['pattern']) if 'pattern' in r and r['pattern'] != None else None + category = str(r['category']) if 'category' in r and r['category'] != None else None + f_demand = demand + f_pattern = f"'{pattern}'" if pattern is not None else 'null' + f_category = f"'{category}'" if category is not None else 'null' + redo_sql += f"\ninsert into demands (junction, demand, pattern, category) values ({f_junction}, {f_demand}, {f_pattern}, {f_category});" + new['demands'].append({ 'demand': demand, 'pattern': pattern, 'category': category }) + + undo_sql = f"delete from demands where junction = {f_junction};" + for r in old['demands']: + demand = float(r['demand']) + pattern = str(r['pattern']) if 'pattern' in r and r['pattern'] != None else None + category = str(r['category']) if 'category' in r and r['category'] != None else None + f_demand = demand + f_pattern = f"'{pattern}'" if pattern is not None else 'null' + f_category = f"'{category}'" if category is not None else 'null' + undo_sql += f"\ninsert into demands (junction, demand, pattern, category) values ({f_junction}, {f_demand}, {f_pattern}, {f_category});" + + redo_cs = g_update_prefix | { 'type': 'demand' } | new + undo_cs = g_update_prefix | { 'type': 'demand' } | old + + return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) + + +def set_demand(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_demand(name, cs)) + + +#-------------------------------------------------------------- +# [EPA2][EPA3][IN][OUT] +# node base_demand (pattern) ;category +#-------------------------------------------------------------- + + +def inp_in_demand(line: str) -> str: + tokens = line.split() + + num = len(tokens) + has_desc = tokens[-1].startswith(';') + num_without_desc = (num - 1) if has_desc else num + + junction = str(tokens[0]) + demand = float(tokens[1]) + pattern = str(tokens[2]) if num_without_desc >= 3 else None + pattern = f"'{pattern}'" if pattern is not None else 'null' + category = str(tokens[3]) if num_without_desc >= 4 else None + category = f"'{category}'" if category is not None else 'null' + + return str(f"insert into demands (junction, demand, pattern, category) values ('{junction}', {demand}, {pattern}, {category});") + + +def inp_out_demand(name: str) -> list[str]: + lines = [] + objs = read_all(name, "select * from demands order by _order") + for obj in objs: + junction = obj['junction'] + demand = obj['demand'] + pattern = obj['pattern'] if obj['pattern'] is not None else '' + category = f";{obj['category']}" if obj['category'] is not None else ';' + lines.append(f'{junction} {demand} {pattern} {category}') + return lines + + +def delete_demand_by_junction(name: str, junction: str) -> ChangeSet: + row = try_read(name, f"select * from demands where junction = '{junction}'") + if row is None: + return ChangeSet() + return ChangeSet(g_update_prefix | {'type': 'demand', 'junction': junction, 'demands': []}) + + +def unset_demand_by_pattern(name: str, pattern: str) -> ChangeSet: + cs = ChangeSet() + + rows = read_all(name, f"select distinct junction from demands where pattern = '{pattern}'") + for row in rows: + ds = get_demand(name, row['junction']) + for d in ds['demands']: + d['pattern'] = None + cs.append(g_update_prefix | {'type': 'demand', 'junction': row['junction'], 'demands': ds['demands']}) + + return cs diff --git a/app/native/api/sections.py b/app/native/api/sections.py new file mode 100644 index 0000000..8c48a0d --- /dev/null +++ b/app/native/api/sections.py @@ -0,0 +1,90 @@ +s1_title = 'title' +s2_junction = 'junction' +s3_reservoir = 'reservoir' +s4_tank = 'tank' +s5_pipe = 'pipe' +s6_pump = 'pump' +s7_valve = 'valve' +s8_tag = 'tag' +s9_demand = 'demand' +s10_status = 'status' +s11_pattern = 'pattern' +s12_curve = 'curve' +s13_control = 'control' +s14_rule = 'rule' +s15_energy = 'energy' +s15_pump_energy = 'pump_energy' +s16_emitter = 'emitter' +s17_quality = 'quality' +s18_source = 'source' +s19_reaction = 'reaction' +s19_pipe_reaction = 'pipe_reaction' +s19_tank_reaction = 'tank_reaction' +s20_mixing = 'mixing' +s21_time = 'time' +s22_report = 'report' +s23_option = 'option' +s23_option_v3 = 'option_v3' +s24_coordinate = 'coordinate' +s25_vertex = 'vertex' +s26_label = 'label' +s27_backdrop = 'backdrop' +s28_end = 'end' +s29_scada_device = 'scada_device' +s30_scada_device_data = 'scada_device_data' +s31_scada_element = 'scada_element' +s32_region = 'region' +s33_dma = 'district_metering_area' +s34_sa = 'service_area' +s35_vd = 'virtual_district' + +TITLE = 'TITLE' +JUNCTIONS = 'JUNCTIONS' +RESERVOIRS = 'RESERVOIRS' +TANKS = 'TANKS' +PIPES = 'PIPES' +PUMPS = 'PUMPS' +VALVES = 'VALVES' +TAGS = 'TAGS' +DEMANDS = 'DEMANDS' +STATUS = 'STATUS' +PATTERNS = 'PATTERNS' +CURVES = 'CURVES' +CONTROLS = 'CONTROLS' +RULES = 'RULES' +ENERGY = 'ENERGY' +EMITTERS = 'EMITTERS' +QUALITY = 'QUALITY' +SOURCES = 'SOURCES' +REACTIONS = 'REACTIONS' +MIXING = 'MIXING' +TIMES = 'TIMES' +REPORT = 'REPORT' +OPTIONS = 'OPTIONS' +COORDINATES = 'COORDINATES' +VERTICES = 'VERTICES' +REGION='REGION' +BOUND='BOUND' +REGION_NODES='DATA_NODE_OF_REGION' +LABELS = 'LABELS' +BACKDROP = 'BACKDROP' +END = 'END' + +section_name = [TITLE, JUNCTIONS, RESERVOIRS, TANKS, PIPES, + PUMPS, VALVES, TAGS, DEMANDS, STATUS, + PATTERNS, CURVES, CONTROLS, RULES, ENERGY, + EMITTERS, QUALITY, SOURCES, REACTIONS, MIXING, + TIMES, REPORT, OPTIONS, COORDINATES, VERTICES, + REGION, BOUND, REGION_NODES, LABELS, BACKDROP, END] + +# DingZQ, 2025-02-04 +# 我们在从服务器调用run_project的时候 +# 会将 database的project内容dump成 epanet v2 的inp文件,然后调用 runepanet.exe 去计算结果 +# 其中上面的 SECTION : REGION, BOUND, REGION_NODES 在 epanet v2 中没有,是我们自己定制的 +# 所以需要将这些 section 从 section_name 中移除 +section_names_for_epanetv2 = [TITLE, JUNCTIONS, RESERVOIRS, TANKS, PIPES, + PUMPS, VALVES, TAGS, DEMANDS, STATUS, + PATTERNS, CURVES, CONTROLS, RULES, ENERGY, + EMITTERS, QUALITY, SOURCES, REACTIONS, MIXING, + TIMES, REPORT, OPTIONS, COORDINATES, VERTICES, + LABELS, BACKDROP, END] \ No newline at end of file diff --git a/app/native/api/__init__.cp312-win_amd64.pyd b/app/native/api_encap/__init__.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/__init__.cp312-win_amd64.pyd rename to app/native/api_encap/__init__.cp312-win_amd64.pyd diff --git a/app/native/api/__init__.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/__init__.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/__init__.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/__init__.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/batch_api.cp312-win_amd64.pyd b/app/native/api_encap/batch_api.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/batch_api.cp312-win_amd64.pyd rename to app/native/api_encap/batch_api.cp312-win_amd64.pyd diff --git a/app/native/api/batch_api.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/batch_api.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/batch_api.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/batch_api.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/batch_api_cs.cp312-win_amd64.pyd b/app/native/api_encap/batch_api_cs.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/batch_api_cs.cp312-win_amd64.pyd rename to app/native/api_encap/batch_api_cs.cp312-win_amd64.pyd diff --git a/app/native/api/batch_api_cs.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/batch_api_cs.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/batch_api_cs.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/batch_api_cs.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/batch_exe.cp312-win_amd64.pyd b/app/native/api_encap/batch_exe.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/batch_exe.cp312-win_amd64.pyd rename to app/native/api_encap/batch_exe.cp312-win_amd64.pyd diff --git a/app/native/api/batch_exe.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/batch_exe.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/batch_exe.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/batch_exe.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/clean_api.cp312-win_amd64.pyd b/app/native/api_encap/clean_api.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/clean_api.cp312-win_amd64.pyd rename to app/native/api_encap/clean_api.cp312-win_amd64.pyd diff --git a/app/native/api/clean_api.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/clean_api.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/clean_api.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/clean_api.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/connection.cp312-win_amd64.pyd b/app/native/api_encap/connection.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/connection.cp312-win_amd64.pyd rename to app/native/api_encap/connection.cp312-win_amd64.pyd diff --git a/app/native/api/connection.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/connection.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/connection.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/connection.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/database.cp312-win_amd64.pyd b/app/native/api_encap/database.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/database.cp312-win_amd64.pyd rename to app/native/api_encap/database.cp312-win_amd64.pyd diff --git a/app/native/api/database.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/database.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/database.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/database.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/extension_data.cp312-win_amd64.pyd b/app/native/api_encap/extension_data.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/extension_data.cp312-win_amd64.pyd rename to app/native/api_encap/extension_data.cp312-win_amd64.pyd diff --git a/app/native/api/extension_data.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/extension_data.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/extension_data.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/extension_data.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/inp_in.cp312-win_amd64.pyd b/app/native/api_encap/inp_in.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/inp_in.cp312-win_amd64.pyd rename to app/native/api_encap/inp_in.cp312-win_amd64.pyd diff --git a/app/native/api/inp_in.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/inp_in.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/inp_in.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/inp_in.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/inp_out.cp312-win_amd64.pyd b/app/native/api_encap/inp_out.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/inp_out.cp312-win_amd64.pyd rename to app/native/api_encap/inp_out.cp312-win_amd64.pyd diff --git a/app/native/api/inp_out.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/inp_out.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/inp_out.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/inp_out.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/postgresql_info.cp312-win_amd64.pyd b/app/native/api_encap/postgresql_info.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/postgresql_info.cp312-win_amd64.pyd rename to app/native/api_encap/postgresql_info.cp312-win_amd64.pyd diff --git a/app/native/api/postgresql_info.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/postgresql_info.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/postgresql_info.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/postgresql_info.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/project.cp312-win_amd64.pyd b/app/native/api_encap/project.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/project.cp312-win_amd64.pyd rename to app/native/api_encap/project.cp312-win_amd64.pyd diff --git a/app/native/api/project.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/project.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/project.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/project.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s0_base.cp312-win_amd64.pyd b/app/native/api_encap/s0_base.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s0_base.cp312-win_amd64.pyd rename to app/native/api_encap/s0_base.cp312-win_amd64.pyd diff --git a/app/native/api/s0_base.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s0_base.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s0_base.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s0_base.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s10_status.cp312-win_amd64.pyd b/app/native/api_encap/s10_status.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s10_status.cp312-win_amd64.pyd rename to app/native/api_encap/s10_status.cp312-win_amd64.pyd diff --git a/app/native/api/s10_status.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s10_status.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s10_status.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s10_status.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s11_patterns.cp312-win_amd64.pyd b/app/native/api_encap/s11_patterns.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s11_patterns.cp312-win_amd64.pyd rename to app/native/api_encap/s11_patterns.cp312-win_amd64.pyd diff --git a/app/native/api/s11_patterns.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s11_patterns.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s11_patterns.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s11_patterns.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s12_curves.cp312-win_amd64.pyd b/app/native/api_encap/s12_curves.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s12_curves.cp312-win_amd64.pyd rename to app/native/api_encap/s12_curves.cp312-win_amd64.pyd diff --git a/app/native/api/s12_curves.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s12_curves.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s12_curves.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s12_curves.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s13_controls.cp312-win_amd64.pyd b/app/native/api_encap/s13_controls.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s13_controls.cp312-win_amd64.pyd rename to app/native/api_encap/s13_controls.cp312-win_amd64.pyd diff --git a/app/native/api/s13_controls.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s13_controls.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s13_controls.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s13_controls.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s14_rules.cp312-win_amd64.pyd b/app/native/api_encap/s14_rules.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s14_rules.cp312-win_amd64.pyd rename to app/native/api_encap/s14_rules.cp312-win_amd64.pyd diff --git a/app/native/api/s14_rules.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s14_rules.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s14_rules.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s14_rules.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s15_energy.cp312-win_amd64.pyd b/app/native/api_encap/s15_energy.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s15_energy.cp312-win_amd64.pyd rename to app/native/api_encap/s15_energy.cp312-win_amd64.pyd diff --git a/app/native/api/s15_energy.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s15_energy.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s15_energy.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s15_energy.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s16_emitters.cp312-win_amd64.pyd b/app/native/api_encap/s16_emitters.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s16_emitters.cp312-win_amd64.pyd rename to app/native/api_encap/s16_emitters.cp312-win_amd64.pyd diff --git a/app/native/api/s16_emitters.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s16_emitters.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s16_emitters.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s16_emitters.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s17_quality.cp312-win_amd64.pyd b/app/native/api_encap/s17_quality.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s17_quality.cp312-win_amd64.pyd rename to app/native/api_encap/s17_quality.cp312-win_amd64.pyd diff --git a/app/native/api/s17_quality.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s17_quality.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s17_quality.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s17_quality.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s18_sources.cp312-win_amd64.pyd b/app/native/api_encap/s18_sources.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s18_sources.cp312-win_amd64.pyd rename to app/native/api_encap/s18_sources.cp312-win_amd64.pyd diff --git a/app/native/api/s18_sources.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s18_sources.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s18_sources.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s18_sources.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s19_reactions.cp312-win_amd64.pyd b/app/native/api_encap/s19_reactions.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s19_reactions.cp312-win_amd64.pyd rename to app/native/api_encap/s19_reactions.cp312-win_amd64.pyd diff --git a/app/native/api/s19_reactions.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s19_reactions.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s19_reactions.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s19_reactions.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s1_title.cp312-win_amd64.pyd b/app/native/api_encap/s1_title.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s1_title.cp312-win_amd64.pyd rename to app/native/api_encap/s1_title.cp312-win_amd64.pyd diff --git a/app/native/api/s1_title.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s1_title.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s1_title.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s1_title.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s20_mixing.cp312-win_amd64.pyd b/app/native/api_encap/s20_mixing.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s20_mixing.cp312-win_amd64.pyd rename to app/native/api_encap/s20_mixing.cp312-win_amd64.pyd diff --git a/app/native/api/s20_mixing.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s20_mixing.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s20_mixing.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s20_mixing.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s21_times.cp312-win_amd64.pyd b/app/native/api_encap/s21_times.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s21_times.cp312-win_amd64.pyd rename to app/native/api_encap/s21_times.cp312-win_amd64.pyd diff --git a/app/native/api/s21_times.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s21_times.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s21_times.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s21_times.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s22_report.cp312-win_amd64.pyd b/app/native/api_encap/s22_report.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s22_report.cp312-win_amd64.pyd rename to app/native/api_encap/s22_report.cp312-win_amd64.pyd diff --git a/app/native/api/s22_report.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s22_report.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s22_report.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s22_report.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s23_options.cp312-win_amd64.pyd b/app/native/api_encap/s23_options.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s23_options.cp312-win_amd64.pyd rename to app/native/api_encap/s23_options.cp312-win_amd64.pyd diff --git a/app/native/api/s23_options.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s23_options.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s23_options.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s23_options.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s23_options_util.cp312-win_amd64.pyd b/app/native/api_encap/s23_options_util.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s23_options_util.cp312-win_amd64.pyd rename to app/native/api_encap/s23_options_util.cp312-win_amd64.pyd diff --git a/app/native/api/s23_options_util.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s23_options_util.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s23_options_util.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s23_options_util.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s23_options_v3.cp312-win_amd64.pyd b/app/native/api_encap/s23_options_v3.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s23_options_v3.cp312-win_amd64.pyd rename to app/native/api_encap/s23_options_v3.cp312-win_amd64.pyd diff --git a/app/native/api/s23_options_v3.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s23_options_v3.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s23_options_v3.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s23_options_v3.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s24_coordinates.cp312-win_amd64.pyd b/app/native/api_encap/s24_coordinates.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s24_coordinates.cp312-win_amd64.pyd rename to app/native/api_encap/s24_coordinates.cp312-win_amd64.pyd diff --git a/app/native/api/s24_coordinates.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s24_coordinates.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s24_coordinates.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s24_coordinates.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s25_vertices.cp312-win_amd64.pyd b/app/native/api_encap/s25_vertices.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s25_vertices.cp312-win_amd64.pyd rename to app/native/api_encap/s25_vertices.cp312-win_amd64.pyd diff --git a/app/native/api/s25_vertices.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s25_vertices.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s25_vertices.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s25_vertices.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s26_labels.cp312-win_amd64.pyd b/app/native/api_encap/s26_labels.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s26_labels.cp312-win_amd64.pyd rename to app/native/api_encap/s26_labels.cp312-win_amd64.pyd diff --git a/app/native/api/s26_labels.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s26_labels.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s26_labels.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s26_labels.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s27_backdrop.cp312-win_amd64.pyd b/app/native/api_encap/s27_backdrop.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s27_backdrop.cp312-win_amd64.pyd rename to app/native/api_encap/s27_backdrop.cp312-win_amd64.pyd diff --git a/app/native/api/s27_backdrop.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s27_backdrop.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s27_backdrop.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s27_backdrop.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s28_end.cp312-win_amd64.pyd b/app/native/api_encap/s28_end.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s28_end.cp312-win_amd64.pyd rename to app/native/api_encap/s28_end.cp312-win_amd64.pyd diff --git a/app/native/api/s28_end.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s28_end.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s28_end.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s28_end.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s29_scada_device.cp312-win_amd64.pyd b/app/native/api_encap/s29_scada_device.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s29_scada_device.cp312-win_amd64.pyd rename to app/native/api_encap/s29_scada_device.cp312-win_amd64.pyd diff --git a/app/native/api/s29_scada_device.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s29_scada_device.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s29_scada_device.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s29_scada_device.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s2_junctions.cp312-win_amd64.pyd b/app/native/api_encap/s2_junctions.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s2_junctions.cp312-win_amd64.pyd rename to app/native/api_encap/s2_junctions.cp312-win_amd64.pyd diff --git a/app/native/api/s2_junctions.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s2_junctions.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s2_junctions.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s2_junctions.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s30_scada_device_data.cp312-win_amd64.pyd b/app/native/api_encap/s30_scada_device_data.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s30_scada_device_data.cp312-win_amd64.pyd rename to app/native/api_encap/s30_scada_device_data.cp312-win_amd64.pyd diff --git a/app/native/api/s30_scada_device_data.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s30_scada_device_data.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s30_scada_device_data.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s30_scada_device_data.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s31_scada_element.cp312-win_amd64.pyd b/app/native/api_encap/s31_scada_element.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s31_scada_element.cp312-win_amd64.pyd rename to app/native/api_encap/s31_scada_element.cp312-win_amd64.pyd diff --git a/app/native/api/s31_scada_element.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s31_scada_element.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s31_scada_element.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s31_scada_element.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s32_region.cp312-win_amd64.pyd b/app/native/api_encap/s32_region.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s32_region.cp312-win_amd64.pyd rename to app/native/api_encap/s32_region.cp312-win_amd64.pyd diff --git a/app/native/api/s32_region.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s32_region.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s32_region.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s32_region.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s32_region_util.cp312-win_amd64.pyd b/app/native/api_encap/s32_region_util.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s32_region_util.cp312-win_amd64.pyd rename to app/native/api_encap/s32_region_util.cp312-win_amd64.pyd diff --git a/app/native/api/s32_region_util.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s32_region_util.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s32_region_util.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s32_region_util.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s33_dma.cp312-win_amd64.pyd b/app/native/api_encap/s33_dma.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s33_dma.cp312-win_amd64.pyd rename to app/native/api_encap/s33_dma.cp312-win_amd64.pyd diff --git a/app/native/api/s33_dma.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s33_dma.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s33_dma.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s33_dma.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s33_dma_cal.cp312-win_amd64.pyd b/app/native/api_encap/s33_dma_cal.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s33_dma_cal.cp312-win_amd64.pyd rename to app/native/api_encap/s33_dma_cal.cp312-win_amd64.pyd diff --git a/app/native/api/s33_dma_cal.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s33_dma_cal.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s33_dma_cal.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s33_dma_cal.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s33_dma_gen.cp312-win_amd64.pyd b/app/native/api_encap/s33_dma_gen.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s33_dma_gen.cp312-win_amd64.pyd rename to app/native/api_encap/s33_dma_gen.cp312-win_amd64.pyd diff --git a/app/native/api/s33_dma_gen.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s33_dma_gen.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s33_dma_gen.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s33_dma_gen.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s34_sa.cp312-win_amd64.pyd b/app/native/api_encap/s34_sa.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s34_sa.cp312-win_amd64.pyd rename to app/native/api_encap/s34_sa.cp312-win_amd64.pyd diff --git a/app/native/api/s34_sa.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s34_sa.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s34_sa.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s34_sa.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s34_sa_cal.cp312-win_amd64.pyd b/app/native/api_encap/s34_sa_cal.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s34_sa_cal.cp312-win_amd64.pyd rename to app/native/api_encap/s34_sa_cal.cp312-win_amd64.pyd diff --git a/app/native/api/s34_sa_cal.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s34_sa_cal.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s34_sa_cal.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s34_sa_cal.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s34_sa_gen.cp312-win_amd64.pyd b/app/native/api_encap/s34_sa_gen.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s34_sa_gen.cp312-win_amd64.pyd rename to app/native/api_encap/s34_sa_gen.cp312-win_amd64.pyd diff --git a/app/native/api/s34_sa_gen.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s34_sa_gen.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s34_sa_gen.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s34_sa_gen.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s35_vd.cp312-win_amd64.pyd b/app/native/api_encap/s35_vd.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s35_vd.cp312-win_amd64.pyd rename to app/native/api_encap/s35_vd.cp312-win_amd64.pyd diff --git a/app/native/api/s35_vd.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s35_vd.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s35_vd.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s35_vd.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s35_vd_cal.cp312-win_amd64.pyd b/app/native/api_encap/s35_vd_cal.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s35_vd_cal.cp312-win_amd64.pyd rename to app/native/api_encap/s35_vd_cal.cp312-win_amd64.pyd diff --git a/app/native/api/s35_vd_cal.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s35_vd_cal.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s35_vd_cal.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s35_vd_cal.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s35_vd_gen.cp312-win_amd64.pyd b/app/native/api_encap/s35_vd_gen.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s35_vd_gen.cp312-win_amd64.pyd rename to app/native/api_encap/s35_vd_gen.cp312-win_amd64.pyd diff --git a/app/native/api/s35_vd_gen.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s35_vd_gen.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s35_vd_gen.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s35_vd_gen.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s36_wda.cp312-win_amd64.pyd b/app/native/api_encap/s36_wda.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s36_wda.cp312-win_amd64.pyd rename to app/native/api_encap/s36_wda.cp312-win_amd64.pyd diff --git a/app/native/api/s36_wda.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s36_wda.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s36_wda.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s36_wda.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s36_wda_cal.cp312-win_amd64.pyd b/app/native/api_encap/s36_wda_cal.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s36_wda_cal.cp312-win_amd64.pyd rename to app/native/api_encap/s36_wda_cal.cp312-win_amd64.pyd diff --git a/app/native/api/s36_wda_cal.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s36_wda_cal.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s36_wda_cal.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s36_wda_cal.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s38_scada_info.cp312-win_amd64.pyd b/app/native/api_encap/s38_scada_info.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s38_scada_info.cp312-win_amd64.pyd rename to app/native/api_encap/s38_scada_info.cp312-win_amd64.pyd diff --git a/app/native/api/s38_scada_info.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s38_scada_info.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s38_scada_info.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s38_scada_info.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s39_user.cp312-win_amd64.pyd b/app/native/api_encap/s39_user.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s39_user.cp312-win_amd64.pyd rename to app/native/api_encap/s39_user.cp312-win_amd64.pyd diff --git a/app/native/api/s39_user.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s39_user.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s39_user.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s39_user.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s3_reservoirs.cp312-win_amd64.pyd b/app/native/api_encap/s3_reservoirs.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s3_reservoirs.cp312-win_amd64.pyd rename to app/native/api_encap/s3_reservoirs.cp312-win_amd64.pyd diff --git a/app/native/api/s3_reservoirs.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s3_reservoirs.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s3_reservoirs.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s3_reservoirs.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s40_schema.cp312-win_amd64.pyd b/app/native/api_encap/s40_schema.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s40_schema.cp312-win_amd64.pyd rename to app/native/api_encap/s40_schema.cp312-win_amd64.pyd diff --git a/app/native/api/s40_schema.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s40_schema.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s40_schema.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s40_schema.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s41_pipe_risk_probability.cp312-win_amd64.pyd b/app/native/api_encap/s41_pipe_risk_probability.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s41_pipe_risk_probability.cp312-win_amd64.pyd rename to app/native/api_encap/s41_pipe_risk_probability.cp312-win_amd64.pyd diff --git a/app/native/api/s41_pipe_risk_probability.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s41_pipe_risk_probability.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s41_pipe_risk_probability.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s41_pipe_risk_probability.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s42_sensor_placement.cp312-win_amd64.pyd b/app/native/api_encap/s42_sensor_placement.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s42_sensor_placement.cp312-win_amd64.pyd rename to app/native/api_encap/s42_sensor_placement.cp312-win_amd64.pyd diff --git a/app/native/api/s42_sensor_placement.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s42_sensor_placement.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s42_sensor_placement.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s42_sensor_placement.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s43_burst_locate_result.cp312-win_amd64.pyd b/app/native/api_encap/s43_burst_locate_result.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s43_burst_locate_result.cp312-win_amd64.pyd rename to app/native/api_encap/s43_burst_locate_result.cp312-win_amd64.pyd diff --git a/app/native/api/s43_burst_locate_result.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s43_burst_locate_result.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s43_burst_locate_result.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s43_burst_locate_result.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s4_tanks.cp312-win_amd64.pyd b/app/native/api_encap/s4_tanks.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s4_tanks.cp312-win_amd64.pyd rename to app/native/api_encap/s4_tanks.cp312-win_amd64.pyd diff --git a/app/native/api/s4_tanks.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s4_tanks.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s4_tanks.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s4_tanks.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s5_pipes.cp312-win_amd64.pyd b/app/native/api_encap/s5_pipes.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s5_pipes.cp312-win_amd64.pyd rename to app/native/api_encap/s5_pipes.cp312-win_amd64.pyd diff --git a/app/native/api/s5_pipes.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s5_pipes.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s5_pipes.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s5_pipes.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s6_pumps.cp312-win_amd64.pyd b/app/native/api_encap/s6_pumps.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s6_pumps.cp312-win_amd64.pyd rename to app/native/api_encap/s6_pumps.cp312-win_amd64.pyd diff --git a/app/native/api/s6_pumps.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s6_pumps.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s6_pumps.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s6_pumps.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s7_valves.cp312-win_amd64.pyd b/app/native/api_encap/s7_valves.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s7_valves.cp312-win_amd64.pyd rename to app/native/api_encap/s7_valves.cp312-win_amd64.pyd diff --git a/app/native/api/s7_valves.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s7_valves.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s7_valves.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s7_valves.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s8_tags.cp312-win_amd64.pyd b/app/native/api_encap/s8_tags.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s8_tags.cp312-win_amd64.pyd rename to app/native/api_encap/s8_tags.cp312-win_amd64.pyd diff --git a/app/native/api/s8_tags.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s8_tags.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s8_tags.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s8_tags.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/s9_demands.cp312-win_amd64.pyd b/app/native/api_encap/s9_demands.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/s9_demands.cp312-win_amd64.pyd rename to app/native/api_encap/s9_demands.cp312-win_amd64.pyd diff --git a/app/native/api/s9_demands.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/s9_demands.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/s9_demands.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/s9_demands.cpython-312-x86_64-linux-gnu.so diff --git a/app/native/api/sections.cp312-win_amd64.pyd b/app/native/api_encap/sections.cp312-win_amd64.pyd similarity index 100% rename from app/native/api/sections.cp312-win_amd64.pyd rename to app/native/api_encap/sections.cp312-win_amd64.pyd diff --git a/app/native/api/sections.cpython-312-x86_64-linux-gnu.so b/app/native/api_encap/sections.cpython-312-x86_64-linux-gnu.so similarity index 100% rename from app/native/api/sections.cpython-312-x86_64-linux-gnu.so rename to app/native/api_encap/sections.cpython-312-x86_64-linux-gnu.so diff --git a/app/services/epanet/epanet.py b/app/services/epanet/epanet.py index 6c8943a..633d54a 100644 --- a/app/services/epanet/epanet.py +++ b/app/services/epanet/epanet.py @@ -10,8 +10,8 @@ import logging from typing import Any sys.path.append("..") -from api import project -from api import inp_out +from app.native.api import project +from app.native.api import inp_out def _verify_platform(): diff --git a/app/services/simulation.py b/app/services/simulation.py index c7eca9a..b922e8f 100644 --- a/app/services/simulation.py +++ b/app/services/simulation.py @@ -1233,9 +1233,14 @@ def run_simulation( # print(node_result) # 存储 starttime = time.time() + # 临时处理输入的name问题,后续需要优化,需要传递/获取iot数据库的名字 + if name.find("_") != -1: + db_name = name.split("_")[2] + else: + db_name = name if simulation_type.upper() == "REALTIME": TimescaleInternalStorage.store_realtime_simulation( - node_result, link_result, modify_pattern_start_time, db_name=name + node_result, link_result, modify_pattern_start_time, db_name=db_name ) elif simulation_type.upper() == "EXTENDED": TimescaleInternalStorage.store_scheme_simulation( @@ -1245,7 +1250,7 @@ def run_simulation( link_result, modify_pattern_start_time, num_periods_result, - db_name=name, + db_name=db_name, ) endtime = time.time() logging.info("store time: %f", endtime - starttime) diff --git a/scripts/run_server.py b/scripts/run_server.py index 79db060..70416aa 100644 --- a/scripts/run_server.py +++ b/scripts/run_server.py @@ -16,6 +16,6 @@ if __name__ == "__main__": "app.main:app", host="0.0.0.0", port=8000, - workers=4, # 这里可以设置多进程 + # workers=4, # 这里可以设置多进程 loop="asyncio", )