diff --git a/app/native/wndb/database.py b/app/native/wndb/database.py index c043d03..248b4a3 100644 --- a/app/native/wndb/database.py +++ b/app/native/wndb/database.py @@ -136,18 +136,20 @@ def execute_undo(name: str, discard: bool = False) -> ChangeSet: write(name, row['undo']) + parent = row['parent'] if row['parent'] != None else 0 + # update foreign key - write(name, f"update current_operation set id = {row['parent']} where id = {row['id']}") + write(name, f"update current_operation set id = {parent} where id = {row['id']}") if discard: # update foreign key - write(name, f"update operation set redo_child = null where id = {row['parent']}") + write(name, f"update operation set redo_child = null where id = {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']}") + write(name, f"update operation set redo_child = {row['id']} where id = {parent}") - e = eval(row['undo_cs']) + e = eval(row['undo_cs']) if row['undo_cs'] not in [None, ''] else [] return ChangeSet.from_list(e) @@ -159,9 +161,10 @@ def execute_redo(name: str) -> 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']}") + parent = row['parent'] if row['parent'] != None else 0 + write(name, f"update current_operation set id = {row['id']} where id = {parent}") - e = eval(row['redo_cs']) + e = eval(row['redo_cs']) if row['redo_cs'] not in [None, ''] else [] return ChangeSet.from_list(e) diff --git a/app/native/wndb/s29_scada_device.py b/app/native/wndb/s29_scada_device.py index 7af8ee0..ec2f139 100644 --- a/app/native/wndb/s29_scada_device.py +++ b/app/native/wndb/s29_scada_device.py @@ -72,7 +72,7 @@ def _set_scada_device(name: str, cs: ChangeSet) -> DbChangeSet: 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) + return execute_command(name, _set_scada_device(name, cs)) def _add_scada_device(name: str, cs: ChangeSet) -> DbChangeSet: @@ -90,7 +90,7 @@ def _add_scada_device(name: str, cs: ChangeSet) -> DbChangeSet: 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) + return execute_command(name, _add_scada_device(name, cs)) def _delete_scada_device(name: str, cs: ChangeSet) -> DbChangeSet: @@ -108,7 +108,7 @@ def _delete_scada_device(name: str, cs: ChangeSet) -> DbChangeSet: 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) + return execute_command(name, _delete_scada_device(name, cs)) def get_all_scada_device_ids(name: str) -> list[str]: diff --git a/app/native/wndb/s30_scada_device_data.py b/app/native/wndb/s30_scada_device_data.py index d1a6043..5f23800 100644 --- a/app/native/wndb/s30_scada_device_data.py +++ b/app/native/wndb/s30_scada_device_data.py @@ -45,7 +45,7 @@ def _set_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet: def set_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, _set_scada_device_data(name, cs), False) + return execute_command(name, _set_scada_device_data(name, cs)) def _add_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet: @@ -66,7 +66,7 @@ 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) + return execute_command(name, _add_scada_device_data(name, cs)) def _delete_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet: @@ -87,4 +87,4 @@ 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) + return execute_command(name, _delete_scada_device_data(name, cs)) diff --git a/app/native/wndb/s32_region_util.py b/app/native/wndb/s32_region_util.py index 4fdb46d..f9c5e88 100644 --- a/app/native/wndb/s32_region_util.py +++ b/app/native/wndb/s32_region_util.py @@ -1,8 +1,8 @@ -import ctypes import platform import os import math from typing import Any +import pyclipper 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 @@ -414,40 +414,20 @@ def inflate_boundary(name: str, boundary: list[tuple[float, float]], delta: floa if boundary[0] == boundary[-1]: del(boundary[-1]) - lib = ctypes.CDLL(os.path.join(os.getcwd(), 'api', 'CClipper2.dll')) + precision = 2 + scale = 10 ** precision + path = [(round(x * scale), round(y * scale)) for x, y in boundary] - 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)) + offset = pyclipper.PyclipperOffset(miter_limit=2.0) + offset.AddPath(path, pyclipper.JT_SQUARE, pyclipper.ET_CLOSEDPOLYGON) + solutions = offset.Execute(round(delta * scale)) + if len(solutions) == 0: 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])) + for x, y in solutions[0]: + result.append((x / scale, y / scale)) result.append(result[0]) - - lib.free_paths(ctypes.byref(c_out_path)) return result diff --git a/app/native/wndb/s33_dma_cal.py b/app/native/wndb/s33_dma_cal.py index e2d7ae9..5da2ea6 100644 --- a/app/native/wndb/s33_dma_cal.py +++ b/app/native/wndb/s33_dma_cal.py @@ -32,6 +32,13 @@ 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]]: + if part_type != PARTITION_TYPE_RB and part_type != PARTITION_TYPE_KWAY: + return [] + if part_count <= 0: + return [] + elif part_count == 1: + return [nodes] + topology = Topology(name, nodes) t_nodes = topology.nodes() t_links = topology.links() @@ -52,7 +59,16 @@ def calculate_district_metering_area_for_nodes(name: str, nodes: list[str], part 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) + options = pymetis.Options() + options.set_defaults() + options._set(pymetis.OptionKey.CONTIG, 1) + options._set(pymetis.OptionKey.SEED, 0) + n_cuts, membership = pymetis.part_graph( + nparts=part_count, + adjacency=adjacency_list, + recursive=recursive, + options=options, + ) result: list[list[str]] = [] for i in range(0, part_count): diff --git a/app/native/wndb/s34_sa_cal.py b/app/native/wndb/s34_sa_cal.py index 025b4c1..158a350 100644 --- a/app/native/wndb/s34_sa_cal.py +++ b/app/native/wndb/s34_sa_cal.py @@ -1,97 +1,96 @@ import os -import ctypes -from .project 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 +import platform +import subprocess +import uuid from queue import Queue -from .database import * -from .s0_base import get_node_links, get_link_nodes +from typing import Any -sys.path.append('..') -from app.infra.epanet.epanet import run_project +from app.infra.epanet.epanet import Output -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']: +from .inp_out import dump_inp +from .project import have_project +from .s0_base import get_link_nodes, get_node_links +from .s23_options_util import get_option_v3 + + +def _update_section(lines: list[str], section: str, transform) -> list[str]: + result: list[str] = [] + i = 0 + while i < len(lines): + line = lines[i] + if line.strip() == f'[{section}]': + result.append(line) + i += 1 + section_lines: list[str] = [] + while i < len(lines) and not lines[i].startswith('['): + section_lines.append(lines[i]) + i += 1 + result.extend(transform(section_lines)) + continue + result.append(line) + i += 1 + return result + + +def _build_service_area_input(name: str, inp_path: str) -> None: + dump_inp(name, inp_path, '2') + + with open(inp_path, encoding='utf-8') as file: + lines = file.read().splitlines() + + unbalanced = get_option_v3(name).get('IF_UNBALANCED', '').strip() + if unbalanced != '': + lines = _update_section( + lines, + 'OPTIONS', + lambda option_lines: [ + f'UNBALANCED {unbalanced}' if line.startswith('UNBALANCED ') else line + for line in option_lines + ], + ) + + with open(inp_path, mode='w', encoding='utf-8') as file: + file.write('\n'.join(lines) + '\n') + + +def _run_epanet_output(inp_path: str, rpt_path: str, out_path: str) -> dict[str, Any]: + epanet_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'infra', 'epanet')) + if platform.system() == 'Windows': + exe = os.path.join(epanet_dir, 'windows', 'runepanet.exe') + else: + exe = os.path.join(epanet_dir, 'linux', 'runepanet') + if not os.access(exe, os.X_OK): + os.chmod(exe, 0o755) + + env = os.environ.copy() + if platform.system() == 'Linux': + lib_dir = os.path.dirname(exe) + env['LD_LIBRARY_PATH'] = f"{lib_dir}:{env.get('LD_LIBRARY_PATH', '')}" + + process = subprocess.run([exe, inp_path, rpt_path, out_path], env=env, capture_output=True, text=True) + if process.returncode != 0: + raise RuntimeError( + f'EPANET failed for [{inp_path}] with code {process.returncode}: ' + f'stdout={process.stdout} stderr={process.stderr}' + ) + + return Output(out_path).dump() + + +def _calculate_service_area(name: str, output: dict[str, Any], time_index: int) -> dict[str, list[str]]: + sources: dict[str, list[str]] = {} + for node_result in output['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']: + for link_result in output['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: Queue[str] = Queue() queue.put(source) while not queue.empty(): @@ -107,9 +106,6 @@ def _calculate_service_area(name: str, inp, time_index: int = 0) -> dict[str, li 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(): @@ -120,17 +116,15 @@ def _calculate_service_area(name: str, inp, time_index: int = 0) -> dict[str, li 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 + for source in concentrations.keys(): + concentration_map[node][source] = 1.0 - node_upstream : dict[str, list[tuple[str, str]]] = {} + node_upstream: dict[str, list[tuple[str, str]]] = {} for node in node_wip: - if node not in node_upstream: - node_upstream[node] = [] + node_upstream[node] = [] links = get_node_links(name, node) for link in links: @@ -141,7 +135,7 @@ def _calculate_service_area(name: str, inp, time_index: int = 0) -> dict[str, li node_upstream[node].append((link, node2)) while len(node_wip) != 0: - done = [] + done: list[str] = [] for node in node_wip: up_link_nodes = node_upstream[node] ready = True @@ -149,33 +143,38 @@ def _calculate_service_area(name: str, inp, time_index: int = 0) -> dict[str, li 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]]) + if not ready: + continue - # 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 + for link, upstream_node in up_link_nodes: + if upstream_node not in concentration_map: + continue + for source, concentration in concentration_map[upstream_node].items(): + concentration_map[node][source] += concentration * abs(link_flows[link]) - done.append(node) + total_concentration = sum(concentration_map[node].values()) + if total_concentration == 0: + raise RuntimeError(f'Failed to normalize service area concentration for node [{node}] at time [{time_index}]') + + for source in concentration_map[node].keys(): + concentration_map[node][source] /= total_concentration + + done.append(node) + + if len(done) == 0: + raise RuntimeError(f'Failed to resolve service area graph for time [{time_index}]') for node in done: node_wip.remove(node) source_to_main_node: dict[str, list[str]] = {} - for node, value in concentration_map.items(): + for node, concentrations 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 + for source, concentration in concentrations.items(): + if concentration > max_concentration: + max_concentration = concentration + max_source = source if max_source not in source_to_main_node: source_to_main_node[max_source] = [] source_to_main_node[max_source].append(node) @@ -184,15 +183,28 @@ def _calculate_service_area(name: str, inp, time_index: int = 0) -> dict[str, li def calculate_service_area(name: str) -> list[dict[str, list[str]]]: - inp = json.loads(run_project(name, True)) + if not have_project(name): + raise Exception(f'Not found project [{name}]') - result: list[dict[str, list[str]]] = [] + root = os.path.abspath(os.getcwd()) + token = f'{os.getpid()}_{uuid.uuid4().hex}' + inp_path = os.path.join(root, 'db_inp', f'{name}.service_area.{token}.inp') + rpt_path = os.path.join(root, 'temp', f'{name}.service_area.{token}.rpt') + out_path = os.path.join(root, 'temp', f'{name}.service_area.{token}.opt') - time_count = len(inp['node_results'][0]['result']) + os.makedirs(os.path.dirname(inp_path), exist_ok=True) + os.makedirs(os.path.dirname(rpt_path), exist_ok=True) - for i in range(time_count): - sas = _calculate_service_area(name, inp, i) - result.append(sas) + try: + _build_service_area_input(name, inp_path) + output = _run_epanet_output(inp_path, rpt_path, out_path) - return result -''' + results: list[dict[str, list[str]]] = [] + time_count = len(output['node_results'][0]['result']) + for time_index in range(time_count): + results.append(_calculate_service_area(name, output, time_index)) + return results + finally: + for path in (inp_path, rpt_path, out_path): + if os.path.exists(path): + os.remove(path)