From e0b6ce75596bc42a200b80da0c61125ecbb8b8fe Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Fri, 31 Mar 2023 09:55:06 +0800 Subject: [PATCH 01/40] Start region work --- api/__init__.py | 2 + api/s24_coordinates.py | 2 +- api/s32_virtual_district.py | 65 +++++++++++++++++++++++ script/sql/create/24.coordinates.sql | 3 +- script/sql/create/32.virtual_district.sql | 9 ++++ script/sql/drop/24.coordinates.sql | 2 - script/sql/drop/32.virtual_district.sql | 3 ++ tjnetwork.py | 8 +++ 8 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 api/s32_virtual_district.py create mode 100644 script/sql/create/32.virtual_district.sql create mode 100644 script/sql/drop/32.virtual_district.sql diff --git a/api/__init__.py b/api/__init__.py index 02f4cc8..1b04041 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -130,3 +130,5 @@ from .del_cmd import clean_scada_device_data from .s31_scada_element import SCADA_ELEMENT_STATUS_OFFLINE, SCADA_ELEMENT_STATUS_ONLINE from .s31_scada_element import get_scada_element_schema, get_scada_elements, get_scada_element, set_scada_element, add_scada_element, delete_scada_element from .del_cmd import clean_scada_element + +from .s32_virtual_district import generate_virtual_district \ No newline at end of file diff --git a/api/s24_coordinates.py b/api/s24_coordinates.py index 6c54245..03952af 100644 --- a/api/s24_coordinates.py +++ b/api/s24_coordinates.py @@ -24,7 +24,7 @@ def get_node_coord(name: str, id: str) -> dict[str, float]: def inp_in_coord(line: str) -> str: tokens = line.split() node = tokens[0] - coord = f"'({tokens[1]}, {tokens[2]})'" + coord = f"st_geomfromtext('point({tokens[1]} {tokens[2]})')" return f"insert into coordinates (node, coord) values ('{node}', {coord});" diff --git a/api/s32_virtual_district.py b/api/s32_virtual_district.py new file mode 100644 index 0000000..a166815 --- /dev/null +++ b/api/s32_virtual_district.py @@ -0,0 +1,65 @@ +from .database import * + + +def generate_virtual_district(name: str, centers: list[str]): + i = 0 + node_index: dict[str, int] = {} + rows = read_all(name, 'select id from _node') + for row in rows: + i += 1 + node_index[str(row['id'])] = i + + write(name, 'drop table if exists vd_graph') + write(name, 'create table vd_graph (id serial, source integer, target integer, cost numeric)') + + 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 vd_graph (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 vd_graph (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 vd_graph (source, target, cost) values ({source}, {target}, 0.0)") + + node_distance: dict[str, dict[str, Any]] = {} + for center in centers: + for node, index in node_index.items(): + if node == center: + continue + # TODO: check none + distance = float(read(name, f"select max(agg_cost) as distance from pgr_dijkstraCost('select id, source, target, cost from vd_graph', {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, 'drop table vd_graph') + + 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) + + write(name, 'delete from virtual_district') + + for center, value in center_node.items(): + write(name, f'drop table if exists vd_{center}') + write(name, f'create table vd_{center} (node varchar(32) primary key references _node(id))') + + for node in value: + write(name, f"insert into vd_{center} values ('{node}')") + + # TODO: check none + boundary = read(name, f'select st_astext(st_convexhull(st_collect(array(select coord from coordinates where node in (select * from vd_{center}))))) as boundary' )['boundary'] + write(name, f"insert into virtual_district (id, center, boundary) values ('vd_{center}', '{center}', st_geomfromtext('{boundary}'))") + + write(name, f'drop table vd_{center}') diff --git a/script/sql/create/24.coordinates.sql b/script/sql/create/24.coordinates.sql index 692bca7..945acef 100644 --- a/script/sql/create/24.coordinates.sql +++ b/script/sql/create/24.coordinates.sql @@ -3,10 +3,9 @@ create table coordinates ( node varchar(32) primary key references _node(id) -, coord point not null +, coord geometry ); -- delete when delete node -create index coordinates_spgist on coordinates using spgist(coord); create index coordinates_gist on coordinates using gist(coord); diff --git a/script/sql/create/32.virtual_district.sql b/script/sql/create/32.virtual_district.sql new file mode 100644 index 0000000..12edec0 --- /dev/null +++ b/script/sql/create/32.virtual_district.sql @@ -0,0 +1,9 @@ +create table virtual_district +( + id text primary key +, center varchar(32) references _node(id) not null unique +, boundary geometry unique +, nodes varchar(32)[] +); + +create index virtual_district_gist on virtual_district using gist(boundary); diff --git a/script/sql/drop/24.coordinates.sql b/script/sql/drop/24.coordinates.sql index b7f86b0..00c63ee 100644 --- a/script/sql/drop/24.coordinates.sql +++ b/script/sql/drop/24.coordinates.sql @@ -2,6 +2,4 @@ drop index if exists coordinates_gist; -drop index if exists coordinates_spgist; - drop table if exists coordinates; diff --git a/script/sql/drop/32.virtual_district.sql b/script/sql/drop/32.virtual_district.sql new file mode 100644 index 0000000..33921c1 --- /dev/null +++ b/script/sql/drop/32.virtual_district.sql @@ -0,0 +1,3 @@ +drop index if exists virtual_district_gist; + +drop table if exists virtual_district; diff --git a/tjnetwork.py b/tjnetwork.py index d291b6e..856d01a 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -921,3 +921,11 @@ def delete_scada_element(name: str, cs: ChangeSet) -> ChangeSet: def clean_scada_element(name: str) -> ChangeSet: return api.clean_scada_element(name) + + +############################################################ +# virtual_district 32 +############################################################ + +def generate_virtual_district(name: str, centers: list[str]): + return api.generate_virtual_district(name, centers) From e57721a29ab9db20f98acc3d596728f41e444007 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Fri, 31 Mar 2023 15:18:46 +0800 Subject: [PATCH 02/40] Add extension back --- create_template.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/create_template.py b/create_template.py index 50ff02b..8f325a9 100644 --- a/create_template.py +++ b/create_template.py @@ -33,11 +33,13 @@ sql_create = [ "script/sql/create/29.scada_device.sql", "script/sql/create/30.scada_device_data.sql", "script/sql/create/31.scada_element.sql", + "script/sql/create/32.virtual_district.sql", "script/sql/create/operation.sql" ] sql_drop = [ "script/sql/drop/operation.sql", + "script/sql/drop/32.virtual_district.sql", "script/sql/drop/31.scada_element.sql", "script/sql/drop/30.scada_device_data.sql", "script/sql/drop/29.scada_device.sql", @@ -78,6 +80,8 @@ def create_template(): cur.execute("create database project") with pg.connect(conninfo="dbname=project host=127.0.0.1") as conn: with conn.cursor() as cur: + cur.execute('create extension postgis cascade') + cur.execute('create extension pgrouting cascade') for sql in sql_create: with open(sql, "r") as f: cur.execute(f.read()) From 0ac4a882950722dc515bf1fa791c171ff68e043c Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 10:09:12 +0800 Subject: [PATCH 03/40] Replace coordinate with postgis geometry --- api/s24_coordinates.py | 28 +++++++++++++++++++++------- api/s2_junctions.py | 13 ++++++------- api/s3_reservoirs.py | 13 ++++++------- api/s4_tanks.py | 13 ++++++------- 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/api/s24_coordinates.py b/api/s24_coordinates.py index 03952af..9e73651 100644 --- a/api/s24_coordinates.py +++ b/api/s24_coordinates.py @@ -1,17 +1,31 @@ from .database import * +def sql_update_coord(node: str, x: float, y: float) -> str: + coord = f"st_geomfromtext('point({x} {y})')" + return 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 f"insert into coordinates (node, coord) values ('{node}', {coord});" + + +def sql_delete_coord(node: str) -> str: + return f"delete from coordinates where node = '{node}';" + + def _to_client_point(coord: str) -> dict[str, float]: - xy = coord.removeprefix('(').removesuffix(')').split(',') + xy = coord.lower().removeprefix('point(').removesuffix(')').split(' ') return { 'x': float(xy[0]), 'y': float(xy[1]) } -def get_node_coord(name: str, id: str) -> dict[str, float]: - row = try_read(name, f"select * from coordinates where node = '{id}'") +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, f"insert into coordinates (node, coord) values ('{id}', '(0.0,0.0)');") + write(name, sql_insert_coord(node, 0.0, 0.0)) return {'x': 0.0, 'y': 0.0} - return _to_client_point(row['coord']) + return _to_client_point(row['coord_geom']) #-------------------------------------------------------------- @@ -30,10 +44,10 @@ def inp_in_coord(line: str) -> str: def inp_out_coord(name: str) -> list[str]: lines = [] - objs = read_all(name, 'select * from coordinates') + objs = read_all(name, 'select node, st_astext(coord) as coord_geom from coordinates') for obj in objs: node = obj['node'] - coord = _to_client_point(obj['coord']) + coord = _to_client_point(obj['coord_geom']) x = coord['x'] y = coord['y'] lines.append(f'{node} {x} {y}') diff --git a/api/s2_junctions.py b/api/s2_junctions.py index cbe3c1b..a09f484 100644 --- a/api/s2_junctions.py +++ b/api/s2_junctions.py @@ -35,7 +35,6 @@ class Junction(object): self.f_type = f"'{self.type}'" self.f_id = f"'{self.id}'" - self.f_coord = f"'({self.x}, {self.y})'" self.f_elevation = self.elevation def as_dict(self) -> dict[str, Any]: @@ -57,9 +56,9 @@ def set_junction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: new = Junction(raw_new) redo_sql = f"update junctions set elevation = {new.f_elevation} where id = {new.f_id};" - redo_sql += f"\nupdate coordinates set coord = {new.f_coord} where node = {new.f_id};" + redo_sql += f"\n{sql_update_coord(new.id, new.x, new.y)}" - undo_sql = f"update coordinates set coord = {old.f_coord} where node = {old.f_id};" + 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() @@ -81,9 +80,9 @@ def add_junction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: 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"\ninsert into coordinates (node, coord) values ({new.f_id}, {new.f_coord});" + redo_sql += f"\n{sql_insert_coord(new.id, new.x, new.y)}" - undo_sql = f"delete from coordinates where node = {new.f_id};" + 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};" @@ -104,13 +103,13 @@ def add_junction(name: str, cs: ChangeSet) -> ChangeSet: def delete_junction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: old = Junction(get_junction(name, cs.operations[0]['id'])) - redo_sql = f"delete from coordinates where node = {old.f_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"\ninsert into coordinates (node, coord) values ({old.f_id}, {old.f_coord});" + 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() diff --git a/api/s3_reservoirs.py b/api/s3_reservoirs.py index d3a3d7f..bac215d 100644 --- a/api/s3_reservoirs.py +++ b/api/s3_reservoirs.py @@ -38,7 +38,6 @@ class Reservoir(object): self.f_type = f"'{self.type}'" self.f_id = f"'{self.id}'" - self.f_coord = f"'({self.x}, {self.y})'" self.f_head = self.head self.f_pattern = f"'{self.pattern}'" if self.pattern != None else 'null' @@ -61,9 +60,9 @@ def set_reservoir_cmd(name: str, cs: ChangeSet) -> DbChangeSet: 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"\nupdate coordinates set coord = {new.f_coord} where node = {new.f_id};" + redo_sql += f"\n{sql_update_coord(new.id, new.x, new.y)}" - undo_sql = f"update coordinates set coord = {old.f_coord} where node = {old.f_id};" + 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() @@ -85,9 +84,9 @@ def add_reservoir_cmd(name: str, cs: ChangeSet) -> DbChangeSet: 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"\ninsert into coordinates (node, coord) values ({new.f_id}, {new.f_coord});" + redo_sql += f"\n{sql_insert_coord(new.id, new.x, new.y)}" - undo_sql = f"delete from coordinates where node = {new.f_id};" + 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};" @@ -108,13 +107,13 @@ def add_reservoir(name: str, cs: ChangeSet) -> ChangeSet: def delete_reservoir_cmd(name: str, cs: ChangeSet) -> DbChangeSet: old = Reservoir(get_reservoir(name, cs.operations[0]['id'])) - redo_sql = f"delete from coordinates where node = {old.f_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"\ninsert into coordinates (node, coord) values ({old.f_id}, {old.f_coord});" + 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() diff --git a/api/s4_tanks.py b/api/s4_tanks.py index 45341fe..98ce2ea 100644 --- a/api/s4_tanks.py +++ b/api/s4_tanks.py @@ -60,7 +60,6 @@ class Tank(object): self.f_type = f"'{self.type}'" self.f_id = f"'{self.id}'" - self.f_coord = f"'({self.x}, {self.y})'" self.f_elevation = self.elevation self.f_init_level = self.init_level self.f_min_level = self.min_level @@ -89,9 +88,9 @@ def set_tank_cmd(name: str, cs: ChangeSet) -> DbChangeSet: 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"\nupdate coordinates set coord = {new.f_coord} where node = {new.f_id};" + redo_sql += f"\n{sql_update_coord(new.id, new.x, new.y)}" - undo_sql = f"update coordinates set coord = {old.f_coord} where node = {old.f_id};" + 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() @@ -113,9 +112,9 @@ def add_tank_cmd(name: str, cs: ChangeSet) -> DbChangeSet: 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"\ninsert into coordinates (node, coord) values ({new.f_id}, {new.f_coord});" + redo_sql += f"\n{sql_insert_coord(new.id, new.x, new.y)}" - undo_sql = f"delete from coordinates where node = {new.f_id};" + 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};" @@ -136,13 +135,13 @@ def add_tank(name: str, cs: ChangeSet) -> ChangeSet: def delete_tank_cmd(name: str, cs: ChangeSet) -> DbChangeSet: old = Tank(get_tank(name, cs.operations[0]['id'])) - redo_sql = f"delete from coordinates where node = {old.f_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"\ninsert into coordinates (node, coord) values ({old.f_id}, {old.f_coord});" + 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() From a62700d27e0fa2e84ec037dcff71f9e0f1a75112 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 11:11:05 +0800 Subject: [PATCH 04/40] Support to calculate virtual district --- api/__init__.py | 2 +- api/s32_virtual_district.py | 43 +++++++++++++++++++++++++++---------- tjnetwork.py | 4 ++-- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/api/__init__.py b/api/__init__.py index a3e264d..de43ec5 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -132,4 +132,4 @@ from .s31_scada_element import SCADA_ELEMENT_STATUS_OFFLINE, SCADA_ELEMENT_STATU from .s31_scada_element import get_scada_element_schema, get_scada_elements, get_scada_element, set_scada_element, add_scada_element, delete_scada_element from .del_cmd import clean_scada_element -from .s32_virtual_district import generate_virtual_district \ No newline at end of file +from .s32_virtual_district import calculate_virtual_district \ No newline at end of file diff --git a/api/s32_virtual_district.py b/api/s32_virtual_district.py index a166815..85b8585 100644 --- a/api/s32_virtual_district.py +++ b/api/s32_virtual_district.py @@ -1,17 +1,23 @@ from .database import * +from .s0_base import get_node_links - -def generate_virtual_district(name: str, centers: list[str]): - i = 0 - node_index: dict[str, int] = {} - rows = read_all(name, 'select id from _node') - for row in rows: - i += 1 - node_index[str(row['id'])] = i - +def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: write(name, 'drop table if exists vd_graph') write(name, 'create table vd_graph (id serial, source integer, target integer, cost numeric)') + # 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'])] @@ -29,6 +35,7 @@ def generate_virtual_district(name: str, centers: list[str]): target = node_index[str(valve['node2'])] write(name, f"insert into vd_graph (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(): @@ -41,15 +48,18 @@ def generate_virtual_district(name: str, centers: list[str]): elif distance < node_distance[node]['distance']: node_distance[node] = { 'center': center, 'distance' : distance } + # destroy topology graph write(name, 'drop table vd_graph') + # 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) - write(name, 'delete from virtual_district') + # write(name, 'delete from virtual_district') + vds: list[dict[str, Any]] = [] for center, value in center_node.items(): write(name, f'drop table if exists vd_{center}') @@ -60,6 +70,17 @@ def generate_virtual_district(name: str, centers: list[str]): # TODO: check none boundary = read(name, f'select st_astext(st_convexhull(st_collect(array(select coord from coordinates where node in (select * from vd_{center}))))) as boundary' )['boundary'] - write(name, f"insert into virtual_district (id, center, boundary) values ('vd_{center}', '{center}', st_geomfromtext('{boundary}'))") + # write(name, f"insert into virtual_district (id, center, boundary) values ('vd_{center}', '{center}', st_geomfromtext('{boundary}'))") + + outlines = str(boundary).removeprefix('POLYGON((').removesuffix('))').split(',') + outline_tuples = [] + for pt in outlines: + xy = pt.split(' ') + outline_tuples.append((float(xy[0]), float(xy[1]))) + + vd = { 'center': center, 'nodes': value, 'boundary': outline_tuples } + vds.append(vd) write(name, f'drop table vd_{center}') + + return { 'virtual_districts': vds, 'isolated_nodes': isolated_nodes } diff --git a/tjnetwork.py b/tjnetwork.py index 3f4e5d3..b84f50d 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -939,5 +939,5 @@ def clean_scada_element(name: str) -> ChangeSet: # virtual_district 32 ############################################################ -def generate_virtual_district(name: str, centers: list[str]): - return api.generate_virtual_district(name, centers) +def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: + return api.calculate_virtual_district(name, centers) From c6518cbae159f0651257568ca26d63aae736cf34 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 11:18:55 +0800 Subject: [PATCH 05/40] Code refactor --- api/s32_virtual_district.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/api/s32_virtual_district.py b/api/s32_virtual_district.py index 85b8585..6ce4682 100644 --- a/api/s32_virtual_district.py +++ b/api/s32_virtual_district.py @@ -1,6 +1,16 @@ from .database import * from .s0_base import get_node_links + +def _polygon_to_nodes(polygon: str) -> list[tuple[float, float]]: + boundary = polygon.removeprefix('POLYGON((').removesuffix('))').split(',') + xys = [] + for pt in boundary: + xy = pt.split(' ') + xys.append((float(xy[0]), float(xy[1]))) + return xys + + def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: write(name, 'drop table if exists vd_graph') write(name, 'create table vd_graph (id serial, source integer, target integer, cost numeric)') @@ -71,15 +81,8 @@ def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: # TODO: check none boundary = read(name, f'select st_astext(st_convexhull(st_collect(array(select coord from coordinates where node in (select * from vd_{center}))))) as boundary' )['boundary'] # write(name, f"insert into virtual_district (id, center, boundary) values ('vd_{center}', '{center}', st_geomfromtext('{boundary}'))") - - outlines = str(boundary).removeprefix('POLYGON((').removesuffix('))').split(',') - outline_tuples = [] - for pt in outlines: - xy = pt.split(' ') - outline_tuples.append((float(xy[0]), float(xy[1]))) - - vd = { 'center': center, 'nodes': value, 'boundary': outline_tuples } - vds.append(vd) + xys = _polygon_to_nodes(boundary) + vds.append({ 'center': center, 'nodes': value, 'boundary': xys }) write(name, f'drop table vd_{center}') From 12150ef57e56a4cd835f338132686c7ca1c22e8d Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 14:25:43 +0800 Subject: [PATCH 06/40] Add general region table --- create_template.py | 4 ++-- script/sql/create/32.region.sql | 7 +++++++ script/sql/create/32.virtual_district.sql | 9 --------- script/sql/drop/32.region.sql | 3 +++ script/sql/drop/32.virtual_district.sql | 3 --- 5 files changed, 12 insertions(+), 14 deletions(-) create mode 100644 script/sql/create/32.region.sql delete mode 100644 script/sql/create/32.virtual_district.sql create mode 100644 script/sql/drop/32.region.sql delete mode 100644 script/sql/drop/32.virtual_district.sql diff --git a/create_template.py b/create_template.py index 8f325a9..c0260c2 100644 --- a/create_template.py +++ b/create_template.py @@ -33,13 +33,13 @@ sql_create = [ "script/sql/create/29.scada_device.sql", "script/sql/create/30.scada_device_data.sql", "script/sql/create/31.scada_element.sql", - "script/sql/create/32.virtual_district.sql", + "script/sql/create/32.region.sql", "script/sql/create/operation.sql" ] sql_drop = [ "script/sql/drop/operation.sql", - "script/sql/drop/32.virtual_district.sql", + "script/sql/drop/32.region.sql", "script/sql/drop/31.scada_element.sql", "script/sql/drop/30.scada_device_data.sql", "script/sql/drop/29.scada_device.sql", diff --git a/script/sql/create/32.region.sql b/script/sql/create/32.region.sql new file mode 100644 index 0000000..0fcf92c --- /dev/null +++ b/script/sql/create/32.region.sql @@ -0,0 +1,7 @@ +create table region +( + id text primary key +, boundary geometry not null unique +); + +create index region_gist on region using gist(boundary); diff --git a/script/sql/create/32.virtual_district.sql b/script/sql/create/32.virtual_district.sql deleted file mode 100644 index 12edec0..0000000 --- a/script/sql/create/32.virtual_district.sql +++ /dev/null @@ -1,9 +0,0 @@ -create table virtual_district -( - id text primary key -, center varchar(32) references _node(id) not null unique -, boundary geometry unique -, nodes varchar(32)[] -); - -create index virtual_district_gist on virtual_district using gist(boundary); diff --git a/script/sql/drop/32.region.sql b/script/sql/drop/32.region.sql new file mode 100644 index 0000000..b30dd82 --- /dev/null +++ b/script/sql/drop/32.region.sql @@ -0,0 +1,3 @@ +drop index if exists region_gist; + +drop table if exists region; diff --git a/script/sql/drop/32.virtual_district.sql b/script/sql/drop/32.virtual_district.sql deleted file mode 100644 index 33921c1..0000000 --- a/script/sql/drop/32.virtual_district.sql +++ /dev/null @@ -1,3 +0,0 @@ -drop index if exists virtual_district_gist; - -drop table if exists virtual_district; From a278335bbe55fe3f28f800471d9507c526b5c140 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 14:26:49 +0800 Subject: [PATCH 07/40] Refactor coord --- api/s24_coordinates.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/s24_coordinates.py b/api/s24_coordinates.py index 9e73651..09a30d5 100644 --- a/api/s24_coordinates.py +++ b/api/s24_coordinates.py @@ -15,7 +15,7 @@ def sql_delete_coord(node: str) -> str: return f"delete from coordinates where node = '{node}';" -def _to_client_point(coord: str) -> dict[str, float]: +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]) } @@ -25,7 +25,7 @@ def get_node_coord(name: str, node: str) -> dict[str, float]: if row == None: write(name, sql_insert_coord(node, 0.0, 0.0)) return {'x': 0.0, 'y': 0.0} - return _to_client_point(row['coord_geom']) + return from_postgis_point(row['coord_geom']) #-------------------------------------------------------------- @@ -47,7 +47,7 @@ def inp_out_coord(name: str) -> list[str]: objs = read_all(name, 'select node, st_astext(coord) as coord_geom from coordinates') for obj in objs: node = obj['node'] - coord = _to_client_point(obj['coord_geom']) + coord = from_postgis_point(obj['coord_geom']) x = coord['x'] y = coord['y'] lines.append(f'{node} {x} {y}') From a0166193efab3e00b5130e99b32f8f0f88958a42 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 14:27:25 +0800 Subject: [PATCH 08/40] Refactor vd --- api/__init__.py | 2 +- api/{s32_virtual_district.py => s37_virtual_district.py} | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) rename api/{s32_virtual_district.py => s37_virtual_district.py} (94%) diff --git a/api/__init__.py b/api/__init__.py index de43ec5..e7f81b0 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -132,4 +132,4 @@ from .s31_scada_element import SCADA_ELEMENT_STATUS_OFFLINE, SCADA_ELEMENT_STATU from .s31_scada_element import get_scada_element_schema, get_scada_elements, get_scada_element, set_scada_element, add_scada_element, delete_scada_element from .del_cmd import clean_scada_element -from .s32_virtual_district import calculate_virtual_district \ No newline at end of file +from .s37_virtual_district import calculate_virtual_district \ No newline at end of file diff --git a/api/s32_virtual_district.py b/api/s37_virtual_district.py similarity index 94% rename from api/s32_virtual_district.py rename to api/s37_virtual_district.py index 6ce4682..93e8d1d 100644 --- a/api/s32_virtual_district.py +++ b/api/s37_virtual_district.py @@ -68,7 +68,6 @@ def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: center_node[value['center']] = [] center_node[value['center']].append(node) - # write(name, 'delete from virtual_district') vds: list[dict[str, Any]] = [] for center, value in center_node.items(): @@ -80,7 +79,6 @@ def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: # TODO: check none boundary = read(name, f'select st_astext(st_convexhull(st_collect(array(select coord from coordinates where node in (select * from vd_{center}))))) as boundary' )['boundary'] - # write(name, f"insert into virtual_district (id, center, boundary) values ('vd_{center}', '{center}', st_geomfromtext('{boundary}'))") xys = _polygon_to_nodes(boundary) vds.append({ 'center': center, 'nodes': value, 'boundary': xys }) From 35d2ce08e1733255e6c345a5b41650260185d64f Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 15:01:11 +0800 Subject: [PATCH 09/40] Clean code --- api/batch_cmd.py | 244 +---------------------------------- api/del_cmd.py | 14 +- api/s10_status.py | 4 +- api/s11_patterns.py | 12 +- api/s12_curves.py | 12 +- api/s13_controls.py | 4 +- api/s14_rules.py | 4 +- api/s15_energy.py | 8 +- api/s16_emitters.py | 4 +- api/s17_quality.py | 4 +- api/s18_sources.py | 12 +- api/s19_reactions.py | 12 +- api/s1_title.py | 4 +- api/s20_mixing.py | 12 +- api/s21_times.py | 4 +- api/s25_vertices.py | 16 +-- api/s26_labels.py | 12 +- api/s27_backdrop.py | 4 +- api/s29_scada_device.py | 14 +- api/s2_junctions.py | 12 +- api/s30_scada_device_data.py | 14 +- api/s31_scada_element.py | 14 +- api/s3_reservoirs.py | 12 +- api/s4_tanks.py | 12 +- api/s5_pipes.py | 12 +- api/s6_pumps.py | 12 +- api/s7_valves.py | 12 +- api/s8_tags.py | 4 +- api/s9_demands.py | 4 +- 29 files changed, 133 insertions(+), 375 deletions(-) diff --git a/api/batch_cmd.py b/api/batch_cmd.py index c6d352f..0237171 100644 --- a/api/batch_cmd.py +++ b/api/batch_cmd.py @@ -1,267 +1,25 @@ from .sections import * from .database import API_ADD, API_UPDATE, API_DELETE, ChangeSet, DbChangeSet, execute_command -from .s1_title import set_title_cmd -from .s2_junctions import set_junction_cmd, add_junction_cmd, delete_junction_cmd -from .s3_reservoirs import set_reservoir_cmd, add_reservoir_cmd, delete_reservoir_cmd -from .s4_tanks import set_tank_cmd, add_tank_cmd, delete_tank_cmd -from .s5_pipes import set_pipe_cmd, add_pipe_cmd, delete_pipe_cmd -from .s6_pumps import set_pump_cmd, add_pump_cmd, delete_pump_cmd -from .s7_valves import set_valve_cmd, add_valve_cmd, delete_valve_cmd -from .s8_tags import set_tag_cmd -from .s9_demands import set_demand_cmd -from .s10_status import set_status_cmd -from .s11_patterns import set_pattern_cmd, add_pattern_cmd, delete_pattern_cmd -from .s12_curves import set_curve_cmd, add_curve_cmd, delete_curve_cmd -from .s13_controls import set_control_cmd -from .s14_rules import set_rule_cmd -from .s15_energy import set_energy_cmd, set_pump_energy_cmd -from .s16_emitters import set_emitter_cmd -from .s17_quality import set_quality_cmd -from .s18_sources import set_source_cmd, add_source_cmd, delete_source_cmd -from .s19_reactions import set_reaction_cmd, set_pipe_reaction_cmd, set_tank_reaction_cmd -from .s20_mixing import set_mixing_cmd, add_mixing_cmd, delete_mixing_cmd -from .s21_times import set_time_cmd -#from .s22_report import * from .s23_options_util import set_option_cmd, set_option_v3_cmd -#from .s24_coordinates import * -from .s25_vertices import set_vertex_cmd, add_vertex_cmd, delete_vertex_cmd -from .s26_labels import set_label_cmd, add_label_cmd, delete_label_cmd -from .s27_backdrop import set_backdrop_cmd -# from .s28_end import * -from .s29_scada_device import set_scada_device_cmd, add_scada_device_cmd, delete_scada_device_cmd -from .s30_scada_device_data import set_scada_device_data_cmd, add_scada_device_data_cmd, delete_scada_device_data_cmd -from .s31_scada_element import set_scada_element_cmd, add_scada_element_cmd, delete_scada_element_cmd from .del_cmd_raw import del_cascade_cmd def add_cmd(name: str, cs: ChangeSet) -> DbChangeSet | None: - type = cs.operations[0]['type'] - - if type == s1_title: - return None - if type == s2_junction: - return add_junction_cmd(name, cs) - elif type == s3_reservoir: - return add_reservoir_cmd(name, cs) - elif type == s4_tank: - return add_tank_cmd(name, cs) - elif type == s5_pipe: - return add_pipe_cmd(name, cs) - elif type == s6_pump: - return add_pump_cmd(name, cs) - elif type == s7_valve: - return add_valve_cmd(name, cs) - elif type == s8_tag: - return None - elif type == s9_demand: - return None - elif type == s10_status: - return None - elif type == s11_pattern: - return add_pattern_cmd(name, cs) - elif type == s12_curve: - return add_curve_cmd(name, cs) - elif type == s13_control: - return None - elif type == s14_rule: - return None - elif type == s15_energy: - return None - elif type == s15_pump_energy: - return None - elif type == s16_emitter: - return None - elif type == s17_quality: - return None - elif type == s18_source: - return add_source_cmd(name, cs) - elif type == s19_reaction: - return None - elif type == s19_pipe_reaction: - return None - elif type == s19_tank_reaction: - return None - elif type == s20_mixing: - return add_mixing_cmd(name, cs) - elif type == s21_time: - return None - elif type == s22_report: - return None - elif type == s23_option: - return None - elif type == s23_option_v3: - return None - elif type == s24_coordinate: - return None - elif type == s25_vertex: - return add_vertex_cmd(name, cs) - elif type == s26_label: - return add_label_cmd(name, cs) - elif type == s27_backdrop: - return None - elif type == s28_end: - return None - elif type == s29_scada_device: - return add_scada_device_cmd(name, cs) - elif type == s30_scada_device_data: - return add_scada_device_data_cmd(name, cs) - elif type == s31_scada_element: - return add_scada_element_cmd(name, cs) - return None def set_cmd(name: str, cs: ChangeSet) -> DbChangeSet | None: type = cs.operations[0]['type'] - if type == s1_title: - return set_title_cmd(name, cs) - if type == s2_junction: - return set_junction_cmd(name, cs) - elif type == s3_reservoir: - return set_reservoir_cmd(name, cs) - elif type == s4_tank: - return set_tank_cmd(name, cs) - elif type == s5_pipe: - return set_pipe_cmd(name, cs) - elif type == s6_pump: - return set_pump_cmd(name, cs) - elif type == s7_valve: - return set_valve_cmd(name, cs) - elif type == s8_tag: - return set_tag_cmd(name, cs) - elif type == s9_demand: - return set_demand_cmd(name, cs) - elif type == s10_status: - return set_status_cmd(name, cs) - elif type == s11_pattern: - return set_pattern_cmd(name, cs) - elif type == s12_curve: - return set_curve_cmd(name, cs) - elif type == s13_control: - return set_control_cmd(name, cs) - elif type == s14_rule: - return set_rule_cmd(name, cs) - elif type == s15_energy: - return set_energy_cmd(name, cs) - elif type == s15_pump_energy: - return set_pump_energy_cmd(name, cs) - elif type == s16_emitter: - return set_emitter_cmd(name, cs) - elif type == s17_quality: - return set_quality_cmd(name, cs) - elif type == s18_source: - return set_source_cmd(name, cs) - elif type == s19_reaction: - return set_reaction_cmd(name, cs) - elif type == s19_pipe_reaction: - return set_pipe_reaction_cmd(name, cs) - elif type == s19_tank_reaction: - return set_tank_reaction_cmd(name, cs) - elif type == s20_mixing: - return set_mixing_cmd(name, cs) - elif type == s21_time: - return set_time_cmd(name, cs) - elif type == s22_report: # no api now - return None - elif type == s23_option: + if type == s23_option: return set_option_cmd(name, cs) elif type == s23_option_v3: return set_option_v3_cmd(name, cs) - elif type == s24_coordinate: # do not support update here - return None - elif type == s25_vertex: - return set_vertex_cmd(name, cs) - elif type == s26_label: - return set_label_cmd(name, cs) - elif type == s27_backdrop: - return set_backdrop_cmd(name, cs) - elif type == s28_end: # end - return None - elif type == s29_scada_device: - return set_scada_device_cmd(name, cs) - elif type == s30_scada_device_data: - return set_scada_device_data_cmd(name, cs) - elif type == s31_scada_element: - return set_scada_element_cmd(name, cs) return None def del_cmd(name: str, cs: ChangeSet) -> DbChangeSet | None: - type = cs.operations[0]['type'] - - if type == s1_title: - return None - if type == s2_junction: - return delete_junction_cmd(name, cs) - elif type == s3_reservoir: - return delete_reservoir_cmd(name, cs) - elif type == s4_tank: - return delete_tank_cmd(name, cs) - elif type == s5_pipe: - return delete_pipe_cmd(name, cs) - elif type == s6_pump: - return delete_pump_cmd(name, cs) - elif type == s7_valve: - return delete_valve_cmd(name, cs) - elif type == s8_tag: - return None - elif type == s9_demand: - return None - elif type == s10_status: - return None - elif type == s11_pattern: - return delete_pattern_cmd(name, cs) - elif type == s12_curve: - return delete_curve_cmd(name, cs) - elif type == s13_control: - return None - elif type == s14_rule: - return None - elif type == s15_energy: - return None - elif type == s15_pump_energy: - return None - elif type == s16_emitter: - return None - elif type == s17_quality: - return None - elif type == s18_source: - return delete_source_cmd(name, cs) - elif type == s19_reaction: - return None - elif type == s19_pipe_reaction: - return None - elif type == s19_tank_reaction: - return None - elif type == s20_mixing: - return delete_mixing_cmd(name, cs) - elif type == s21_time: - return None - elif type == s22_report: - return None - elif type == s23_option: - return None - elif type == s23_option_v3: - return None - elif type == s24_coordinate: - return None - elif type == s25_vertex: - return delete_vertex_cmd(name, cs) - elif type == s26_label: - return delete_label_cmd(name, cs) - elif type == s27_backdrop: - return None - elif type == s28_end: - return None - elif type == s29_scada_device: - return delete_scada_device_cmd(name, cs) - elif type == s30_scada_device_data: - return delete_scada_device_data_cmd(name, cs) - elif type == s31_scada_element: - return delete_scada_element_cmd(name, cs) - return None diff --git a/api/del_cmd.py b/api/del_cmd.py index 1d20140..354c819 100644 --- a/api/del_cmd.py +++ b/api/del_cmd.py @@ -1,8 +1,8 @@ from .del_cmd_raw import * -from .batch_cmd import execute_batch_command -from .s29_scada_device import clean_scada_device_cmd -from .s30_scada_device_data import clean_scada_device_data_cmd -from .s31_scada_element import clean_scada_element_cmd +from .batch_cmds import execute_batch_command +from .s29_scada_device import clean_scada_device_cs +from .s30_scada_device_data import clean_scada_device_data_cs +from .s31_scada_element import clean_scada_element_cs def delete_junction_cascade(name: str, cs: ChangeSet) -> ChangeSet: @@ -54,12 +54,12 @@ def delete_curve_cascade(name: str, cs: ChangeSet) -> ChangeSet: def clean_scada_device(name: str) -> ChangeSet: - return execute_batch_command(name, clean_scada_device_cmd(name)) + 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_cmd(name)) + 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_cmd(name)) + return execute_batch_command(name, clean_scada_element_cs(name)) diff --git a/api/s10_status.py b/api/s10_status.py index 9aa61c4..cf07ccb 100644 --- a/api/s10_status.py +++ b/api/s10_status.py @@ -39,7 +39,7 @@ class Status(object): return { 'type': self.type, 'link': self.link, 'status': self.status, 'setting': self.setting } -def set_status_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -65,7 +65,7 @@ def set_status_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_status(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_status_cmd(name, cs)) + return execute_command(name, _set_status(name, cs)) #-------------------------------------------------------------- diff --git a/api/s11_patterns.py b/api/s11_patterns.py index 236720c..382d554 100644 --- a/api/s11_patterns.py +++ b/api/s11_patterns.py @@ -21,7 +21,7 @@ def get_pattern(name: str, id: str) -> dict[str, Any]: return { 'id': id, 'factors': ps } -def set_pattern_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_pattern(name: str, cs: ChangeSet) -> DbChangeSet: id = cs.operations[0]['id'] f_id = f"'{id}'" @@ -53,10 +53,10 @@ def set_pattern(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_pattern(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, set_pattern_cmd(name, cs)) + return execute_command(name, _set_pattern(name, cs)) -def add_pattern_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _add_pattern(name: str, cs: ChangeSet) -> DbChangeSet: id = cs.operations[0]['id'] f_id = f"'{id}'" @@ -81,10 +81,10 @@ def add_pattern(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_pattern(name, cs.operations[0]['id']) != {}: return ChangeSet() - return execute_command(name, add_pattern_cmd(name, cs)) + return execute_command(name, _add_pattern(name, cs)) -def delete_pattern_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _delete_pattern(name: str, cs: ChangeSet) -> DbChangeSet: id = cs.operations[0]['id'] f_id = f"'{id}'" @@ -109,7 +109,7 @@ def delete_pattern(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_pattern(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, delete_pattern_cmd(name, cs)) + return execute_command(name, _delete_pattern(name, cs)) #-------------------------------------------------------------- diff --git a/api/s12_curves.py b/api/s12_curves.py index a615564..1ea6456 100644 --- a/api/s12_curves.py +++ b/api/s12_curves.py @@ -30,7 +30,7 @@ def get_curve(name: str, id: str) -> dict[str, Any]: return d -def set_curve_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_curve(name: str, cs: ChangeSet) -> DbChangeSet: id = cs.operations[0]['id'] f_id = f"'{id}'" @@ -72,10 +72,10 @@ def set_curve(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_curve(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, set_curve_cmd(name, cs)) + return execute_command(name, _set_curve(name, cs)) -def add_curve_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _add_curve(name: str, cs: ChangeSet) -> DbChangeSet: id = cs.operations[0]['id'] f_id = f"'{id}'" @@ -104,10 +104,10 @@ def add_curve(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_curve(name, cs.operations[0]['id']) != {}: return ChangeSet() - return execute_command(name, add_curve_cmd(name, cs)) + return execute_command(name, _add_curve(name, cs)) -def delete_curve_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _delete_curve(name: str, cs: ChangeSet) -> DbChangeSet: id = cs.operations[0]['id'] f_id = f"'{id}'" @@ -134,7 +134,7 @@ def delete_curve(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_curve(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, delete_curve_cmd(name, cs)) + return execute_command(name, _delete_curve(name, cs)) #-------------------------------------------------------------- diff --git a/api/s13_controls.py b/api/s13_controls.py index 8f50de4..b3cf9b7 100644 --- a/api/s13_controls.py +++ b/api/s13_controls.py @@ -13,7 +13,7 @@ def get_control(name: str) -> dict[str, Any]: return { 'controls': ds } -def set_control_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_control(name: str, cs: ChangeSet) -> DbChangeSet: old = get_control(name) redo_sql = 'delete from controls;' @@ -31,7 +31,7 @@ def set_control_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_control(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_control_cmd(name, cs)) + return execute_command(name, _set_control(name, cs)) #-------------------------------------------------------------- diff --git a/api/s14_rules.py b/api/s14_rules.py index 49a47cf..b7f9e9a 100644 --- a/api/s14_rules.py +++ b/api/s14_rules.py @@ -13,7 +13,7 @@ def get_rule(name: str) -> dict[str, Any]: return { 'rules': ds } -def set_rule_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_rule(name: str, cs: ChangeSet) -> DbChangeSet: old = get_rule(name) redo_sql = 'delete from rules;' @@ -31,7 +31,7 @@ def set_rule_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_rule(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_rule_cmd(name, cs)) + return execute_command(name, _set_rule(name, cs)) #-------------------------------------------------------------- diff --git a/api/s15_energy.py b/api/s15_energy.py index 8bca11b..cc04c6c 100644 --- a/api/s15_energy.py +++ b/api/s15_energy.py @@ -19,7 +19,7 @@ def get_energy(name: str) -> dict[str, Any]: return d -def set_energy_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_energy(name: str, cs: ChangeSet) -> DbChangeSet: raw_old = get_energy(name) old = {} @@ -54,7 +54,7 @@ def set_energy_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_energy(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_energy_cmd(name, cs)) + return execute_command(name, _set_energy(name, cs)) def get_pump_energy_schema(name: str) -> dict[str, dict[str, Any]]: @@ -94,7 +94,7 @@ class PumpEnergy(object): return { 'type': self.type, 'pump': self.pump, 'price': self.price, 'pattern': self.pattern, 'effic': self.effic } -def set_pump_energy_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -128,7 +128,7 @@ def set_pump_energy_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_pump_energy(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_pump_energy_cmd(name, cs)) + return execute_command(name, _set_pump_energy(name, cs)) #-------------------------------------------------------------- diff --git a/api/s16_emitters.py b/api/s16_emitters.py index ee88df4..0bf06ad 100644 --- a/api/s16_emitters.py +++ b/api/s16_emitters.py @@ -30,7 +30,7 @@ class Emitter(object): return { 'type': self.type, 'junction': self.junction, 'coefficient': self.coefficient } -def set_emitter_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -56,7 +56,7 @@ def set_emitter_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_emitter(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_emitter_cmd(name, cs)) + return execute_command(name, _set_emitter(name, cs)) #-------------------------------------------------------------- diff --git a/api/s17_quality.py b/api/s17_quality.py index a524e0c..1087863 100644 --- a/api/s17_quality.py +++ b/api/s17_quality.py @@ -30,7 +30,7 @@ class Quality(object): return { 'type': self.type, 'node': self.node, 'quality': self.quality } -def set_quality_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -56,7 +56,7 @@ def set_quality_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_quality(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_quality_cmd(name, cs)) + return execute_command(name, _set_quality(name, cs)) #-------------------------------------------------------------- diff --git a/api/s18_sources.py b/api/s18_sources.py index dcc4db9..2ea3406 100644 --- a/api/s18_sources.py +++ b/api/s18_sources.py @@ -46,7 +46,7 @@ class Source(object): return { 'type': self.type, 'node': self.node } -def set_source_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -67,10 +67,10 @@ def set_source_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_source(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_source_cmd(name, cs)) + return execute_command(name, _set_source(name, cs)) -def add_source_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _add_source(name: str, cs: ChangeSet) -> DbChangeSet: new = Source(cs.operations[0]) redo_sql = f"insert into sources (node, type, strength, pattern) values ({new.f_node}, {new.f_s_type}, {new.f_strength}, {new.f_pattern});" @@ -83,10 +83,10 @@ def add_source_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def add_source(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, add_source_cmd(name, cs)) + return execute_command(name, _add_source(name, cs)) -def delete_source_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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};" @@ -99,7 +99,7 @@ def delete_source_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def delete_source(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, delete_source_cmd(name, cs)) + return execute_command(name, _delete_source(name, cs)) #-------------------------------------------------------------- diff --git a/api/s19_reactions.py b/api/s19_reactions.py index 19b9c3f..8ebf7a7 100644 --- a/api/s19_reactions.py +++ b/api/s19_reactions.py @@ -22,7 +22,7 @@ def get_reaction(name: str) -> dict[str, Any]: return d -def set_reaction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_reaction(name: str, cs: ChangeSet) -> DbChangeSet: raw_old = get_reaction(name) old = {} @@ -57,7 +57,7 @@ def set_reaction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_reaction(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_reaction_cmd(name, cs)) + return execute_command(name, _set_reaction(name, cs)) def get_pipe_reaction_schema(name: str) -> dict[str, dict[str, Any]]: @@ -92,7 +92,7 @@ class PipeReaction(object): return { 'type': self.type, 'pipe': self.pipe, 'bulk': self.bulk, 'wall': self.wall } -def set_pipe_reaction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -122,7 +122,7 @@ def set_pipe_reaction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_pipe_reaction(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_pipe_reaction_cmd(name, cs)) + return execute_command(name, _set_pipe_reaction(name, cs)) def get_tank_reaction_schema(name: str) -> dict[str, dict[str, Any]]: @@ -152,7 +152,7 @@ class TankReaction(object): return { 'type': self.type, 'tank': self.tank, 'value': self.value } -def set_tank_reaction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -178,7 +178,7 @@ def set_tank_reaction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_tank_reaction(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_tank_reaction_cmd(name, cs)) + return execute_command(name, _set_tank_reaction(name, cs)) #-------------------------------------------------------------- diff --git a/api/s1_title.py b/api/s1_title.py index c897c62..a038857 100644 --- a/api/s1_title.py +++ b/api/s1_title.py @@ -10,7 +10,7 @@ def get_title(name: str) -> dict[str, Any]: return { 'value': title['value'] } -def set_title_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_title(name: str, cs: ChangeSet) -> DbChangeSet: new = cs.operations[0]['value'] old = get_title(name)['value'] @@ -24,7 +24,7 @@ def set_title_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_title(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_title_cmd(name ,cs)) + return execute_command(name, _set_title(name ,cs)) def inp_in_title(section: list[str]) -> str: diff --git a/api/s20_mixing.py b/api/s20_mixing.py index 1b8917c..db4c8d3 100644 --- a/api/s20_mixing.py +++ b/api/s20_mixing.py @@ -42,7 +42,7 @@ class Mixing(object): return { 'type': self.type, 'tank': self.tank } -def set_mixing_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -67,10 +67,10 @@ def set_mixing(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_mixing(name, cs.operations[0]['tank']) == {}: return ChangeSet() - return execute_command(name, set_mixing_cmd(name, cs)) + return execute_command(name, _set_mixing(name, cs)) -def add_mixing_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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});" @@ -87,10 +87,10 @@ def add_mixing(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_mixing(name, cs.operations[0]['tank']) != {}: return ChangeSet() - return execute_command(name, add_mixing_cmd(name, cs)) + return execute_command(name, _add_mixing(name, cs)) -def delete_mixing_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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};" @@ -107,7 +107,7 @@ def delete_mixing(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_mixing(name, cs.operations[0]['tank']) == {}: return ChangeSet() - return execute_command(name, delete_mixing_cmd(name, cs)) + return execute_command(name, _delete_mixing(name, cs)) #-------------------------------------------------------------- diff --git a/api/s21_times.py b/api/s21_times.py index 590e91d..16910d9 100644 --- a/api/s21_times.py +++ b/api/s21_times.py @@ -29,7 +29,7 @@ def get_time(name: str) -> dict[str, Any]: return d -def set_time_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_time(name: str, cs: ChangeSet) -> DbChangeSet: raw_old = get_time(name) old = {} @@ -64,7 +64,7 @@ def set_time_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_time(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_time_cmd(name, cs)) + return execute_command(name, _set_time(name, cs)) #-------------------------------------------------------------- diff --git a/api/s25_vertices.py b/api/s25_vertices.py index 96c381c..20f8cf5 100644 --- a/api/s25_vertices.py +++ b/api/s25_vertices.py @@ -16,7 +16,7 @@ def get_vertex(name: str, link: str) -> dict[str, Any]: return { 'link': link, 'coords': cs } -def set_vertex_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_vertex(name: str, cs: ChangeSet) -> DbChangeSet: link = cs.operations[0]['link'] old = get_vertex(name, link) @@ -44,34 +44,34 @@ def set_vertex_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_vertex(name: str, cs: ChangeSet) -> ChangeSet: - result = set_vertex_cmd(name, cs) + 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_cmd(name: str, cs: ChangeSet) -> DbChangeSet: - result = set_vertex_cmd(name, cs) +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_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _delete_vertex(name: str, cs: ChangeSet) -> DbChangeSet: cs.operations[0]['coords'] = [] - result = set_vertex_cmd(name, cs) + 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_cmd(name, cs) + result = _add_vertex(name, cs) return execute_command(name, result) def delete_vertex(name: str, cs: ChangeSet) -> ChangeSet: - result = delete_vertex_cmd(name, cs) + result = _delete_vertex(name, cs) return execute_command(name, result) diff --git a/api/s26_labels.py b/api/s26_labels.py index 437bc0e..c7e7193 100644 --- a/api/s26_labels.py +++ b/api/s26_labels.py @@ -43,7 +43,7 @@ class Label(object): return { 'type': self.type, 'x': self.x, 'y': self.y } -def set_label_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -64,10 +64,10 @@ def set_label_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_label(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_label_cmd(name, cs)) + return execute_command(name, _set_label(name, cs)) -def add_label_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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});" @@ -80,10 +80,10 @@ def add_label_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def add_label(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, add_label_cmd(name, cs)) + return execute_command(name, _add_label(name, cs)) -def delete_label_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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};" @@ -96,7 +96,7 @@ def delete_label_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def delete_label(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, delete_label_cmd(name, cs)) + return execute_command(name, _delete_label(name, cs)) def inp_in_label(line: str) -> str: diff --git a/api/s27_backdrop.py b/api/s27_backdrop.py index 9766948..18f938a 100644 --- a/api/s27_backdrop.py +++ b/api/s27_backdrop.py @@ -10,7 +10,7 @@ def get_backdrop(name: str) -> dict[str, Any]: return { 'content': e['content'] } -def set_backdrop_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']}';" @@ -23,7 +23,7 @@ def set_backdrop_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_backdrop(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_backdrop_cmd(name, cs)) + return execute_command(name, _set_backdrop(name, cs)) def inp_in_backdrop(section: list[str]) -> str: diff --git a/api/s29_scada_device.py b/api/s29_scada_device.py index 7d48978..c4fe561 100644 --- a/api/s29_scada_device.py +++ b/api/s29_scada_device.py @@ -56,7 +56,7 @@ class ScadaDevice(object): return { 'type': self.type, 'id': self.id } -def set_scada_device_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -79,10 +79,10 @@ def set_scada_device_cmd(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_cmd(name, cs)) + return execute_command(name, _set_scada_device(name, cs)) -def add_scada_device_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _add_scada_device(name: str, cs: ChangeSet) -> DbChangeSet: new = ScadaDevice(cs.operations[0]) redo_sql = f"insert into scada_device (id, name, address, type) values ({new.f_id}, {new.f_name}, {new.f_address}, {new.f_sd_type});" @@ -97,10 +97,10 @@ def add_scada_device_cmd(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_cmd(name, cs)) + return execute_command(name, _add_scada_device(name, cs)) -def delete_scada_device_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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};" @@ -115,10 +115,10 @@ def delete_scada_device_cmd(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_cmd(name, cs)) + return execute_command(name, _delete_scada_device(name, cs)) -def clean_scada_device_cmd(name: str) -> ChangeSet: +def clean_scada_device_cs(name: str) -> ChangeSet: cs = ChangeSet() rows = read_all(name, 'select id from scada_device acs') diff --git a/api/s2_junctions.py b/api/s2_junctions.py index a09f484..84164ea 100644 --- a/api/s2_junctions.py +++ b/api/s2_junctions.py @@ -44,7 +44,7 @@ class Junction(object): return { 'type': self.type, 'id': self.id } -def set_junction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -72,10 +72,10 @@ def set_junction(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_junction(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, set_junction_cmd(name, cs)) + return execute_command(name, _set_junction(name, cs)) -def add_junction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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});" @@ -97,10 +97,10 @@ def add_junction(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_junction(name, cs.operations[0]['id']) != {}: return ChangeSet() - return execute_command(name, add_junction_cmd(name, cs)) + return execute_command(name, _add_junction(name, cs)) -def delete_junction_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _delete_junction(name: str, cs: ChangeSet) -> DbChangeSet: old = Junction(get_junction(name, cs.operations[0]['id'])) redo_sql = sql_delete_coord(old.id) @@ -122,7 +122,7 @@ def delete_junction(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_junction(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, delete_junction_cmd(name, cs)) + return execute_command(name, _delete_junction(name, cs)) #-------------------------------------------------------------- diff --git a/api/s30_scada_device_data.py b/api/s30_scada_device_data.py index e09b813..34ac4e1 100644 --- a/api/s30_scada_device_data.py +++ b/api/s30_scada_device_data.py @@ -16,7 +16,7 @@ def get_scada_device_data(name: str, device_id: str) -> dict[str, Any]: return { 'device_id': device_id, 'data': ds } -def set_scada_device_data_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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) @@ -45,10 +45,10 @@ def set_scada_device_data_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_scada_device_data_cmd(name, cs)) + return execute_command(name, _set_scada_device_data(name, cs)) -def add_scada_device_data_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _add_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet: values = cs.operations[0] device_id = values['device_id'] time = values['time'] @@ -66,10 +66,10 @@ 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_cmd(name, cs)) + return execute_command(name, _add_scada_device_data(name, cs)) -def delete_scada_device_data_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _delete_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet: values = cs.operations[0] device_id = values['device_id'] time = values['time'] @@ -87,10 +87,10 @@ 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_cmd(name, cs)) + return execute_command(name, _delete_scada_device_data(name, cs)) -def clean_scada_device_data_cmd(name: str) -> ChangeSet: +def clean_scada_device_data_cs(name: str) -> ChangeSet: cs = ChangeSet() rows = read_all(name, 'select distinct device_id from scada_device_data acs') diff --git a/api/s31_scada_element.py b/api/s31_scada_element.py index ab47316..798b013 100644 --- a/api/s31_scada_element.py +++ b/api/s31_scada_element.py @@ -114,7 +114,7 @@ class ScadaModel(object): return { 'type': self.type, 'id': self.id } -def set_scada_element_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -139,10 +139,10 @@ def set_scada_element(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if _check_model(name, cs) == False: return ChangeSet() - return execute_command(name, set_scada_element_cmd(name, cs)) + return execute_command(name, _set_scada_element(name, cs)) -def add_scada_element_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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});" @@ -159,10 +159,10 @@ def add_scada_element(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if _check_model(name, cs) == False: return ChangeSet() - return execute_command(name, add_scada_element_cmd(name, cs)) + return execute_command(name, _add_scada_element(name, cs)) -def delete_scada_element_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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};" @@ -177,10 +177,10 @@ def delete_scada_element_cmd(name: str, cs: ChangeSet) -> DbChangeSet: 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_cmd(name, cs)) + return execute_command(name, _delete_scada_element(name, cs)) -def clean_scada_element_cmd(name: str) -> ChangeSet: +def clean_scada_element_cs(name: str) -> ChangeSet: cs = ChangeSet() rows = read_all(name, 'select id from scada_element acs') diff --git a/api/s3_reservoirs.py b/api/s3_reservoirs.py index bac215d..992b50b 100644 --- a/api/s3_reservoirs.py +++ b/api/s3_reservoirs.py @@ -48,7 +48,7 @@ class Reservoir(object): return { 'type': self.type, 'id': self.id } -def set_reservoir_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -76,10 +76,10 @@ def set_reservoir(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_reservoir(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, set_reservoir_cmd(name, cs)) + return execute_command(name, _set_reservoir(name, cs)) -def add_reservoir_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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});" @@ -101,10 +101,10 @@ def add_reservoir(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_reservoir(name, cs.operations[0]['id']) != {}: return ChangeSet() - return execute_command(name, add_reservoir_cmd(name, cs)) + return execute_command(name, _add_reservoir(name, cs)) -def delete_reservoir_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _delete_reservoir(name: str, cs: ChangeSet) -> DbChangeSet: old = Reservoir(get_reservoir(name, cs.operations[0]['id'])) redo_sql = sql_delete_coord(old.id) @@ -126,7 +126,7 @@ def delete_reservoir(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_reservoir(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, delete_reservoir_cmd(name, cs)) + return execute_command(name, _delete_reservoir(name, cs)) #-------------------------------------------------------------- diff --git a/api/s4_tanks.py b/api/s4_tanks.py index 98ce2ea..837c9af 100644 --- a/api/s4_tanks.py +++ b/api/s4_tanks.py @@ -76,7 +76,7 @@ class Tank(object): return { 'type': self.type, 'id': self.id } -def set_tank_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -104,10 +104,10 @@ def set_tank(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_tank(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, set_tank_cmd(name, cs)) + return execute_command(name, _set_tank(name, cs)) -def add_tank_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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});" @@ -129,10 +129,10 @@ def add_tank(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_tank(name, cs.operations[0]['id']) != {}: return ChangeSet() - return execute_command(name, add_tank_cmd(name, cs)) + return execute_command(name, _add_tank(name, cs)) -def delete_tank_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _delete_tank(name: str, cs: ChangeSet) -> DbChangeSet: old = Tank(get_tank(name, cs.operations[0]['id'])) redo_sql = sql_delete_coord(old.id) @@ -154,7 +154,7 @@ def delete_tank(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_tank(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, delete_tank_cmd(name, cs)) + return execute_command(name, _delete_tank(name, cs)) #-------------------------------------------------------------- diff --git a/api/s5_pipes.py b/api/s5_pipes.py index 4c6c089..248e607 100644 --- a/api/s5_pipes.py +++ b/api/s5_pipes.py @@ -63,7 +63,7 @@ class Pipe(object): return { 'type': self.type, 'id': self.id } -def set_pipe_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -88,10 +88,10 @@ def set_pipe(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_pipe(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, set_pipe_cmd(name, cs)) + return execute_command(name, _set_pipe(name, cs)) -def add_pipe_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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});" @@ -111,10 +111,10 @@ def add_pipe(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_pipe(name, cs.operations[0]['id']) != {}: return ChangeSet() - return execute_command(name, add_pipe_cmd(name, cs)) + return execute_command(name, _add_pipe(name, cs)) -def delete_pipe_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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};" @@ -134,7 +134,7 @@ def delete_pipe(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_pipe(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, delete_pipe_cmd(name, cs)) + return execute_command(name, _delete_pipe(name, cs)) #-------------------------------------------------------------- diff --git a/api/s6_pumps.py b/api/s6_pumps.py index 1f73cfb..d57a2c4 100644 --- a/api/s6_pumps.py +++ b/api/s6_pumps.py @@ -54,7 +54,7 @@ class Pump(object): return { 'type': self.type, 'id': self.id } -def set_pump_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -79,10 +79,10 @@ def set_pump(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_pump(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, set_pump_cmd(name, cs)) + return execute_command(name, _set_pump(name, cs)) -def add_pump_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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});" @@ -102,10 +102,10 @@ def add_pump(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_pump(name, cs.operations[0]['id']) != {}: return ChangeSet() - return execute_command(name, add_pump_cmd(name, cs)) + return execute_command(name, _add_pump(name, cs)) -def delete_pump_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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};" @@ -125,7 +125,7 @@ def delete_pump(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_pump(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, delete_pump_cmd(name, cs)) + return execute_command(name, _delete_pump(name, cs)) #-------------------------------------------------------------- diff --git a/api/s7_valves.py b/api/s7_valves.py index c828916..64f79bc 100644 --- a/api/s7_valves.py +++ b/api/s7_valves.py @@ -62,7 +62,7 @@ class Valve(object): return { 'type': self.type, 'id': self.id } -def set_valve_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -87,10 +87,10 @@ def set_valve(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_valve(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, set_valve_cmd(name, cs)) + return execute_command(name, _set_valve(name, cs)) -def add_valve_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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});" @@ -110,10 +110,10 @@ def add_valve(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_valve(name, cs.operations[0]['id']) != {}: return ChangeSet() - return execute_command(name, add_valve_cmd(name, cs)) + return execute_command(name, _add_valve(name, cs)) -def delete_valve_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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};" @@ -133,7 +133,7 @@ def delete_valve(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() if get_valve(name, cs.operations[0]['id']) == {}: return ChangeSet() - return execute_command(name, delete_valve_cmd(name, cs)) + return execute_command(name, _delete_valve(name, cs)) #-------------------------------------------------------------- diff --git a/api/s8_tags.py b/api/s8_tags.py index aa2ed7a..9f72610 100644 --- a/api/s8_tags.py +++ b/api/s8_tags.py @@ -53,7 +53,7 @@ class Tag(object): return { 'type': self.type, 't_type': self.t_type, 'id': self.id, 'tag': self.tag } -def set_tag_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +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']) @@ -89,7 +89,7 @@ def set_tag_cmd(name: str, cs: ChangeSet) -> DbChangeSet: 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_cmd(name, cs)) + return execute_command(name, _set_tag(name, cs)) def inp_in_tag(line: str) -> str: diff --git a/api/s9_demands.py b/api/s9_demands.py index e1465e4..de3de41 100644 --- a/api/s9_demands.py +++ b/api/s9_demands.py @@ -20,7 +20,7 @@ def get_demand(name: str, junction: str) -> dict[str, Any]: return { 'junction': junction, 'demands': ds } -def set_demand_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_demand(name: str, cs: ChangeSet) -> DbChangeSet: junction = cs.operations[0]['junction'] old = get_demand(name, junction) new = { 'junction': junction, 'demands': [] } @@ -56,7 +56,7 @@ def set_demand_cmd(name: str, cs: ChangeSet) -> DbChangeSet: def set_demand(name: str, cs: ChangeSet) -> ChangeSet: - return execute_command(name, set_demand_cmd(name, cs)) + return execute_command(name, _set_demand(name, cs)) #-------------------------------------------------------------- From 4b452c5f0af6543c868116500da0cb3ba79d5a4f Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 15:42:01 +0800 Subject: [PATCH 10/40] Clean code --- api/__init__.py | 26 +++--- api/{del_cmd.py => api_batch.py} | 13 ++- api/{del_cmd_raw.py => api_cs.py} | 133 ++++++++++++++++++++++++------ api/batch_cmd.py | 62 -------------- api/batch_cmds.py | 12 +-- api/s23_options.py | 9 -- api/s23_options_util.py | 14 ++-- api/s23_options_v3.py | 9 -- api/s29_scada_device.py | 10 --- api/s30_scada_device_data.py | 10 --- api/s31_scada_element.py | 10 --- tjnetwork.py | 4 +- 12 files changed, 141 insertions(+), 171 deletions(-) rename api/{del_cmd.py => api_batch.py} (89%) rename api/{del_cmd_raw.py => api_cs.py} (53%) delete mode 100644 api/batch_cmd.py diff --git a/api/__init__.py b/api/__init__.py index e7f81b0..16e2e5f 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -32,25 +32,25 @@ from .s0_base import get_node_links from .s1_title import get_title_schema, get_title, set_title from .s2_junctions import get_junction_schema, add_junction, get_junction, set_junction -from .del_cmd import delete_junction_cascade +from .api_batch import delete_junction_cascade from .s3_reservoirs import get_reservoir_schema, add_reservoir, get_reservoir, set_reservoir -from .del_cmd import delete_reservoir_cascade +from .api_batch 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 -from .del_cmd import delete_tank_cascade +from .api_batch 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 -from .del_cmd import delete_pipe_cascade +from .api_batch import delete_pipe_cascade from .s6_pumps import get_pump_schema, add_pump, get_pump, set_pump -from .del_cmd import delete_pump_cascade +from .api_batch 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 -from .del_cmd import delete_valve_cascade +from .api_batch 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 @@ -61,11 +61,11 @@ 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 .del_cmd import delete_pattern_cascade +from .api_batch 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 .del_cmd import delete_curve_cascade +from .api_batch import delete_curve_cascade from .s13_controls import get_control_schema, get_control, set_control @@ -98,7 +98,7 @@ 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 .s23_options import set_option +from .api_batch 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_M @@ -110,7 +110,7 @@ from .s23_options_util import OPTION_V3_LEAKAGE_MODEL_NONE, OPTION_V3_LEAKAGE_MO 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 .s23_options_v3 import set_option_v3 +from .api_batch import set_option_v3_ex from .s24_coordinates import get_node_coord @@ -122,14 +122,14 @@ 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 from .s29_scada_device import get_scada_device_schema, get_scada_devices, get_scada_device, set_scada_device, add_scada_device, delete_scada_device -from .del_cmd import clean_scada_device +from .api_batch 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 .del_cmd import clean_scada_device_data +from .api_batch 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_elements, get_scada_element, set_scada_element, add_scada_element, delete_scada_element -from .del_cmd import clean_scada_element +from .api_batch import clean_scada_element from .s37_virtual_district import calculate_virtual_district \ No newline at end of file diff --git a/api/del_cmd.py b/api/api_batch.py similarity index 89% rename from api/del_cmd.py rename to api/api_batch.py index 354c819..091d2d4 100644 --- a/api/del_cmd.py +++ b/api/api_batch.py @@ -1,8 +1,5 @@ -from .del_cmd_raw import * from .batch_cmds import execute_batch_command -from .s29_scada_device import clean_scada_device_cs -from .s30_scada_device_data import clean_scada_device_data_cs -from .s31_scada_element import clean_scada_element_cs +from .api_cs import * def delete_junction_cascade(name: str, cs: ChangeSet) -> ChangeSet: @@ -53,6 +50,14 @@ def delete_curve_cascade(name: str, cs: ChangeSet) -> ChangeSet: return execute_batch_command(name, cs) +def set_option_ex(name: str, cs: ChangeSet) -> ChangeSet: + return execute_batch_command(name, set_option_cs(cs)) + + +def set_option_v3_ex(name: str, cs: ChangeSet) -> ChangeSet: + return execute_batch_command(name, set_option_v3_cs(cs)) + + def clean_scada_device(name: str) -> ChangeSet: return execute_batch_command(name, clean_scada_device_cs(name)) diff --git a/api/del_cmd_raw.py b/api/api_cs.py similarity index 53% rename from api/del_cmd_raw.py rename to api/api_cs.py index 1953fda..33705f4 100644 --- a/api/del_cmd_raw.py +++ b/api/api_cs.py @@ -18,8 +18,10 @@ 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_cmd(name: str, cs: ChangeSet) -> ChangeSet: + +def delete_junction_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: result = ChangeSet() id = cs.operations[0]['id'] @@ -31,11 +33,11 @@ def delete_junction_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: for link in links: if is_pipe(name, link): - result.merge(delete_pipe_cascade_batch_cmd(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': 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_cmd(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': 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_cmd(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': 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)) @@ -48,7 +50,7 @@ def delete_junction_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: return result -def delete_reservoir_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: +def delete_reservoir_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: result = ChangeSet() id = cs.operations[0]['id'] @@ -60,11 +62,11 @@ def delete_reservoir_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: for link in links: if is_pipe(name, link): - result.merge(delete_pipe_cascade_batch_cmd(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': 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_cmd(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': 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_cmd(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': 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)) @@ -75,7 +77,7 @@ def delete_reservoir_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: return result -def delete_tank_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: +def delete_tank_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: result = ChangeSet() id = cs.operations[0]['id'] @@ -87,11 +89,11 @@ def delete_tank_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: for link in links: if is_pipe(name, link): - result.merge(delete_pipe_cascade_batch_cmd(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': 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_cmd(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': 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_cmd(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': 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)) @@ -104,7 +106,7 @@ def delete_tank_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: return result -def delete_pipe_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: +def delete_pipe_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: result = ChangeSet() id = cs.operations[0]['id'] @@ -121,7 +123,7 @@ def delete_pipe_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: return result -def delete_pump_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: +def delete_pump_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: result = ChangeSet() id = cs.operations[0]['id'] @@ -138,7 +140,7 @@ def delete_pump_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: return result -def delete_valve_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: +def delete_valve_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: result = ChangeSet() id = cs.operations[0]['id'] @@ -155,7 +157,7 @@ def delete_valve_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: return result -def delete_pattern_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: +def delete_pattern_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: result = ChangeSet() id = cs.operations[0]['id'] @@ -173,7 +175,7 @@ def delete_pattern_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: return result -def delete_curve_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: +def delete_curve_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: result = ChangeSet() id = cs.operations[0]['id'] @@ -189,24 +191,103 @@ def delete_curve_cascade_batch_cmd(name: str, cs: ChangeSet) -> ChangeSet: return result -def del_cascade_cmd(name: str, cs: ChangeSet) -> ChangeSet: +def del_cascade_cs(name: str, cs: ChangeSet) -> ChangeSet: type = cs.operations[0]['type'] if type == s2_junction: - return delete_junction_cascade_batch_cmd(name, cs) + return delete_junction_cascade_batch_cs(name, cs) elif type == s3_reservoir: - return delete_reservoir_cascade_batch_cmd(name, cs) + return delete_reservoir_cascade_batch_cs(name, cs) elif type == s4_tank: - return delete_tank_cascade_batch_cmd(name, cs) + return delete_tank_cascade_batch_cs(name, cs) elif type == s5_pipe: - return delete_pipe_cascade_batch_cmd(name, cs) + return delete_pipe_cascade_batch_cs(name, cs) elif type == s6_pump: - return delete_pump_cascade_batch_cmd(name, cs) + return delete_pump_cascade_batch_cs(name, cs) elif type == s7_valve: - return delete_valve_cascade_batch_cmd(name, cs) + return delete_valve_cascade_batch_cs(name, cs) elif type == s11_pattern: - return delete_pattern_cascade_batch_cmd(name, cs) + return delete_pattern_cascade_batch_cs(name, cs) elif type == s12_curve: - return delete_curve_cascade_batch_cmd(name, cs) + return delete_curve_cascade_batch_cs(name, cs) + + return cs + + +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 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 extend(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) + + # TODO: support clean return cs diff --git a/api/batch_cmd.py b/api/batch_cmd.py deleted file mode 100644 index 0237171..0000000 --- a/api/batch_cmd.py +++ /dev/null @@ -1,62 +0,0 @@ -from .sections import * -from .database import API_ADD, API_UPDATE, API_DELETE, ChangeSet, DbChangeSet, execute_command -from .s23_options_util import set_option_cmd, set_option_v3_cmd -from .del_cmd_raw import del_cascade_cmd - - -def add_cmd(name: str, cs: ChangeSet) -> DbChangeSet | None: - return None - - -def set_cmd(name: str, cs: ChangeSet) -> DbChangeSet | None: - type = cs.operations[0]['type'] - - if type == s23_option: - return set_option_cmd(name, cs) - elif type == s23_option_v3: - return set_option_v3_cmd(name, cs) - - return None - - -def del_cmd(name: str, cs: ChangeSet) -> DbChangeSet | None: - return None - - -def execute_batch_command(name: str, cs: ChangeSet) -> ChangeSet: - css: list[DbChangeSet] = [] - - # for delete, generate cascade command - new_cs = ChangeSet() - for op in cs.operations: - if op['operation'] == API_DELETE: - new_cs.merge(del_cascade_cmd(name, ChangeSet(op))) - else: - new_cs.merge(ChangeSet(op)) - - try: - for op in new_cs.operations: - operation = op['operation'] - - r = None - - if operation == API_ADD: - r = add_cmd(name, ChangeSet(op)) - elif operation == API_UPDATE: - r = set_cmd(name, ChangeSet(op)) - elif operation == API_DELETE: - r = del_cmd(name, ChangeSet(op)) - - if r == None: - print(f'ERROR: Build [{op}] returns None') - return ChangeSet() - - css.append(r) - - except: - return ChangeSet() - - try: - return execute_command(name, DbChangeSet.from_list(css)) - except: - return ChangeSet() diff --git a/api/batch_cmds.py b/api/batch_cmds.py index 479d92b..f0cd93a 100644 --- a/api/batch_cmds.py +++ b/api/batch_cmds.py @@ -22,18 +22,14 @@ 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 .s22_report import * -from .s23_options import set_option -from .s23_options_v3 import set_option_v3 -#from .s24_coordinates import * +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 .s28_end import * 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 .del_cmd_raw import del_cascade_cmd +from .api_cs import extend def execute_add_command(name: str, cs: ChangeSet) -> ChangeSet: @@ -272,7 +268,7 @@ def execute_batch_commands(name: str, cs: ChangeSet) -> ChangeSet: new_cs = ChangeSet() for op in cs.operations: if op['operation'] == API_DELETE: - new_cs.merge(del_cascade_cmd(name, ChangeSet(op))) + new_cs.merge(extend(name, ChangeSet(op))) else: new_cs.merge(ChangeSet(op)) @@ -304,7 +300,7 @@ def execute_batch_command(name: str, cs: ChangeSet) -> ChangeSet: new_cs = ChangeSet() for op in cs.operations: if op['operation'] == API_DELETE: - new_cs.merge(del_cascade_cmd(name, ChangeSet(op))) + new_cs.merge(extend(name, ChangeSet(op))) else: new_cs.merge(ChangeSet(op)) diff --git a/api/s23_options.py b/api/s23_options.py index 6748f76..08d674c 100644 --- a/api/s23_options.py +++ b/api/s23_options.py @@ -1,14 +1,5 @@ from .database import * from .s23_options_util import get_option_schema, generate_v3 -from .batch_cmd import execute_batch_command - - -def set_option(name: str, cs: ChangeSet) -> ChangeSet: - cs.operations[0]['operation'] = API_UPDATE - cs.operations[0]['type'] = 'option' - new_cs = cs - new_cs.merge(generate_v3(cs)) - return execute_batch_command(name, new_cs) def _inp_in_option(section: list[str]) -> ChangeSet: diff --git a/api/s23_options_util.py b/api/s23_options_util.py index 5a7a4be..8995c3b 100644 --- a/api/s23_options_util.py +++ b/api/s23_options_util.py @@ -106,7 +106,7 @@ def get_option(name: str) -> dict[str, Any]: return d -def set_option_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_option(name: str, cs: ChangeSet) -> DbChangeSet: raw_old = get_option(name) old = {} @@ -140,9 +140,8 @@ def set_option_cmd(name: str, cs: ChangeSet) -> DbChangeSet: return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) -def set_option_only(name: str, cs: ChangeSet) -> ChangeSet: - v2_cmd = set_option_cmd(name, cs) - return execute_command(name, v2_cmd) +def set_option(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_option(name, cs)) OPTION_V3_FLOW_UNITS_CFS = OPTION_UNITS_CFS @@ -231,7 +230,7 @@ def get_option_v3(name: str) -> dict[str, Any]: return d -def set_option_v3_cmd(name: str, cs: ChangeSet) -> DbChangeSet: +def _set_option_v3(name: str, cs: ChangeSet) -> DbChangeSet: raw_old = get_option_v3(name) old = {} @@ -265,9 +264,8 @@ def set_option_v3_cmd(name: str, cs: ChangeSet) -> DbChangeSet: return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs]) -def set_option_v3_only(name: str, cs: ChangeSet) -> ChangeSet: - v3_cmd = set_option_v3_cmd(name, cs) - return execute_command(name, v3_cmd) +def set_option_v3(name: str, cs: ChangeSet) -> ChangeSet: + return execute_command(name, _set_option_v3(name, cs)) _key_map_23 = { diff --git a/api/s23_options_v3.py b/api/s23_options_v3.py index 4e62c88..0a7738b 100644 --- a/api/s23_options_v3.py +++ b/api/s23_options_v3.py @@ -1,14 +1,5 @@ from .database import * from .s23_options_util import get_option_schema, get_option_v3_schema, generate_v2, generate_v3 -from .batch_cmd import execute_batch_command - - -def set_option_v3(name: str, 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 execute_batch_command(name, new_cs) def _parse_v2(v2_lines: list[str]) -> dict[str, str]: diff --git a/api/s29_scada_device.py b/api/s29_scada_device.py index c4fe561..337277c 100644 --- a/api/s29_scada_device.py +++ b/api/s29_scada_device.py @@ -116,13 +116,3 @@ 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)) - - -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 diff --git a/api/s30_scada_device_data.py b/api/s30_scada_device_data.py index 34ac4e1..5f23800 100644 --- a/api/s30_scada_device_data.py +++ b/api/s30_scada_device_data.py @@ -88,13 +88,3 @@ def delete_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet: if row == None: return ChangeSet() return execute_command(name, _delete_scada_device_data(name, 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 diff --git a/api/s31_scada_element.py b/api/s31_scada_element.py index 798b013..f4739ab 100644 --- a/api/s31_scada_element.py +++ b/api/s31_scada_element.py @@ -178,13 +178,3 @@ 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 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 diff --git a/tjnetwork.py b/tjnetwork.py index b84f50d..ab98e66 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -772,7 +772,7 @@ def get_option(name: str) -> dict[str, Any]: return api.get_option(name) def set_option(name: str, cs: ChangeSet) -> ChangeSet: - return api.set_option(name, cs) + return api.set_option_ex(name, cs) ############################################################ @@ -786,7 +786,7 @@ def get_option_v3(name: str) -> dict[str, Any]: return api.get_option_v3(name) def set_option_v3(name: str, cs: ChangeSet) -> ChangeSet: - return api.set_option_v3(name, cs) + return api.set_option_v3_ex(name, cs) ############################################################ From a15ccecaa7571a6032c095485530477adc36b394 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 16:30:59 +0800 Subject: [PATCH 11/40] Clean code --- api/__init__.py | 28 ++++++------ api/api_batch.py | 70 ----------------------------- api/batch_api.py | 65 +++++++++++++++++++++++++++ api/{api_cs.py => batch_api_cs.py} | 2 +- api/{batch_cmds.py => batch_exe.py} | 14 ++---- 5 files changed, 83 insertions(+), 96 deletions(-) delete mode 100644 api/api_batch.py create mode 100644 api/batch_api.py rename api/{api_cs.py => batch_api_cs.py} (99%) rename api/{batch_cmds.py => batch_exe.py} (96%) diff --git a/api/__init__.py b/api/__init__.py index 16e2e5f..1bd0975 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -19,7 +19,7 @@ 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 .batch_cmds import execute_batch_commands, execute_batch_command +from .batch_exe import execute_batch_commands, execute_batch_command from .s0_base import JUNCTION, RESERVOIR, TANK, PIPE, PUMP, VALVE, PATTERN, CURVE from .s0_base import is_node, is_junction, is_reservoir, is_tank @@ -32,25 +32,25 @@ from .s0_base import get_node_links from .s1_title import get_title_schema, get_title, set_title from .s2_junctions import get_junction_schema, add_junction, get_junction, set_junction -from .api_batch import delete_junction_cascade +from .batch_api import delete_junction_cascade from .s3_reservoirs import get_reservoir_schema, add_reservoir, get_reservoir, set_reservoir -from .api_batch import delete_reservoir_cascade +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 -from .api_batch import delete_tank_cascade +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 -from .api_batch import delete_pipe_cascade +from .batch_api import delete_pipe_cascade from .s6_pumps import get_pump_schema, add_pump, get_pump, set_pump -from .api_batch import delete_pump_cascade +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 -from .api_batch import delete_valve_cascade +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 @@ -61,11 +61,11 @@ 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 .api_batch import delete_pattern_cascade +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 .api_batch import delete_curve_cascade +from .batch_api import delete_curve_cascade from .s13_controls import get_control_schema, get_control, set_control @@ -98,7 +98,7 @@ 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 .api_batch import set_option_ex +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_M @@ -110,7 +110,7 @@ from .s23_options_util import OPTION_V3_LEAKAGE_MODEL_NONE, OPTION_V3_LEAKAGE_MO 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 .api_batch import set_option_v3_ex +from .batch_api import set_option_v3_ex from .s24_coordinates import get_node_coord @@ -122,14 +122,14 @@ 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 from .s29_scada_device import get_scada_device_schema, get_scada_devices, get_scada_device, set_scada_device, add_scada_device, delete_scada_device -from .api_batch import clean_scada_device +from .batch_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 .api_batch import clean_scada_device_data +from .batch_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_elements, get_scada_element, set_scada_element, add_scada_element, delete_scada_element -from .api_batch import clean_scada_element +from .batch_api import clean_scada_element from .s37_virtual_district import calculate_virtual_district \ No newline at end of file diff --git a/api/api_batch.py b/api/api_batch.py deleted file mode 100644 index 091d2d4..0000000 --- a/api/api_batch.py +++ /dev/null @@ -1,70 +0,0 @@ -from .batch_cmds import execute_batch_command -from .api_cs import * - - -def delete_junction_cascade(name: str, cs: ChangeSet) -> ChangeSet: - cs.operations[0] |= { 'operation' : API_DELETE, 'type' : 'junction' } - #raw_cmd = delete_junction_cascade_batch_cmd(name, cs) - return execute_batch_command(name, cs) - - -def delete_reservoir_cascade(name: str, cs: ChangeSet) -> ChangeSet: - cs.operations[0] |= { 'operation' : API_DELETE, 'type' : 'reservoir' } - #raw_cmd = delete_reservoir_cascade_batch_cmd(name, cs) - return execute_batch_command(name, cs) - - -def delete_tank_cascade(name: str, cs: ChangeSet) -> ChangeSet: - cs.operations[0] |= { 'operation' : API_DELETE, 'type' : 'tank' } - #raw_cmd = delete_tank_cascade_batch_cmd(name, cs) - return execute_batch_command(name, cs) - - -def delete_pipe_cascade(name: str, cs: ChangeSet) -> ChangeSet: - cs.operations[0] |= { 'operation' : API_DELETE, 'type' : 'pipe' } - #raw_cmd = delete_pipe_cascade_batch_cmd(name, cs) - return execute_batch_command(name, cs) - - -def delete_pump_cascade(name: str, cs: ChangeSet) -> ChangeSet: - cs.operations[0] |= { 'operation' : API_DELETE, 'type' : 'pump' } - #raw_cmd = delete_pump_cascade_batch_cmd(name, cs) - return execute_batch_command(name, cs) - - -def delete_valve_cascade(name: str, cs: ChangeSet) -> ChangeSet: - cs.operations[0] |= { 'operation' : API_DELETE, 'type' : 'valve' } - #raw_cmd = delete_valve_cascade_batch_cmd(name, cs) - return execute_batch_command(name, cs) - - -def delete_pattern_cascade(name: str, cs: ChangeSet) -> ChangeSet: - cs.operations[0] |= { 'operation' : API_DELETE, 'type' : 'pattern' } - #raw_cmd = delete_pattern_cascade_batch_cmd(name, cs) - return execute_batch_command(name, cs) - - -def delete_curve_cascade(name: str, cs: ChangeSet) -> ChangeSet: - cs.operations[0] |= { 'operation' : API_DELETE, 'type' : 'curve' } - #raw_cmd = delete_curve_cascade_batch_cmd(name, cs) - return execute_batch_command(name, cs) - - -def set_option_ex(name: str, cs: ChangeSet) -> ChangeSet: - return execute_batch_command(name, set_option_cs(cs)) - - -def set_option_v3_ex(name: str, cs: ChangeSet) -> ChangeSet: - return execute_batch_command(name, set_option_v3_cs(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/api/batch_api.py b/api/batch_api.py new file mode 100644 index 0000000..a98884d --- /dev/null +++ b/api/batch_api.py @@ -0,0 +1,65 @@ +from .sections import * +from .batch_api_cs import * +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) + + +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/api/api_cs.py b/api/batch_api_cs.py similarity index 99% rename from api/api_cs.py rename to api/batch_api_cs.py index 33705f4..6db3f3a 100644 --- a/api/api_cs.py +++ b/api/batch_api_cs.py @@ -260,7 +260,7 @@ def clean_scada_element_cs(name: str) -> ChangeSet: return cs -def extend(name: str, cs: ChangeSet) -> ChangeSet: +def rewrite_batch_api(name: str, cs: ChangeSet) -> ChangeSet: op = cs.operations[0] api = op['operation'] type = op['type'] diff --git a/api/batch_cmds.py b/api/batch_exe.py similarity index 96% rename from api/batch_cmds.py rename to api/batch_exe.py index f0cd93a..cb41355 100644 --- a/api/batch_cmds.py +++ b/api/batch_exe.py @@ -29,7 +29,7 @@ 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 .api_cs import extend +from .batch_api_cs import rewrite_batch_api def execute_add_command(name: str, cs: ChangeSet) -> ChangeSet: @@ -264,13 +264,9 @@ def execute_delete_command(name: str, cs: ChangeSet) -> ChangeSet: def execute_batch_commands(name: str, cs: ChangeSet) -> ChangeSet: - # for delete, generate cascade command new_cs = ChangeSet() for op in cs.operations: - if op['operation'] == API_DELETE: - new_cs.merge(extend(name, ChangeSet(op))) - else: - new_cs.merge(ChangeSet(op)) + new_cs.merge(rewrite_batch_api(name, ChangeSet(op))) result = ChangeSet() @@ -296,13 +292,9 @@ 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'") - # for delete, generate cascade command new_cs = ChangeSet() for op in cs.operations: - if op['operation'] == API_DELETE: - new_cs.merge(extend(name, ChangeSet(op))) - else: - new_cs.merge(ChangeSet(op)) + new_cs.merge(rewrite_batch_api(name, ChangeSet(op))) result = ChangeSet() From 2cff3d0a2e6a8a78ec27d0c387e374a13a57137f Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 16:36:36 +0800 Subject: [PATCH 12/40] Clean code --- api/__init__.py | 6 +++--- api/batch_api.py | 14 +------------- api/clean_api.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 api/clean_api.py diff --git a/api/__init__.py b/api/__init__.py index 1bd0975..e8c72de 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -122,14 +122,14 @@ 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 from .s29_scada_device import get_scada_device_schema, get_scada_devices, get_scada_device, set_scada_device, add_scada_device, delete_scada_device -from .batch_api import clean_scada_device +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 .batch_api import clean_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_elements, get_scada_element, set_scada_element, add_scada_element, delete_scada_element -from .batch_api import clean_scada_element +from .clean_api import clean_scada_element from .s37_virtual_district import calculate_virtual_district \ No newline at end of file diff --git a/api/batch_api.py b/api/batch_api.py index a98884d..1c47d54 100644 --- a/api/batch_api.py +++ b/api/batch_api.py @@ -1,5 +1,5 @@ from .sections import * -from .batch_api_cs import * +from .database import ChangeSet, API_DELETE, API_UPDATE from .batch_exe import execute_batch_command @@ -51,15 +51,3 @@ def set_option_ex(name: str, cs: ChangeSet) -> ChangeSet: 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) - - -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/api/clean_api.py b/api/clean_api.py new file mode 100644 index 0000000..178d380 --- /dev/null +++ b/api/clean_api.py @@ -0,0 +1,16 @@ +from .database import ChangeSet +from .batch_api_cs import clean_scada_device_cs, clean_scada_device_data_cs, clean_scada_element_cs +from .batch_exe import execute_batch_command + +# TODO: merge to batch_api + +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)) From e407f8ccd74a5a937af250f122b599d4dc05704e Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 16:40:20 +0800 Subject: [PATCH 13/40] Clean code --- api/batch_api_cs.py | 55 --------------------------------------------- api/clean_api.py | 33 +++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 57 deletions(-) diff --git a/api/batch_api_cs.py b/api/batch_api_cs.py index 6db3f3a..755a8e4 100644 --- a/api/batch_api_cs.py +++ b/api/batch_api_cs.py @@ -191,29 +191,6 @@ def delete_curve_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet: return result -def del_cascade_cs(name: str, cs: ChangeSet) -> ChangeSet: - type = cs.operations[0]['type'] - - 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) - - return cs - - def set_option_cs(cs: ChangeSet) -> ChangeSet: cs.operations[0]['operation'] = API_UPDATE cs.operations[0]['type'] = 'option' @@ -230,36 +207,6 @@ def set_option_v3_cs(cs: ChangeSet) -> ChangeSet: return new_cs -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 rewrite_batch_api(name: str, cs: ChangeSet) -> ChangeSet: op = cs.operations[0] api = op['operation'] @@ -288,6 +235,4 @@ def rewrite_batch_api(name: str, cs: ChangeSet) -> ChangeSet: elif type == s23_option_v3: return set_option_v3_cs(cs) - # TODO: support clean - return cs diff --git a/api/clean_api.py b/api/clean_api.py index 178d380..ba3d7f8 100644 --- a/api/clean_api.py +++ b/api/clean_api.py @@ -1,9 +1,38 @@ -from .database import ChangeSet -from .batch_api_cs import clean_scada_device_cs, clean_scada_device_data_cs, clean_scada_element_cs +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)) From a3e8f693d929788dc820b9ef362172a4f5354e2c Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 17:21:29 +0800 Subject: [PATCH 14/40] Add test for virtual district --- test_tjnetwork.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test_tjnetwork.py b/test_tjnetwork.py index 2d0ddaa..df4ea32 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -5564,5 +5564,42 @@ class TestApi: self.leave(p) + # 37 virtual_district + + def test_virtual_district(self): + p = 'test_virtual_district' + read_inp(p, f'./inp/net3.inp', '3') + + open_project(p) + + result = calculate_virtual_district(p, ['107', '139', '267', '211']) + + assert result == { + 'virtual_districts':[ + { + 'center': '107', + 'nodes': ['10', '101', '103', '105', '109', '111', '115', '117', '119', '120', '139', '257', '259', '261', '263', '267', 'Lake'], + 'boundary': [(23.38, 12.95), (12.96, 21.31), (8.0, 27.53), (9.0, 27.85), (33.28, 24.54), (23.38, 12.95)] + }, + { + 'center': '139', + 'nodes': ['15', '20', '60', '601', '61', '121', '123', '125', '127', '129', '131', '141', '143', '145', '147', '149', '151', '153', 'River', '3'], + 'boundary': [(33.02, 19.29), (30.24, 20.38), (28.29, 21.39), (23.54, 25.5), (23.0, 29.49), (24.15, 31.06), (37.89, 29.55), (38.68, 23.76), (37.47, 21.97), (33.02, 19.29)] + }, + { + 'center': '267', + 'nodes': ['35', '40', '113', '157', '159', '161', '163', '164', '166', '167', '169', '171', '173', '177', '179', '181', '183', '184', '185', '187', '189', '191', '193', '195', '197', '204', '211', '265', '269', '271', '1', '107'], + 'boundary': [(34.2, 5.54), (25.15, 9.52), (23.64, 11.04), (20.97, 15.18), (18.45, 20.46), (24.85, 20.16), (34.2, 5.54)] + }, + { + 'center': '211', + 'nodes': ['50', '199', '201', '203', '205', '206', '207', '208', '209', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '2'], + 'boundary': [(37.04, 0.0), (34.15, 1.1), (32.17, 1.88), (29.2, 6.46), (29.16, 7.38), (29.42, 8.44), (31.14, 8.89), (44.86, 9.32), (43.53, 7.38), (37.04, 0.0)] + }], + 'isolated_nodes': []} + + self.leave(p) + + if __name__ == '__main__': pytest.main() From d66087225f84ec377cd90193b00303ca557a9570 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 17:51:34 +0800 Subject: [PATCH 15/40] Support database region --- api/__init__.py | 4 +- api/s32_region_util.py | 15 ++++++++ api/s33_region.py | 81 +++++++++++++++++++++++++++++++++++++++++ test_tjnetwork.py | 83 ++++++++++++++++++++++++++++++++++++++++++ tjnetwork.py | 43 +++++++++++++++++++++- 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 api/s32_region_util.py create mode 100644 api/s33_region.py diff --git a/api/__init__.py b/api/__init__.py index e8c72de..ac46621 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -132,4 +132,6 @@ from .s31_scada_element import SCADA_ELEMENT_STATUS_OFFLINE, SCADA_ELEMENT_STATU from .s31_scada_element import get_scada_element_schema, get_scada_elements, get_scada_element, set_scada_element, add_scada_element, delete_scada_element from .clean_api import clean_scada_element -from .s37_virtual_district import calculate_virtual_district \ No newline at end of file +from .s33_region import get_region_schema, get_region, set_region, add_region, delete_region + +from .s37_virtual_district import calculate_virtual_district diff --git a/api/s32_region_util.py b/api/s32_region_util.py new file mode 100644 index 0000000..7a32c65 --- /dev/null +++ b/api/s32_region_util.py @@ -0,0 +1,15 @@ + +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 f'polygon(({polygon[:-1]}))' diff --git a/api/s33_region.py b/api/s33_region.py new file mode 100644 index 0000000..7ef0f1e --- /dev/null +++ b/api/s33_region.py @@ -0,0 +1,81 @@ +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() + if len(cs.operations[0]['boundary']) < 3: + 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() + if len(cs.operations[0]['boundary']) < 3: + 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)) diff --git a/test_tjnetwork.py b/test_tjnetwork.py index df4ea32..cdaf9ee 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -5564,6 +5564,89 @@ class TestApi: self.leave(p) + # 33 region + + + def test_region(self): + p = 'test_region' + self.enter(p) + + r = get_region(p, 'r') + assert r == {} + + add_region(p, ChangeSet({'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]})) + r = get_region(p, 'r') + assert r == { 'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] } + + set_region(p, ChangeSet({'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)]})) + r = get_region(p, 'r') + assert r == { 'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)] } + + delete_region(p, ChangeSet({'id': 'r'})) + r = get_region(p, 'r') + assert r == {} + + self.leave(p) + + + def test_region_op(self): + p = 'test_region_op' + self.enter(p) + + cs = add_region(p, ChangeSet({'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]})).operations[0] + assert cs['operation'] == API_ADD + assert cs['type'] == 'region' + assert cs['id'] == 'r' + assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] + + cs = execute_undo(p).operations[0] + assert cs['operation'] == API_DELETE + assert cs['type'] == 'region' + assert cs['id'] == 'r' + + cs = execute_redo(p).operations[0] + assert cs['operation'] == API_ADD + assert cs['type'] == 'region' + assert cs['id'] == 'r' + assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] + + cs = set_region(p, ChangeSet({'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)]})).operations[0] + assert cs['operation'] == API_UPDATE + assert cs['type'] == 'region' + assert cs['id'] == 'r' + assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)] + + cs = execute_undo(p).operations[0] + assert cs['operation'] == API_UPDATE + assert cs['type'] == 'region' + assert cs['id'] == 'r' + assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] + + cs = execute_redo(p).operations[0] + assert cs['operation'] == API_UPDATE + assert cs['type'] == 'region' + assert cs['id'] == 'r' + assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)] + + cs = delete_region(p, ChangeSet({'id': 'r'})).operations[0] + assert cs['operation'] == API_DELETE + assert cs['type'] == 'region' + assert cs['id'] == 'r' + + cs = execute_undo(p).operations[0] + assert cs['operation'] == API_ADD + assert cs['type'] == 'region' + assert cs['id'] == 'r' + assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)] + + cs = execute_redo(p).operations[0] + assert cs['operation'] == API_DELETE + assert cs['type'] == 'region' + assert cs['id'] == 'r' + + self.leave(p) + + # 37 virtual_district def test_virtual_district(self): diff --git a/tjnetwork.py b/tjnetwork.py index ab98e66..874c71d 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -936,7 +936,48 @@ def clean_scada_element(name: str) -> ChangeSet: ############################################################ -# virtual_district 32 +# region_util 32 +############################################################ + + +############################################################ +# general_region 33 +############################################################ + +def get_region_schema(name: str) -> dict[str, dict[str, Any]]: + return api.get_region_schema(name) + +def get_region(name: str, id: str) -> dict[str, Any]: + return api.get_region(name, id) + +def set_region(name: str, cs: ChangeSet) -> ChangeSet: + return api.set_region(name, cs) + +# example: add_region(p, ChangeSet({'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]})) +def add_region(name: str, cs: ChangeSet) -> ChangeSet: + return api.add_region(name, cs) + +def delete_region(name: str, cs: ChangeSet) -> ChangeSet: + return api.delete_region(name, cs) + + +############################################################ +# water_distribution 34 +############################################################ + + +############################################################ +# district_metering_area 35 +############################################################ + + +############################################################ +# service_area 36 +############################################################ + + +############################################################ +# virtual_district 37 ############################################################ def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: From 538284f5023b36468cfe42137ccd8ea3e985e309 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 17:58:29 +0800 Subject: [PATCH 16/40] Add more checking for region api --- api/s33_region.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/s33_region.py b/api/s33_region.py index 7ef0f1e..9d90e20 100644 --- a/api/s33_region.py +++ b/api/s33_region.py @@ -32,7 +32,8 @@ def _set_region(name: str, cs: ChangeSet) -> DbChangeSet: def set_region(name: str, cs: ChangeSet) -> ChangeSet: if 'id' not in cs.operations[0] or 'boundary' not in cs.operations[0]: return ChangeSet() - if len(cs.operations[0]['boundary']) < 3: + 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() @@ -54,7 +55,8 @@ def _add_region(name: str, cs: ChangeSet) -> DbChangeSet: def add_region(name: str, cs: ChangeSet) -> ChangeSet: if 'id' not in cs.operations[0] or 'boundary' not in cs.operations[0]: return ChangeSet() - if len(cs.operations[0]['boundary']) < 3: + 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() From 04a3d0057e2deaa311ec960ac2a3b7fdd493580a Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sat, 29 Apr 2023 19:15:34 +0800 Subject: [PATCH 17/40] Support more region utils, such as convex hull --- api/__init__.py | 2 ++ api/s32_region_util.py | 34 ++++++++++++++++++++++++++ api/s37_virtual_district.py | 36 +++++++--------------------- script/sql/create/32.region.sql | 24 +++++++++++++++++++ script/sql/drop/32.region.sql | 8 +++++++ test_tjnetwork.py | 42 ++++++++++++++++++++++++++++++++- tjnetwork.py | 9 +++++++ 7 files changed, 126 insertions(+), 29 deletions(-) diff --git a/api/__init__.py b/api/__init__.py index ac46621..86f7d27 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -132,6 +132,8 @@ from .s31_scada_element import SCADA_ELEMENT_STATUS_OFFLINE, SCADA_ELEMENT_STATU from .s31_scada_element import get_scada_element_schema, get_scada_elements, get_scada_element, set_scada_element, add_scada_element, delete_scada_element from .clean_api import clean_scada_element +from .s32_region_util import get_nodes_in_boundary, get_nodes_in_region, calculate_convex_hull + from .s33_region import get_region_schema, get_region, set_region, add_region, delete_region from .s37_virtual_district import calculate_virtual_district diff --git a/api/s32_region_util.py b/api/s32_region_util.py index 7a32c65..4dfe5fe 100644 --- a/api/s32_region_util.py +++ b/api/s32_region_util.py @@ -1,3 +1,4 @@ +from .database import read, read_all, write def from_postgis_polygon(polygon: str) -> list[tuple[float, float]]: boundary = polygon.lower().removeprefix('polygon((').removesuffix('))').split(',') @@ -13,3 +14,36 @@ def to_postgis_polygon(boundary: list[tuple[float, float]]) -> str: for pt in boundary: polygon += f'{pt[0]} {pt[1]},' return f'polygon(({polygon[:-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_nodes_in_region(name: str, id: str) -> list[str]: + nodes: list[str] = [] + 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 = '{id}'"): + nodes.append(row['node']) + return 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) diff --git a/api/s37_virtual_district.py b/api/s37_virtual_district.py index 93e8d1d..f878c36 100644 --- a/api/s37_virtual_district.py +++ b/api/s37_virtual_district.py @@ -1,19 +1,10 @@ from .database import * from .s0_base import get_node_links - - -def _polygon_to_nodes(polygon: str) -> list[tuple[float, float]]: - boundary = polygon.removeprefix('POLYGON((').removesuffix('))').split(',') - xys = [] - for pt in boundary: - xy = pt.split(' ') - xys.append((float(xy[0]), float(xy[1]))) - return xys +from .s32_region_util import calculate_convex_hull def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: - write(name, 'drop table if exists vd_graph') - write(name, 'create table vd_graph (id serial, source integer, target integer, cost numeric)') + write(name, 'delete from temp_vd_topology') # map node name to index i = 0 @@ -33,17 +24,17 @@ def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: source = node_index[str(pipe['node1'])] target = node_index[str(pipe['node2'])] cost = float(pipe['length']) - write(name, f"insert into vd_graph (source, target, cost) values ({source}, {target}, {cost})") + 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 vd_graph (source, target, cost) values ({source}, {target}, 0.0)") + 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 vd_graph (source, target, cost) values ({source}, {target}, 0.0)") + 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]] = {} @@ -52,14 +43,13 @@ def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: if node == center: continue # TODO: check none - distance = float(read(name, f"select max(agg_cost) as distance from pgr_dijkstraCost('select id, source, target, cost from vd_graph', {index}, {node_index[center]}, false)")['distance']) + 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 } - # destroy topology graph - write(name, 'drop table vd_graph') + write(name, 'delete from temp_vd_topology') # reorganize the distance result center_node: dict[str, list[str]] = {} @@ -71,17 +61,7 @@ def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: vds: list[dict[str, Any]] = [] for center, value in center_node.items(): - write(name, f'drop table if exists vd_{center}') - write(name, f'create table vd_{center} (node varchar(32) primary key references _node(id))') - - for node in value: - write(name, f"insert into vd_{center} values ('{node}')") - - # TODO: check none - boundary = read(name, f'select st_astext(st_convexhull(st_collect(array(select coord from coordinates where node in (select * from vd_{center}))))) as boundary' )['boundary'] - xys = _polygon_to_nodes(boundary) + xys = calculate_convex_hull(name, value) vds.append({ 'center': center, 'nodes': value, 'boundary': xys }) - write(name, f'drop table vd_{center}') - return { 'virtual_districts': vds, 'isolated_nodes': isolated_nodes } diff --git a/script/sql/create/32.region.sql b/script/sql/create/32.region.sql index 0fcf92c..eea878e 100644 --- a/script/sql/create/32.region.sql +++ b/script/sql/create/32.region.sql @@ -5,3 +5,27 @@ create table region ); create index region_gist on region using gist(boundary); + + +create table temp_region +( + id text primary key +, boundary geometry not null unique +); + +create index temp_region_gist on temp_region using gist(boundary); + + +create table temp_node +( + node varchar(32) primary key references _node(id) +); + + +create table temp_vd_topology +( + id serial +, source integer +, target integer +, cost numeric +); diff --git a/script/sql/drop/32.region.sql b/script/sql/drop/32.region.sql index b30dd82..71bc330 100644 --- a/script/sql/drop/32.region.sql +++ b/script/sql/drop/32.region.sql @@ -1,3 +1,11 @@ +drop table if exists temp_vd_topology; + +drop table if exists temp_node; + +drop index if exists temp_region_gist; + +drop table if exists temp_region; + drop index if exists region_gist; drop table if exists region; diff --git a/test_tjnetwork.py b/test_tjnetwork.py index cdaf9ee..de1d392 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -5564,6 +5564,47 @@ class TestApi: self.leave(p) + # 32 region_util + + + def test_get_nodes_in_boundary(self): + p = 'test_get_nodes_in_boundary' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts'] + nodes = get_nodes_in_boundary(p, vds[0]['boundary']) + assert nodes == ['10', '101', '103', '105', '107', '109', '111', '113', '115', '117', '119', '120', '121', '125', '139', '149', '151', '153', '157', '159', '161', '191', '193', '195', '197', '257', '259', '261', '263', '267', 'Lake'] + + self.leave(p) + + + def test_get_nodes_in_region(self): + p = 'test_get_nodes_in_region' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts'] + add_region(p, ChangeSet({'id': 'r', 'boundary': vds[0]['boundary']})) + + nodes = get_nodes_in_region(p, 'r') + assert nodes == ['10', '101', '103', '105', '107', '109', '111', '113', '115', '117', '119', '120', '121', '125', '139', '149', '151', '153', '157', '159', '161', '191', '193', '195', '197', '257', '259', '261', '263', '267', 'Lake'] + + self.leave(p) + + + def test_calculate_convex_hull(self): + p = 'test_calculate_convex_hull' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + nodes = ['10', '101', '103', '105', '109', '111', '115', '117', '119', '120', '139', '257', '259', '261', '263', '267', 'Lake'] + ch = calculate_convex_hull(p, nodes) + assert ch == [(23.38, 12.95), (12.96, 21.31), (8.0, 27.53), (9.0, 27.85), (33.28, 24.54), (23.38, 12.95)] + + self.leave(p) + + # 33 region @@ -5652,7 +5693,6 @@ class TestApi: def test_virtual_district(self): p = 'test_virtual_district' read_inp(p, f'./inp/net3.inp', '3') - open_project(p) result = calculate_virtual_district(p, ['107', '139', '267', '211']) diff --git a/tjnetwork.py b/tjnetwork.py index 874c71d..7c5c298 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -939,6 +939,15 @@ def clean_scada_element(name: str) -> ChangeSet: # region_util 32 ############################################################ +def get_nodes_in_boundary(name: str, boundary: list[tuple[float, float]]) -> list[str]: + return api.get_nodes_in_boundary(name, boundary) + +def get_nodes_in_region(name: str, id: str) -> list[str]: + return api.get_nodes_in_region(name, id) + +def calculate_convex_hull(name: str, nodes: list[str]) -> list[tuple[float, float]]: + return api.calculate_convex_hull(name, nodes) + ############################################################ # general_region 33 From 3c233f6e9c7c5ce5eb07165841764f5e850b671d Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 11:28:17 +0800 Subject: [PATCH 18/40] Add clipper2 c wrapper lib --- api/CClipper2.dll | Bin 0 -> 92672 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 api/CClipper2.dll diff --git a/api/CClipper2.dll b/api/CClipper2.dll new file mode 100644 index 0000000000000000000000000000000000000000..d21c60636824e1afa34d8f19b13a9d046b9ae3a5 GIT binary patch literal 92672 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%0A%Zm+H;OF4r63;@We( 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 literal 0 HcmV?d00001 From 5376ead768e7d64a6a4d81f17fba7f20d4e959c5 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 11:28:40 +0800 Subject: [PATCH 19/40] Add more interface to base --- api/s0_base.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api/s0_base.py b/api/s0_base.py index 5609f8f..bace826 100644 --- a/api/s0_base.py +++ b/api/s0_base.py @@ -1,5 +1,6 @@ from psycopg.rows import dict_row, Row from .connection import g_conn_dict as conn +from .database import read _NODE = '_node' @@ -105,3 +106,14 @@ def get_node_links(name: str, id: str) -> list[str]: 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'])] From c4b01fc48cb25580e27b9cfa7f7657e95ae6a837 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 11:28:57 +0800 Subject: [PATCH 20/40] Check if node has coordinate --- api/s24_coordinates.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/s24_coordinates.py b/api/s24_coordinates.py index 09a30d5..6c36216 100644 --- a/api/s24_coordinates.py +++ b/api/s24_coordinates.py @@ -28,6 +28,10 @@ def get_node_coord(name: str, node: str) -> dict[str, float]: return from_postgis_point(row['coord_geom']) +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 From e033a3c3c7fcb2b65fb1d2581a4ec17a8fa2e1d8 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 11:29:31 +0800 Subject: [PATCH 21/40] Add temp link for region calculation --- script/sql/create/32.region.sql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/script/sql/create/32.region.sql b/script/sql/create/32.region.sql index eea878e..a8dff0f 100644 --- a/script/sql/create/32.region.sql +++ b/script/sql/create/32.region.sql @@ -22,6 +22,12 @@ create table temp_node ); +create table temp_link +( + link varchar(32) primary key references _link(id) +); + + create table temp_vd_topology ( id serial From 93ec527fff9d0fa0ab1eb98cb0a2e8049434bb24 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 11:30:33 +0800 Subject: [PATCH 22/40] Add more algorithms, such as path, inflate --- api/__init__.py | 2 +- api/s32_region_util.py | 174 ++++++++++++++++++++++++++++++++++++++++- tjnetwork.py | 14 +++- 3 files changed, 184 insertions(+), 6 deletions(-) diff --git a/api/__init__.py b/api/__init__.py index 86f7d27..79f4a17 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -132,7 +132,7 @@ from .s31_scada_element import SCADA_ELEMENT_STATUS_OFFLINE, SCADA_ELEMENT_STATU from .s31_scada_element import get_scada_element_schema, get_scada_elements, get_scada_element, set_scada_element, add_scada_element, delete_scada_element from .clean_api import clean_scada_element -from .s32_region_util import get_nodes_in_boundary, get_nodes_in_region, calculate_convex_hull +from .s32_region_util import get_nodes_in_boundary, get_nodes_in_region, calculate_convex_hull, calculate_boundary, inflate_boundary, inflate_region from .s33_region import get_region_schema, get_region, set_region, add_region, delete_region diff --git a/api/s32_region_util.py b/api/s32_region_util.py index 4dfe5fe..6e1f7f9 100644 --- a/api/s32_region_util.py +++ b/api/s32_region_util.py @@ -1,4 +1,12 @@ -from .database import read, read_all, write +import ctypes +import platform +import os +import math +import collections +from .s0_base import get_node_links, get_link_nodes +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(',') @@ -30,9 +38,9 @@ def get_nodes_in_boundary(name: str, boundary: list[tuple[float, float]]) -> lis return nodes -def get_nodes_in_region(name: str, id: str) -> list[str]: +def get_nodes_in_region(name: str, region_id: str) -> list[str]: nodes: list[str] = [] - 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 = '{id}'"): + 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 @@ -47,3 +55,163 @@ def calculate_convex_hull(name: str, nodes: list[str]) -> list[tuple[float, floa 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.asin(v[1]) + math.pi * 0.5 + 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.asin(-v[1]) + math.pi * 1.5 + 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'] + v = _normal((x2 - x1, y2 - y1)) + return _angle(v) + + +def calculate_boundary(name: str, nodes: list[str]) -> list[tuple[float, float]]: + new_nodes = {} + max_x_node = '' + for node in nodes: + if not node_has_coord(name, node): + continue + if get_node_links(name, node) == 0: + continue + new_nodes[node] = get_node_coord(name, node) | { 'links': [] } + if max_x_node == '' or new_nodes[node]['x'] > new_nodes[max_x_node]['x']: + max_x_node = node + + new_links = {} + for node in new_nodes: + for link in get_node_links(name, node): + candidate = True + link_nodes = get_link_nodes(name, link) + for link_node in link_nodes: + if link_node not in new_nodes: + candidate = False + break + if candidate: + new_links[link] = { 'node1' : link_nodes[0], 'node2' : link_nodes[1] } + if link not in new_nodes[link_nodes[0]]['links']: + new_nodes[link_nodes[0]]['links'].append(link) + if link not in new_nodes[link_nodes[1]]['links']: + new_nodes[link_nodes[1]]['links'].append(link) + + cursor = max_x_node + in_angle = 0 + + paths: list[str] = [] + while True: + paths.append(cursor) + sorted_links = [] + overlapped_link = '' + for link in new_nodes[cursor]['links']: + angle = _angle_of_node_link(cursor, link, new_nodes, new_links) + if angle == in_angle: + overlapped_link = link + continue + sorted_links.append((angle, link)) + + # work into a branch, return + if len(sorted_links) == 0: + cursor = paths[-2] + in_angle = in_angle = _angle_of_node_link(cursor, overlapped_link, new_nodes, new_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 + + cursor = new_links[out_link]['node1'] if cursor == new_links[out_link]['node2'] else new_links[out_link]['node2'] + # end up trip :) + if cursor == max_x_node: + paths.append(cursor) + break + + in_angle = _angle_of_node_link(cursor, out_link, new_nodes, new_links) + + boundary: list[tuple[float, float]] = [] + for node in paths: + boundary.append((new_nodes[node]['x'], new_nodes[node]['y'])) + + 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) + c_jt = ctypes.c_int(0) + c_et = ctypes.c_int(0) + 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/tjnetwork.py b/tjnetwork.py index 7c5c298..8a78435 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -942,12 +942,21 @@ def clean_scada_element(name: str) -> ChangeSet: def get_nodes_in_boundary(name: str, boundary: list[tuple[float, float]]) -> list[str]: return api.get_nodes_in_boundary(name, boundary) -def get_nodes_in_region(name: str, id: str) -> list[str]: - return api.get_nodes_in_region(name, id) +def get_nodes_in_region(name: str, region_id: str) -> list[str]: + return api.get_nodes_in_region(name, region_id) def calculate_convex_hull(name: str, nodes: list[str]) -> list[tuple[float, float]]: return api.calculate_convex_hull(name, nodes) +def calculate_boundary(name: str, nodes: list[str]) -> list[tuple[float, float]]: + return api.calculate_boundary(name, nodes) + +def inflate_boundary(name: str, boundary: list[tuple[float, float]], delta: float = 0.5) -> list[tuple[float, float]]: + return api.inflate_boundary(name, boundary, delta) + +def inflate_region(name: str, region_id: str, delta: float = 0.5) -> list[tuple[float, float]]: + return api.inflate_region(name, region_id, delta) + ############################################################ # general_region 33 @@ -989,5 +998,6 @@ def delete_region(name: str, cs: ChangeSet) -> ChangeSet: # virtual_district 37 ############################################################ +# parent is whole network def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: return api.calculate_virtual_district(name, centers) From 493b46fd4e8500bdb635c8a438cdf81664fad517 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 11:30:47 +0800 Subject: [PATCH 23/40] Refine virtual district --- api/s37_virtual_district.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/api/s37_virtual_district.py b/api/s37_virtual_district.py index f878c36..b019fd7 100644 --- a/api/s37_virtual_district.py +++ b/api/s37_virtual_district.py @@ -1,6 +1,6 @@ from .database import * from .s0_base import get_node_links -from .s32_region_util import calculate_convex_hull +from .s32_region_util import calculate_boundary, calculate_convex_hull, inflate_boundary def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: @@ -41,6 +41,7 @@ def calculate_virtual_district(name: str, centers: list[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']) @@ -61,7 +62,9 @@ def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: vds: list[dict[str, Any]] = [] for center, value in center_node.items(): - xys = calculate_convex_hull(name, value) - vds.append({ 'center': center, 'nodes': value, 'boundary': xys }) + p = calculate_boundary(name, value) + b = inflate_boundary(name, p) + c = calculate_convex_hull(name, value) + vds.append({ 'center': center, 'nodes': value, 'path': p, 'boundary': b, 'convex_hull': c}) return { 'virtual_districts': vds, 'isolated_nodes': isolated_nodes } From a4dc2d62cc06dd1bbb4fef255a983061df12e921 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 11:31:14 +0800 Subject: [PATCH 24/40] Add test for new utils --- test_tjnetwork.py | 60 +++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/test_tjnetwork.py b/test_tjnetwork.py index de1d392..14e218f 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -5574,7 +5574,7 @@ class TestApi: vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts'] nodes = get_nodes_in_boundary(p, vds[0]['boundary']) - assert nodes == ['10', '101', '103', '105', '107', '109', '111', '113', '115', '117', '119', '120', '121', '125', '139', '149', '151', '153', '157', '159', '161', '191', '193', '195', '197', '257', '259', '261', '263', '267', 'Lake'] + assert nodes == ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'] self.leave(p) @@ -5588,7 +5588,7 @@ class TestApi: add_region(p, ChangeSet({'id': 'r', 'boundary': vds[0]['boundary']})) nodes = get_nodes_in_region(p, 'r') - assert nodes == ['10', '101', '103', '105', '107', '109', '111', '113', '115', '117', '119', '120', '121', '125', '139', '149', '151', '153', '157', '159', '161', '191', '193', '195', '197', '257', '259', '261', '263', '267', 'Lake'] + assert nodes == ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'] self.leave(p) @@ -5598,9 +5598,9 @@ class TestApi: read_inp(p, f'./inp/net3.inp', '3') open_project(p) - nodes = ['10', '101', '103', '105', '109', '111', '115', '117', '119', '120', '139', '257', '259', '261', '263', '267', 'Lake'] + nodes = ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'] ch = calculate_convex_hull(p, nodes) - assert ch == [(23.38, 12.95), (12.96, 21.31), (8.0, 27.53), (9.0, 27.85), (33.28, 24.54), (23.38, 12.95)] + assert ch == [(20.21, 17.53), (12.96, 21.31), (8.0, 27.53), (9.0, 27.85), (23.7, 22.76), (20.21, 17.53)] self.leave(p) @@ -5698,27 +5698,37 @@ class TestApi: result = calculate_virtual_district(p, ['107', '139', '267', '211']) assert result == { - 'virtual_districts':[ - { - 'center': '107', - 'nodes': ['10', '101', '103', '105', '109', '111', '115', '117', '119', '120', '139', '257', '259', '261', '263', '267', 'Lake'], - 'boundary': [(23.38, 12.95), (12.96, 21.31), (8.0, 27.53), (9.0, 27.85), (33.28, 24.54), (23.38, 12.95)] - }, - { - 'center': '139', - 'nodes': ['15', '20', '60', '601', '61', '121', '123', '125', '127', '129', '131', '141', '143', '145', '147', '149', '151', '153', 'River', '3'], - 'boundary': [(33.02, 19.29), (30.24, 20.38), (28.29, 21.39), (23.54, 25.5), (23.0, 29.49), (24.15, 31.06), (37.89, 29.55), (38.68, 23.76), (37.47, 21.97), (33.02, 19.29)] - }, - { - 'center': '267', - 'nodes': ['35', '40', '113', '157', '159', '161', '163', '164', '166', '167', '169', '171', '173', '177', '179', '181', '183', '184', '185', '187', '189', '191', '193', '195', '197', '204', '211', '265', '269', '271', '1', '107'], - 'boundary': [(34.2, 5.54), (25.15, 9.52), (23.64, 11.04), (20.97, 15.18), (18.45, 20.46), (24.85, 20.16), (34.2, 5.54)] - }, - { - 'center': '211', - 'nodes': ['50', '199', '201', '203', '205', '206', '207', '208', '209', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '2'], - 'boundary': [(37.04, 0.0), (34.15, 1.1), (32.17, 1.88), (29.2, 6.46), (29.16, 7.38), (29.42, 8.44), (31.14, 8.89), (44.86, 9.32), (43.53, 7.38), (37.04, 0.0)] - }], + 'virtual_districts': + [ + { + 'center': '107', + 'nodes': ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'], + 'path': [(23.7, 22.76), (22.08, 23.1), (21.17, 23.32), (20.8, 23.4), (20.32, 21.57), (16.97, 21.28), (13.81, 22.94), (9.0, 27.85), (8.0, 27.53), (9.0, 27.85), (13.81, 22.94), (12.96, 21.31), (17.64, 18.92), (20.21, 17.53), (20.98, 19.18), (21.69, 21.28), (22.08, 23.1)], + 'boundary': [(20.57, 17.12), (21.44, 18.98), (21.45, 19.01), (22.17, 21.13), (22.18, 21.16), (22.46, 22.5), (24.09, 22.17), (24.29, 23.150000000000002), (22.19, 23.580000000000002), (22.2, 23.59), (21.28, 23.81), (20.71, 23.93), (20.37, 23.72), (19.92, 22.03), (17.06, 21.79), (14.120000000000001, 23.330000000000002), (9.26, 28.3), (8.98, 28.37), (7.37, 27.85), (7.68, 26.900000000000002), (8.85, 27.27), (13.19, 22.84), (12.42, 21.36), (12.55, 20.96), (17.41, 18.47), (20.16, 16.990000000000002), (20.57, 17.12)], + 'convex_hull': [(20.21, 17.53), (12.96, 21.31), (8.0, 27.53), (9.0, 27.85), (23.7, 22.76), (20.21, 17.53)] + }, + { + 'center': '139', + 'nodes': ['15', '20', '60', '601', '61', '121', '123', '125', '127', '129', '131', '139', '141', '143', '145', '147', '149', '151', '153', 'River', '3'], + 'path': [(38.68, 23.76), (37.47, 21.97), (35.68, 23.08), (33.28, 24.54), (30.32, 26.39), (37.89, 29.55), (30.32, 26.39), (29.29, 26.4), (29.44, 26.91), (29.41, 27.27), (29.44, 26.91), (29.29, 26.4), (24.59, 25.64), (23.54, 25.5), (23.37, 27.31), (23.71, 29.03), (23.9, 29.94), (24.15, 31.06), (23.9, 29.94), (23.0, 29.49), (23.71, 29.03), (23.37, 27.31), (23.54, 25.5), (24.59, 25.64), (28.13, 22.63), (28.29, 21.39), (29.62, 20.74), (30.24, 20.38), (33.02, 19.29), (35.68, 23.08), (37.47, 21.97)], + 'boundary': [(33.33, 18.86), (35.82, 22.400000000000002), (37.38, 21.44), (37.77, 21.52), (39.37, 23.89), (38.550000000000004, 24.45), (37.32, 22.64), (35.94, 23.51), (33.54, 24.97), (33.53, 24.96), (31.39, 26.29), (38.54, 29.28), (38.160000000000004, 30.2), (30.22, 26.89), (29.93, 26.89), (29.94, 26.91), (29.87, 27.810000000000002), (28.87, 27.73), (28.93, 26.95), (28.89, 26.830000000000002), (24.52, 26.13), (24.52, 26.14), (23.990000000000002, 26.060000000000002), (23.87, 27.29), (24.2, 28.93), (24.39, 29.830000000000002), (24.75, 31.44), (23.77, 31.66), (23.46, 30.28), (22.52, 29.810000000000002), (22.48, 29.23), (23.150000000000002, 28.79), (22.87, 27.37), (22.87, 27.3), (23.06, 25.240000000000002), (23.400000000000002, 24.98), (24.44, 25.11), (27.650000000000002, 22.38), (27.810000000000002, 21.2), (27.96, 21.0), (29.400000000000002, 20.29), (30.01, 19.94), (30.04, 19.92), (33.0, 18.76), (33.33, 18.86)], + 'convex_hull': [(33.02, 19.29), (30.24, 20.38), (28.29, 21.39), (23.54, 25.5), (23.0, 29.49), (24.15, 31.06), (37.89, 29.55), (38.68, 23.76), (37.47, 21.97), (33.02, 19.29)] + }, + { + 'center': '267', + 'nodes': ['35', '40', '113', '157', '159', '161', '163', '164', '166', '167', '169', '171', '173', '177', '179', '181', '183', '184', '185', '187', '189', '191', '193', '195', '197', '204', '265', '267', '269', '271', '1'], + 'path': [(27.46, 9.84), (27.02, 9.81), (25.71, 10.4), (25.45, 10.18), (24.15, 11.37), (25.03, 12.14), (25.97, 11.0), (25.72, 10.74), (25.46, 10.52), (25.72, 10.74), (25.97, 11.0), (26.65, 11.8), (26.87, 11.59), (26.65, 11.8), (25.68, 12.74), (25.88, 12.98), (25.68, 12.74), (25.39, 13.6), (25.39, 14.98), (25.98, 15.14), (26.48, 15.13), (25.98, 15.14), (25.39, 14.98), (25.1, 15.28), (23.12, 17.5), (24.85, 20.16), (23.12, 17.5), (25.1, 15.28), (23.18, 14.72), (22.88, 14.35), (22.04, 16.61), (22.88, 14.35), (22.1, 14.07), (20.97, 15.18), (22.1, 14.07), (23.64, 11.04), (23.8, 10.9), (25.01, 9.67), (25.15, 9.52), (25.01, 9.67), (25.45, 10.18), (25.71, 10.4), (27.02, 9.81)], + 'boundary': [(25.86, 9.5), (25.68, 9.68), (25.79, 9.8), (26.87, 9.33), (26.990000000000002, 9.31), (27.990000000000002, 9.38), (27.92, 10.370000000000001), (27.12, 10.31), (26.34, 10.66), (26.35, 10.67), (26.68, 11.07), (26.89, 10.88), (27.580000000000002, 11.61), (27.0, 12.16), (26.36, 12.77), (26.580000000000002, 13.040000000000001), (25.91, 13.6), (25.89, 13.67), (25.89, 14.6), (26.03, 14.63), (26.97, 14.620000000000001), (26.990000000000002, 15.620000000000001), (25.95, 15.64), (25.88, 15.63), (25.54, 15.530000000000001), (25.46, 15.63), (25.45, 15.620000000000001), (23.740000000000002, 17.54), (25.54, 20.31), (24.7, 20.85), (22.61, 17.63), (22.63, 17.29), (24.2, 15.530000000000001), (23.09, 15.21), (22.330000000000002, 17.25), (21.400000000000002, 16.9), (22.23, 14.64), (22.22, 14.64), (20.96, 15.89), (20.26, 15.17), (21.68, 13.77), (23.22, 10.77), (23.27, 10.700000000000001), (23.47, 10.52), (24.650000000000002, 9.32), (25.13, 8.81), (25.86, 9.5)], + 'convex_hull': [(25.15, 9.52), (23.64, 11.04), (20.97, 15.18), (22.04, 16.61), (24.85, 20.16), (26.48, 15.13), (27.46, 9.84), (25.15, 9.52)] + }, + { + 'center': '211', + 'nodes': ['50', '199', '201', '203', '205', '206', '207', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '2'], + 'path': [(44.86, 9.32), (42.11, 8.67), (39.95, 8.73), (35.26, 6.16), (34.2, 5.54), (33.76, 6.59), (32.54, 6.81), (31.66, 6.64), (31.0, 6.61), (31.07, 8.29), (30.89, 8.57), (31.14, 8.89), (30.89, 8.57), (29.42, 8.44), (29.16, 7.38), (29.2, 6.46), (31.0, 6.61), (31.66, 6.64), (32.54, 6.81), (33.76, 6.59), (34.2, 5.54), (35.37, 3.08), (35.76, 2.31), (35.02, 1.81), (35.02, 2.05), (35.87, 2.11), (37.04, 0.0), (35.87, 2.11), (35.76, 2.31), (35.37, 3.08), (36.16, 3.49), (38.38, 2.54), (36.16, 3.49), (35.26, 6.16), (39.95, 8.73), (42.11, 8.67), (43.53, 7.38), (42.11, 8.67)], + 'boundary': [(37.72, -0.19), (36.31, 2.35), (36.2, 2.5300000000000002), (36.21, 2.54), (36.04, 2.86), (36.17, 2.93), (38.64, 1.8800000000000001), (39.04, 2.8000000000000003), (36.550000000000004, 3.86), (35.86, 5.91), (40.07, 8.22), (41.9, 8.17), (43.56, 6.67), (44.24, 7.41), (43.15, 8.39), (45.46, 8.950000000000001), (45.230000000000004, 9.92), (42.04, 9.17), (39.9, 9.23), (39.77, 9.200000000000001), (35.02, 6.6000000000000005), (35.01, 6.59), (34.43, 6.25), (34.17, 6.9), (33.97, 7.0600000000000005), (32.58, 7.3100000000000005), (32.49, 7.3100000000000005), (31.61, 7.13), (31.52, 7.13), (31.57, 8.35), (31.53, 8.5), (31.5, 8.540000000000001), (31.84, 8.98), (31.05, 9.59), (30.63, 9.040000000000001), (29.22, 8.92), (28.97, 8.71), (28.67, 7.46), (28.66, 7.390000000000001), (28.71, 6.23), (29.03, 5.94), (31.04, 6.11), (31.03, 6.11), (31.7, 6.140000000000001), (31.740000000000002, 6.15), (32.54, 6.3), (33.4, 6.140000000000001), (33.74, 5.34), (33.75, 5.33), (34.92, 2.86), (35.11, 2.47), (34.74, 2.22), (35.02, 1.81), (35.04, 1.81), (35.06, 1.55), (35.58, 1.58), (36.85, -0.68), (37.72, -0.19)], + 'convex_hull': [(37.04, 0.0), (34.15, 1.1), (32.17, 1.88), (29.2, 6.46), (29.16, 7.38), (29.42, 8.44), (31.14, 8.89), (44.86, 9.32), (43.53, 7.38), (37.04, 0.0)] + } + ], 'isolated_nodes': []} self.leave(p) From 8d49db294211898737ccc765c7ae2333e954dab0 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 12:04:52 +0800 Subject: [PATCH 25/40] Add more test --- api/s37_virtual_district.py | 10 +-- test_tjnetwork.py | 125 ++++++++++++++++++++++++++---------- 2 files changed, 95 insertions(+), 40 deletions(-) diff --git a/api/s37_virtual_district.py b/api/s37_virtual_district.py index b019fd7..4d2f824 100644 --- a/api/s37_virtual_district.py +++ b/api/s37_virtual_district.py @@ -1,6 +1,6 @@ from .database import * from .s0_base import get_node_links -from .s32_region_util import calculate_boundary, calculate_convex_hull, inflate_boundary +#from .s32_region_util import calculate_boundary, calculate_convex_hull, inflate_boundary def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: @@ -62,9 +62,9 @@ def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: vds: list[dict[str, Any]] = [] for center, value in center_node.items(): - p = calculate_boundary(name, value) - b = inflate_boundary(name, p) - c = calculate_convex_hull(name, value) - vds.append({ 'center': center, 'nodes': value, 'path': p, 'boundary': b, 'convex_hull': c}) + #p = calculate_boundary(name, value) + #b = inflate_boundary(name, p) + #c = calculate_convex_hull(name, value) + vds.append({ 'center': center, 'nodes': value }) return { 'virtual_districts': vds, 'isolated_nodes': isolated_nodes } diff --git a/test_tjnetwork.py b/test_tjnetwork.py index 14e218f..bbad971 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -5573,7 +5573,9 @@ class TestApi: open_project(p) vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts'] - nodes = get_nodes_in_boundary(p, vds[0]['boundary']) + boundary = calculate_boundary(p, vds[0]['nodes']) + boundary = inflate_boundary(p, boundary) + nodes = get_nodes_in_boundary(p, boundary) assert nodes == ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'] self.leave(p) @@ -5585,7 +5587,10 @@ class TestApi: open_project(p) vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts'] - add_region(p, ChangeSet({'id': 'r', 'boundary': vds[0]['boundary']})) + boundary = calculate_boundary(p, vds[0]['nodes']) + boundary = inflate_boundary(p, boundary) + + add_region(p, ChangeSet({'id': 'r', 'boundary': boundary})) nodes = get_nodes_in_region(p, 'r') assert nodes == ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'] @@ -5605,6 +5610,45 @@ class TestApi: self.leave(p) + def test_calculate_boundary(self): + p = 'test_calculate_boundary' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + nodes = ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'] + b = calculate_boundary(p, nodes) + assert b == [(23.7, 22.76), (22.08, 23.1), (21.17, 23.32), (20.8, 23.4), (20.32, 21.57), (16.97, 21.28), (13.81, 22.94), (9.0, 27.85), (8.0, 27.53), (9.0, 27.85), (13.81, 22.94), (12.96, 21.31), (17.64, 18.92), (20.21, 17.53), (20.98, 19.18), (21.69, 21.28), (22.08, 23.1), (23.7, 22.76)] + + self.leave(p) + + + def test_inflate_boundary(self): + p = 'test_inflate_boundary' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + nodes = ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'] + b = calculate_boundary(p, nodes) + b = inflate_boundary(p, b) + assert b == [(20.57, 17.12), (21.44, 18.98), (21.45, 19.01), (22.17, 21.13), (22.18, 21.16), (22.46, 22.5), (24.09, 22.17), (24.29, 23.150000000000002), (22.19, 23.580000000000002), (22.2, 23.59), (21.28, 23.81), (20.71, 23.93), (20.37, 23.72), (19.92, 22.03), (17.06, 21.79), (14.120000000000001, 23.330000000000002), (9.26, 28.3), (8.98, 28.37), (7.37, 27.85), (7.68, 26.900000000000002), (8.85, 27.27), (13.19, 22.84), (12.42, 21.36), (12.55, 20.96), (17.41, 18.47), (20.16, 16.990000000000002), (20.57, 17.12)] + + self.leave(p) + + + def test_inflate_region(self): + p = 'test_inflate_region' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts'] + boundary = calculate_boundary(p, vds[0]['nodes']) + add_region(p, ChangeSet({'id': 'r', 'boundary': boundary})) + b = inflate_region(p, 'r') + assert b == [(20.57, 17.12), (21.44, 18.98), (21.45, 19.01), (22.17, 21.13), (22.18, 21.16), (22.46, 22.5), (24.09, 22.17), (24.29, 23.150000000000002), (22.19, 23.580000000000002), (22.2, 23.59), (21.28, 23.81), (20.71, 23.93), (20.37, 23.72), (19.92, 22.03), (17.06, 21.79), (14.120000000000001, 23.330000000000002), (9.26, 28.3), (8.98, 28.37), (7.37, 27.85), (7.68, 26.900000000000002), (8.85, 27.27), (13.19, 22.84), (12.42, 21.36), (12.55, 20.96), (17.41, 18.47), (20.16, 16.990000000000002), (20.57, 17.12)] + + self.leave(p) + + # 33 region @@ -5696,40 +5740,51 @@ class TestApi: open_project(p) result = calculate_virtual_district(p, ['107', '139', '267', '211']) + assert result['isolated_nodes'] == [] + vds = result['virtual_districts'] + assert len(vds) == 4 + assert vds[0]['center'] == '107' + assert vds[0]['nodes'] == ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'] + assert vds[1]['center'] == '139' + assert vds[1]['nodes'] == ['15', '20', '60', '601', '61', '121', '123', '125', '127', '129', '131', '139', '141', '143', '145', '147', '149', '151', '153', 'River', '3'] + assert vds[2]['center'] == '267' + assert vds[2]['nodes'] == ['35', '40', '113', '157', '159', '161', '163', '164', '166', '167', '169', '171', '173', '177', '179', '181', '183', '184', '185', '187', '189', '191', '193', '195', '197', '204', '265', '267', '269', '271', '1'] + assert vds[3]['center'] == '211' + assert vds[3]['nodes'] == ['50', '199', '201', '203', '205', '206', '207', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '2'] - assert result == { - 'virtual_districts': - [ - { - 'center': '107', - 'nodes': ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'], - 'path': [(23.7, 22.76), (22.08, 23.1), (21.17, 23.32), (20.8, 23.4), (20.32, 21.57), (16.97, 21.28), (13.81, 22.94), (9.0, 27.85), (8.0, 27.53), (9.0, 27.85), (13.81, 22.94), (12.96, 21.31), (17.64, 18.92), (20.21, 17.53), (20.98, 19.18), (21.69, 21.28), (22.08, 23.1)], - 'boundary': [(20.57, 17.12), (21.44, 18.98), (21.45, 19.01), (22.17, 21.13), (22.18, 21.16), (22.46, 22.5), (24.09, 22.17), (24.29, 23.150000000000002), (22.19, 23.580000000000002), (22.2, 23.59), (21.28, 23.81), (20.71, 23.93), (20.37, 23.72), (19.92, 22.03), (17.06, 21.79), (14.120000000000001, 23.330000000000002), (9.26, 28.3), (8.98, 28.37), (7.37, 27.85), (7.68, 26.900000000000002), (8.85, 27.27), (13.19, 22.84), (12.42, 21.36), (12.55, 20.96), (17.41, 18.47), (20.16, 16.990000000000002), (20.57, 17.12)], - 'convex_hull': [(20.21, 17.53), (12.96, 21.31), (8.0, 27.53), (9.0, 27.85), (23.7, 22.76), (20.21, 17.53)] - }, - { - 'center': '139', - 'nodes': ['15', '20', '60', '601', '61', '121', '123', '125', '127', '129', '131', '139', '141', '143', '145', '147', '149', '151', '153', 'River', '3'], - 'path': [(38.68, 23.76), (37.47, 21.97), (35.68, 23.08), (33.28, 24.54), (30.32, 26.39), (37.89, 29.55), (30.32, 26.39), (29.29, 26.4), (29.44, 26.91), (29.41, 27.27), (29.44, 26.91), (29.29, 26.4), (24.59, 25.64), (23.54, 25.5), (23.37, 27.31), (23.71, 29.03), (23.9, 29.94), (24.15, 31.06), (23.9, 29.94), (23.0, 29.49), (23.71, 29.03), (23.37, 27.31), (23.54, 25.5), (24.59, 25.64), (28.13, 22.63), (28.29, 21.39), (29.62, 20.74), (30.24, 20.38), (33.02, 19.29), (35.68, 23.08), (37.47, 21.97)], - 'boundary': [(33.33, 18.86), (35.82, 22.400000000000002), (37.38, 21.44), (37.77, 21.52), (39.37, 23.89), (38.550000000000004, 24.45), (37.32, 22.64), (35.94, 23.51), (33.54, 24.97), (33.53, 24.96), (31.39, 26.29), (38.54, 29.28), (38.160000000000004, 30.2), (30.22, 26.89), (29.93, 26.89), (29.94, 26.91), (29.87, 27.810000000000002), (28.87, 27.73), (28.93, 26.95), (28.89, 26.830000000000002), (24.52, 26.13), (24.52, 26.14), (23.990000000000002, 26.060000000000002), (23.87, 27.29), (24.2, 28.93), (24.39, 29.830000000000002), (24.75, 31.44), (23.77, 31.66), (23.46, 30.28), (22.52, 29.810000000000002), (22.48, 29.23), (23.150000000000002, 28.79), (22.87, 27.37), (22.87, 27.3), (23.06, 25.240000000000002), (23.400000000000002, 24.98), (24.44, 25.11), (27.650000000000002, 22.38), (27.810000000000002, 21.2), (27.96, 21.0), (29.400000000000002, 20.29), (30.01, 19.94), (30.04, 19.92), (33.0, 18.76), (33.33, 18.86)], - 'convex_hull': [(33.02, 19.29), (30.24, 20.38), (28.29, 21.39), (23.54, 25.5), (23.0, 29.49), (24.15, 31.06), (37.89, 29.55), (38.68, 23.76), (37.47, 21.97), (33.02, 19.29)] - }, - { - 'center': '267', - 'nodes': ['35', '40', '113', '157', '159', '161', '163', '164', '166', '167', '169', '171', '173', '177', '179', '181', '183', '184', '185', '187', '189', '191', '193', '195', '197', '204', '265', '267', '269', '271', '1'], - 'path': [(27.46, 9.84), (27.02, 9.81), (25.71, 10.4), (25.45, 10.18), (24.15, 11.37), (25.03, 12.14), (25.97, 11.0), (25.72, 10.74), (25.46, 10.52), (25.72, 10.74), (25.97, 11.0), (26.65, 11.8), (26.87, 11.59), (26.65, 11.8), (25.68, 12.74), (25.88, 12.98), (25.68, 12.74), (25.39, 13.6), (25.39, 14.98), (25.98, 15.14), (26.48, 15.13), (25.98, 15.14), (25.39, 14.98), (25.1, 15.28), (23.12, 17.5), (24.85, 20.16), (23.12, 17.5), (25.1, 15.28), (23.18, 14.72), (22.88, 14.35), (22.04, 16.61), (22.88, 14.35), (22.1, 14.07), (20.97, 15.18), (22.1, 14.07), (23.64, 11.04), (23.8, 10.9), (25.01, 9.67), (25.15, 9.52), (25.01, 9.67), (25.45, 10.18), (25.71, 10.4), (27.02, 9.81)], - 'boundary': [(25.86, 9.5), (25.68, 9.68), (25.79, 9.8), (26.87, 9.33), (26.990000000000002, 9.31), (27.990000000000002, 9.38), (27.92, 10.370000000000001), (27.12, 10.31), (26.34, 10.66), (26.35, 10.67), (26.68, 11.07), (26.89, 10.88), (27.580000000000002, 11.61), (27.0, 12.16), (26.36, 12.77), (26.580000000000002, 13.040000000000001), (25.91, 13.6), (25.89, 13.67), (25.89, 14.6), (26.03, 14.63), (26.97, 14.620000000000001), (26.990000000000002, 15.620000000000001), (25.95, 15.64), (25.88, 15.63), (25.54, 15.530000000000001), (25.46, 15.63), (25.45, 15.620000000000001), (23.740000000000002, 17.54), (25.54, 20.31), (24.7, 20.85), (22.61, 17.63), (22.63, 17.29), (24.2, 15.530000000000001), (23.09, 15.21), (22.330000000000002, 17.25), (21.400000000000002, 16.9), (22.23, 14.64), (22.22, 14.64), (20.96, 15.89), (20.26, 15.17), (21.68, 13.77), (23.22, 10.77), (23.27, 10.700000000000001), (23.47, 10.52), (24.650000000000002, 9.32), (25.13, 8.81), (25.86, 9.5)], - 'convex_hull': [(25.15, 9.52), (23.64, 11.04), (20.97, 15.18), (22.04, 16.61), (24.85, 20.16), (26.48, 15.13), (27.46, 9.84), (25.15, 9.52)] - }, - { - 'center': '211', - 'nodes': ['50', '199', '201', '203', '205', '206', '207', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '2'], - 'path': [(44.86, 9.32), (42.11, 8.67), (39.95, 8.73), (35.26, 6.16), (34.2, 5.54), (33.76, 6.59), (32.54, 6.81), (31.66, 6.64), (31.0, 6.61), (31.07, 8.29), (30.89, 8.57), (31.14, 8.89), (30.89, 8.57), (29.42, 8.44), (29.16, 7.38), (29.2, 6.46), (31.0, 6.61), (31.66, 6.64), (32.54, 6.81), (33.76, 6.59), (34.2, 5.54), (35.37, 3.08), (35.76, 2.31), (35.02, 1.81), (35.02, 2.05), (35.87, 2.11), (37.04, 0.0), (35.87, 2.11), (35.76, 2.31), (35.37, 3.08), (36.16, 3.49), (38.38, 2.54), (36.16, 3.49), (35.26, 6.16), (39.95, 8.73), (42.11, 8.67), (43.53, 7.38), (42.11, 8.67)], - 'boundary': [(37.72, -0.19), (36.31, 2.35), (36.2, 2.5300000000000002), (36.21, 2.54), (36.04, 2.86), (36.17, 2.93), (38.64, 1.8800000000000001), (39.04, 2.8000000000000003), (36.550000000000004, 3.86), (35.86, 5.91), (40.07, 8.22), (41.9, 8.17), (43.56, 6.67), (44.24, 7.41), (43.15, 8.39), (45.46, 8.950000000000001), (45.230000000000004, 9.92), (42.04, 9.17), (39.9, 9.23), (39.77, 9.200000000000001), (35.02, 6.6000000000000005), (35.01, 6.59), (34.43, 6.25), (34.17, 6.9), (33.97, 7.0600000000000005), (32.58, 7.3100000000000005), (32.49, 7.3100000000000005), (31.61, 7.13), (31.52, 7.13), (31.57, 8.35), (31.53, 8.5), (31.5, 8.540000000000001), (31.84, 8.98), (31.05, 9.59), (30.63, 9.040000000000001), (29.22, 8.92), (28.97, 8.71), (28.67, 7.46), (28.66, 7.390000000000001), (28.71, 6.23), (29.03, 5.94), (31.04, 6.11), (31.03, 6.11), (31.7, 6.140000000000001), (31.740000000000002, 6.15), (32.54, 6.3), (33.4, 6.140000000000001), (33.74, 5.34), (33.75, 5.33), (34.92, 2.86), (35.11, 2.47), (34.74, 2.22), (35.02, 1.81), (35.04, 1.81), (35.06, 1.55), (35.58, 1.58), (36.85, -0.68), (37.72, -0.19)], - 'convex_hull': [(37.04, 0.0), (34.15, 1.1), (32.17, 1.88), (29.2, 6.46), (29.16, 7.38), (29.42, 8.44), (31.14, 8.89), (44.86, 9.32), (43.53, 7.38), (37.04, 0.0)] - } - ], - 'isolated_nodes': []} + #assert result == { + # 'virtual_districts': + # [ + # { + # 'center': '107', + # 'nodes': ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'], + # 'boundary': [(23.7, 22.76), (22.08, 23.1), (21.17, 23.32), (20.8, 23.4), (20.32, 21.57), (16.97, 21.28), (13.81, 22.94), (9.0, 27.85), (8.0, 27.53), (9.0, 27.85), (13.81, 22.94), (12.96, 21.31), (17.64, 18.92), (20.21, 17.53), (20.98, 19.18), (21.69, 21.28), (22.08, 23.1)], + # 'inflated_boundary': [(20.57, 17.12), (21.44, 18.98), (21.45, 19.01), (22.17, 21.13), (22.18, 21.16), (22.46, 22.5), (24.09, 22.17), (24.29, 23.150000000000002), (22.19, 23.580000000000002), (22.2, 23.59), (21.28, 23.81), (20.71, 23.93), (20.37, 23.72), (19.92, 22.03), (17.06, 21.79), (14.120000000000001, 23.330000000000002), (9.26, 28.3), (8.98, 28.37), (7.37, 27.85), (7.68, 26.900000000000002), (8.85, 27.27), (13.19, 22.84), (12.42, 21.36), (12.55, 20.96), (17.41, 18.47), (20.16, 16.990000000000002), (20.57, 17.12)], + # 'convex_hull': [(20.21, 17.53), (12.96, 21.31), (8.0, 27.53), (9.0, 27.85), (23.7, 22.76), (20.21, 17.53)] + # }, + # { + # 'center': '139', + # 'nodes': ['15', '20', '60', '601', '61', '121', '123', '125', '127', '129', '131', '139', '141', '143', '145', '147', '149', '151', '153', 'River', '3'], + # 'boundary': [(38.68, 23.76), (37.47, 21.97), (35.68, 23.08), (33.28, 24.54), (30.32, 26.39), (37.89, 29.55), (30.32, 26.39), (29.29, 26.4), (29.44, 26.91), (29.41, 27.27), (29.44, 26.91), (29.29, 26.4), (24.59, 25.64), (23.54, 25.5), (23.37, 27.31), (23.71, 29.03), (23.9, 29.94), (24.15, 31.06), (23.9, 29.94), (23.0, 29.49), (23.71, 29.03), (23.37, 27.31), (23.54, 25.5), (24.59, 25.64), (28.13, 22.63), (28.29, 21.39), (29.62, 20.74), (30.24, 20.38), (33.02, 19.29), (35.68, 23.08), (37.47, 21.97)], + # 'inflated_boundary': [(33.33, 18.86), (35.82, 22.400000000000002), (37.38, 21.44), (37.77, 21.52), (39.37, 23.89), (38.550000000000004, 24.45), (37.32, 22.64), (35.94, 23.51), (33.54, 24.97), (33.53, 24.96), (31.39, 26.29), (38.54, 29.28), (38.160000000000004, 30.2), (30.22, 26.89), (29.93, 26.89), (29.94, 26.91), (29.87, 27.810000000000002), (28.87, 27.73), (28.93, 26.95), (28.89, 26.830000000000002), (24.52, 26.13), (24.52, 26.14), (23.990000000000002, 26.060000000000002), (23.87, 27.29), (24.2, 28.93), (24.39, 29.830000000000002), (24.75, 31.44), (23.77, 31.66), (23.46, 30.28), (22.52, 29.810000000000002), (22.48, 29.23), (23.150000000000002, 28.79), (22.87, 27.37), (22.87, 27.3), (23.06, 25.240000000000002), (23.400000000000002, 24.98), (24.44, 25.11), (27.650000000000002, 22.38), (27.810000000000002, 21.2), (27.96, 21.0), (29.400000000000002, 20.29), (30.01, 19.94), (30.04, 19.92), (33.0, 18.76), (33.33, 18.86)], + # 'convex_hull': [(33.02, 19.29), (30.24, 20.38), (28.29, 21.39), (23.54, 25.5), (23.0, 29.49), (24.15, 31.06), (37.89, 29.55), (38.68, 23.76), (37.47, 21.97), (33.02, 19.29)] + # }, + # { + # 'center': '267', + # 'nodes': ['35', '40', '113', '157', '159', '161', '163', '164', '166', '167', '169', '171', '173', '177', '179', '181', '183', '184', '185', '187', '189', '191', '193', '195', '197', '204', '265', '267', '269', '271', '1'], + # 'boundary': [(27.46, 9.84), (27.02, 9.81), (25.71, 10.4), (25.45, 10.18), (24.15, 11.37), (25.03, 12.14), (25.97, 11.0), (25.72, 10.74), (25.46, 10.52), (25.72, 10.74), (25.97, 11.0), (26.65, 11.8), (26.87, 11.59), (26.65, 11.8), (25.68, 12.74), (25.88, 12.98), (25.68, 12.74), (25.39, 13.6), (25.39, 14.98), (25.98, 15.14), (26.48, 15.13), (25.98, 15.14), (25.39, 14.98), (25.1, 15.28), (23.12, 17.5), (24.85, 20.16), (23.12, 17.5), (25.1, 15.28), (23.18, 14.72), (22.88, 14.35), (22.04, 16.61), (22.88, 14.35), (22.1, 14.07), (20.97, 15.18), (22.1, 14.07), (23.64, 11.04), (23.8, 10.9), (25.01, 9.67), (25.15, 9.52), (25.01, 9.67), (25.45, 10.18), (25.71, 10.4), (27.02, 9.81)], + # 'inflated_boundary': [(25.86, 9.5), (25.68, 9.68), (25.79, 9.8), (26.87, 9.33), (26.990000000000002, 9.31), (27.990000000000002, 9.38), (27.92, 10.370000000000001), (27.12, 10.31), (26.34, 10.66), (26.35, 10.67), (26.68, 11.07), (26.89, 10.88), (27.580000000000002, 11.61), (27.0, 12.16), (26.36, 12.77), (26.580000000000002, 13.040000000000001), (25.91, 13.6), (25.89, 13.67), (25.89, 14.6), (26.03, 14.63), (26.97, 14.620000000000001), (26.990000000000002, 15.620000000000001), (25.95, 15.64), (25.88, 15.63), (25.54, 15.530000000000001), (25.46, 15.63), (25.45, 15.620000000000001), (23.740000000000002, 17.54), (25.54, 20.31), (24.7, 20.85), (22.61, 17.63), (22.63, 17.29), (24.2, 15.530000000000001), (23.09, 15.21), (22.330000000000002, 17.25), (21.400000000000002, 16.9), (22.23, 14.64), (22.22, 14.64), (20.96, 15.89), (20.26, 15.17), (21.68, 13.77), (23.22, 10.77), (23.27, 10.700000000000001), (23.47, 10.52), (24.650000000000002, 9.32), (25.13, 8.81), (25.86, 9.5)], + # 'convex_hull': [(25.15, 9.52), (23.64, 11.04), (20.97, 15.18), (22.04, 16.61), (24.85, 20.16), (26.48, 15.13), (27.46, 9.84), (25.15, 9.52)] + # }, + # { + # 'center': '211', + # 'nodes': ['50', '199', '201', '203', '205', '206', '207', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '2'], + # 'boundary': [(44.86, 9.32), (42.11, 8.67), (39.95, 8.73), (35.26, 6.16), (34.2, 5.54), (33.76, 6.59), (32.54, 6.81), (31.66, 6.64), (31.0, 6.61), (31.07, 8.29), (30.89, 8.57), (31.14, 8.89), (30.89, 8.57), (29.42, 8.44), (29.16, 7.38), (29.2, 6.46), (31.0, 6.61), (31.66, 6.64), (32.54, 6.81), (33.76, 6.59), (34.2, 5.54), (35.37, 3.08), (35.76, 2.31), (35.02, 1.81), (35.02, 2.05), (35.87, 2.11), (37.04, 0.0), (35.87, 2.11), (35.76, 2.31), (35.37, 3.08), (36.16, 3.49), (38.38, 2.54), (36.16, 3.49), (35.26, 6.16), (39.95, 8.73), (42.11, 8.67), (43.53, 7.38), (42.11, 8.67)], + # 'inflated_boundary': [(37.72, -0.19), (36.31, 2.35), (36.2, 2.5300000000000002), (36.21, 2.54), (36.04, 2.86), (36.17, 2.93), (38.64, 1.8800000000000001), (39.04, 2.8000000000000003), (36.550000000000004, 3.86), (35.86, 5.91), (40.07, 8.22), (41.9, 8.17), (43.56, 6.67), (44.24, 7.41), (43.15, 8.39), (45.46, 8.950000000000001), (45.230000000000004, 9.92), (42.04, 9.17), (39.9, 9.23), (39.77, 9.200000000000001), (35.02, 6.6000000000000005), (35.01, 6.59), (34.43, 6.25), (34.17, 6.9), (33.97, 7.0600000000000005), (32.58, 7.3100000000000005), (32.49, 7.3100000000000005), (31.61, 7.13), (31.52, 7.13), (31.57, 8.35), (31.53, 8.5), (31.5, 8.540000000000001), (31.84, 8.98), (31.05, 9.59), (30.63, 9.040000000000001), (29.22, 8.92), (28.97, 8.71), (28.67, 7.46), (28.66, 7.390000000000001), (28.71, 6.23), (29.03, 5.94), (31.04, 6.11), (31.03, 6.11), (31.7, 6.140000000000001), (31.740000000000002, 6.15), (32.54, 6.3), (33.4, 6.140000000000001), (33.74, 5.34), (33.75, 5.33), (34.92, 2.86), (35.11, 2.47), (34.74, 2.22), (35.02, 1.81), (35.04, 1.81), (35.06, 1.55), (35.58, 1.58), (36.85, -0.68), (37.72, -0.19)], + # 'convex_hull': [(37.04, 0.0), (34.15, 1.1), (32.17, 1.88), (29.2, 6.46), (29.16, 7.38), (29.42, 8.44), (31.14, 8.89), (44.86, 9.32), (43.53, 7.38), (37.04, 0.0)] + # } + # ], + # 'isolated_nodes': []} self.leave(p) From 45296960e7ea8217fcb22c9a0c699d56f3a071bf Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 12:23:04 +0800 Subject: [PATCH 26/40] Take care dropping table --- script/sql/drop/32.region.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/sql/drop/32.region.sql b/script/sql/drop/32.region.sql index 71bc330..693aa07 100644 --- a/script/sql/drop/32.region.sql +++ b/script/sql/drop/32.region.sql @@ -1,5 +1,7 @@ drop table if exists temp_vd_topology; +drop table if exists temp_link; + drop table if exists temp_node; drop index if exists temp_region_gist; From e30c81a891c9916835b915d4480300565ea1936a Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 14:04:07 +0800 Subject: [PATCH 27/40] Support water distribution --- api/__init__.py | 3 + api/batch_exe.py | 18 +- api/s32_region_util.py | 91 ++++--- api/s34_water_distribution.py | 52 ++++ test_tjnetwork.py | 484 ++++++++++++++++++++++++++++++++++ tjnetwork.py | 20 +- 6 files changed, 618 insertions(+), 50 deletions(-) create mode 100644 api/s34_water_distribution.py diff --git a/api/__init__.py b/api/__init__.py index 79f4a17..c22bd76 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -136,4 +136,7 @@ from .s32_region_util import get_nodes_in_boundary, get_nodes_in_region, calcula from .s33_region import get_region_schema, get_region, set_region, add_region, delete_region +from .s34_water_distribution import DISTRIBUTION_TYPE_ADD, DISTRIBUTION_TYPE_OVERRIDE +from .s34_water_distribution import distribute_demand_to_nodes, distribute_demand_to_region + from .s37_virtual_district import calculate_virtual_district diff --git a/api/batch_exe.py b/api/batch_exe.py index cb41355..7f28c4e 100644 --- a/api/batch_exe.py +++ b/api/batch_exe.py @@ -32,7 +32,7 @@ from .s31_scada_element import set_scada_element, add_scada_element, delete_scad from .batch_api_cs import rewrite_batch_api -def execute_add_command(name: str, cs: ChangeSet) -> ChangeSet: +def _execute_add_command(name: str, cs: ChangeSet) -> ChangeSet: type = cs.operations[0]['type'] if type == s1_title: @@ -109,7 +109,7 @@ def execute_add_command(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() -def execute_update_command(name: str, cs: ChangeSet) -> ChangeSet: +def _execute_update_command(name: str, cs: ChangeSet) -> ChangeSet: type = cs.operations[0]['type'] if type == s1_title: @@ -186,7 +186,7 @@ def execute_update_command(name: str, cs: ChangeSet) -> ChangeSet: return ChangeSet() -def execute_delete_command(name: str, cs: ChangeSet) -> ChangeSet: +def _execute_delete_command(name: str, cs: ChangeSet) -> ChangeSet: type = cs.operations[0]['type'] if type == s1_title: @@ -277,11 +277,11 @@ def execute_batch_commands(name: str, cs: ChangeSet) -> ChangeSet: todo = op operation = op['operation'] if operation == API_ADD: - result.merge(execute_add_command(name, ChangeSet(op))) + result.merge(_execute_add_command(name, ChangeSet(op))) elif operation == API_UPDATE: - result.merge(execute_update_command(name, ChangeSet(op))) + result.merge(_execute_update_command(name, ChangeSet(op))) elif operation == API_DELETE: - result.merge(execute_delete_command(name, ChangeSet(op))) + result.merge(_execute_delete_command(name, ChangeSet(op))) except: print(f'ERROR: Fail to execute {todo}') @@ -305,11 +305,11 @@ def execute_batch_command(name: str, cs: ChangeSet) -> ChangeSet: todo = op operation = op['operation'] if operation == API_ADD: - result.merge(execute_add_command(name, ChangeSet(op))) + result.merge(_execute_add_command(name, ChangeSet(op))) elif operation == API_UPDATE: - result.merge(execute_update_command(name, ChangeSet(op))) + result.merge(_execute_update_command(name, ChangeSet(op))) elif operation == API_DELETE: - result.merge(execute_delete_command(name, ChangeSet(op))) + result.merge(_execute_delete_command(name, ChangeSet(op))) except: print(f'ERROR: Fail to execute {todo}') diff --git a/api/s32_region_util.py b/api/s32_region_util.py index 6e1f7f9..d193063 100644 --- a/api/s32_region_util.py +++ b/api/s32_region_util.py @@ -2,8 +2,9 @@ import ctypes import platform import os import math -import collections -from .s0_base import get_node_links, get_link_nodes +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 @@ -89,35 +90,53 @@ def _angle_of_node_link(node: str, link: str, nodes, links) -> float: return _angle(v) +class Topology: + def __init__(self, db: str, nodes: list[str]) -> None: + self._nodes: dict[str, Any] = {} + self._max_x_node = '' + 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': [] } + if self._max_x_node == '' or self._nodes[node]['x'] > self._nodes[self._max_x_node]['x']: + self._max_x_node = node + + self._links = {} + 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 } + + 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 max_x_node(self): + return self._max_x_node + + def links(self): + return self._links + + def calculate_boundary(name: str, nodes: list[str]) -> list[tuple[float, float]]: - new_nodes = {} - max_x_node = '' - for node in nodes: - if not node_has_coord(name, node): - continue - if get_node_links(name, node) == 0: - continue - new_nodes[node] = get_node_coord(name, node) | { 'links': [] } - if max_x_node == '' or new_nodes[node]['x'] > new_nodes[max_x_node]['x']: - max_x_node = node + topology = Topology(name, nodes) + t_nodes = topology.nodes() + t_links = topology.links() - new_links = {} - for node in new_nodes: - for link in get_node_links(name, node): - candidate = True - link_nodes = get_link_nodes(name, link) - for link_node in link_nodes: - if link_node not in new_nodes: - candidate = False - break - if candidate: - new_links[link] = { 'node1' : link_nodes[0], 'node2' : link_nodes[1] } - if link not in new_nodes[link_nodes[0]]['links']: - new_nodes[link_nodes[0]]['links'].append(link) - if link not in new_nodes[link_nodes[1]]['links']: - new_nodes[link_nodes[1]]['links'].append(link) - - cursor = max_x_node + cursor = topology.max_x_node() in_angle = 0 paths: list[str] = [] @@ -125,8 +144,8 @@ def calculate_boundary(name: str, nodes: list[str]) -> list[tuple[float, float]] paths.append(cursor) sorted_links = [] overlapped_link = '' - for link in new_nodes[cursor]['links']: - angle = _angle_of_node_link(cursor, link, new_nodes, new_links) + 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 @@ -135,7 +154,7 @@ def calculate_boundary(name: str, nodes: list[str]) -> list[tuple[float, float]] # work into a branch, return if len(sorted_links) == 0: cursor = paths[-2] - in_angle = in_angle = _angle_of_node_link(cursor, overlapped_link, new_nodes, new_links) + in_angle = in_angle = _angle_of_node_link(cursor, overlapped_link, t_nodes, t_links) continue sorted_links = sorted(sorted_links, key=lambda s:s[0]) @@ -145,17 +164,17 @@ def calculate_boundary(name: str, nodes: list[str]) -> list[tuple[float, float]] out_link = link break - cursor = new_links[out_link]['node1'] if cursor == new_links[out_link]['node2'] else new_links[out_link]['node2'] + cursor = t_links[out_link]['node1'] if cursor == t_links[out_link]['node2'] else t_links[out_link]['node2'] # end up trip :) - if cursor == max_x_node: + if cursor == topology.max_x_node(): paths.append(cursor) break - in_angle = _angle_of_node_link(cursor, out_link, new_nodes, new_links) + in_angle = _angle_of_node_link(cursor, out_link, t_nodes, t_links) boundary: list[tuple[float, float]] = [] for node in paths: - boundary.append((new_nodes[node]['x'], new_nodes[node]['y'])) + boundary.append((t_nodes[node]['x'], t_nodes[node]['y'])) return boundary diff --git a/api/s34_water_distribution.py b/api/s34_water_distribution.py new file mode 100644 index 0000000..de444eb --- /dev/null +++ b/api/s34_water_distribution.py @@ -0,0 +1,52 @@ +from .database import ChangeSet +from .s0_base import is_junction +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 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) diff --git a/test_tjnetwork.py b/test_tjnetwork.py index bbad971..724f013 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -5732,6 +5732,490 @@ class TestApi: self.leave(p) + # 34 water_distribution + + + def test_distribute_demand_to_nodes(self): + p = 'test_distribute_demand_to_nodes' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts'] + nodes = vds[0]['nodes'] + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands == [0.0, 189.95, 133.2, 135.37, 54.64, 231.4, 141.94, 52.1, 117.71, 176.13, 0.0, 0.0, 0.0, 0.0, 0.0] + + distribute_demand_to_nodes(p, 100.0, nodes, DISTRIBUTION_TYPE_ADD) + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 212.0622112211221 + assert demands[2] == 139.66620217577312 + assert demands[3] == 143.60248991565823 + assert demands[4] == 58.82041804180418 + assert demands[5] == 238.66072607260728 + assert demands[6] == 145.80260848307051 + assert demands[7] == 58.566202175773135 + assert demands[8] == 123.44890722405573 + assert demands[9] == 177.02231145336756 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + distribute_demand_to_nodes(p, -100.0, nodes, DISTRIBUTION_TYPE_ADD) + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands == [0.0, 189.95, 133.2, 135.37, 54.64, 231.4, 141.94, 52.1, 117.71, 176.13, 0.0, 0.0, 0.0, 0.0, 0.0] + + distribute_demand_to_nodes(p, 100.0, nodes, DISTRIBUTION_TYPE_OVERRIDE) + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 22.112211221122113 + assert demands[2] == 6.466202175773133 + assert demands[3] == 8.232489915658233 + assert demands[4] == 4.180418041804181 + assert demands[5] == 7.260726072607261 + assert demands[6] == 3.862608483070529 + assert demands[7] == 6.466202175773133 + assert demands[8] == 5.738907224055739 + assert demands[9] == 0.892311453367559 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + distribute_demand_to_nodes(p, -100.0, nodes, DISTRIBUTION_TYPE_OVERRIDE) + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == -17.357291284684024 + assert demands[1] == -22.112211221122113 + assert demands[2] == -6.466202175773133 + assert demands[3] == -8.232489915658233 + assert demands[4] == -4.180418041804181 + assert demands[5] == -7.260726072607261 + assert demands[6] == -3.862608483070529 + assert demands[7] == -6.466202175773133 + assert demands[8] == -5.738907224055739 + assert demands[9] == -0.892311453367559 + assert demands[10] == -3.9665077618873 + assert demands[11] == -2.9275149737195942 + assert demands[12] == -2.1391027991688056 + assert demands[13] == -2.9275149737195942 + assert demands[14] == -5.469991443588803 + + self.leave(p) + + + def test_distribute_demand_to_nodes_op(self): + p = 'test_distribute_demand_to_nodes' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts'] + nodes = vds[0]['nodes'] + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands == [0.0, 189.95, 133.2, 135.37, 54.64, 231.4, 141.94, 52.1, 117.71, 176.13, 0.0, 0.0, 0.0, 0.0, 0.0] + + cs = distribute_demand_to_nodes(p, 100.0, nodes, DISTRIBUTION_TYPE_ADD).operations + + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 212.0622112211221 + assert demands[2] == 139.66620217577312 + assert demands[3] == 143.60248991565823 + assert demands[4] == 58.82041804180418 + assert demands[5] == 238.66072607260728 + assert demands[6] == 145.80260848307051 + assert demands[7] == 58.566202175773135 + assert demands[8] == 123.44890722405573 + assert demands[9] == 177.02231145336756 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + cs = execute_undo(p).operations + + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + demands.reverse() + assert demands == [0.0, 189.95, 133.2, 135.37, 54.64, 231.4, 141.94, 52.1, 117.71, 176.13, 0.0, 0.0, 0.0, 0.0, 0.0] + + cs = execute_redo(p).operations + + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 212.0622112211221 + assert demands[2] == 139.66620217577312 + assert demands[3] == 143.60248991565823 + assert demands[4] == 58.82041804180418 + assert demands[5] == 238.66072607260728 + assert demands[6] == 145.80260848307051 + assert demands[7] == 58.566202175773135 + assert demands[8] == 123.44890722405573 + assert demands[9] == 177.02231145336756 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + cs = distribute_demand_to_nodes(p, 100.0, nodes, DISTRIBUTION_TYPE_OVERRIDE).operations + + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 22.112211221122113 + assert demands[2] == 6.466202175773133 + assert demands[3] == 8.232489915658233 + assert demands[4] == 4.180418041804181 + assert demands[5] == 7.260726072607261 + assert demands[6] == 3.862608483070529 + assert demands[7] == 6.466202175773133 + assert demands[8] == 5.738907224055739 + assert demands[9] == 0.892311453367559 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + cs = execute_undo(p).operations + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + demands.reverse() + assert demands[0] == 17.357291284684024 + assert demands[1] == 212.0622112211221 + assert demands[2] == 139.66620217577312 + assert demands[3] == 143.60248991565823 + assert demands[4] == 58.82041804180418 + assert demands[5] == 238.66072607260728 + assert demands[6] == 145.80260848307051 + assert demands[7] == 58.566202175773135 + assert demands[8] == 123.44890722405573 + assert demands[9] == 177.02231145336756 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + cs = execute_redo(p).operations + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 22.112211221122113 + assert demands[2] == 6.466202175773133 + assert demands[3] == 8.232489915658233 + assert demands[4] == 4.180418041804181 + assert demands[5] == 7.260726072607261 + assert demands[6] == 3.862608483070529 + assert demands[7] == 6.466202175773133 + assert demands[8] == 5.738907224055739 + assert demands[9] == 0.892311453367559 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + self.leave(p) + + + def test_distribute_demand_to_region(self): + p = 'test_distribute_demand_to_region' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts'] + nodes = vds[0]['nodes'] + boundary = calculate_boundary(p, nodes) + boundary = inflate_boundary(p, boundary, 0.1) + add_region(p, ChangeSet({'id': 'r', 'boundary': boundary})) + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands == [0.0, 189.95, 133.2, 135.37, 54.64, 231.4, 141.94, 52.1, 117.71, 176.13, 0.0, 0.0, 0.0, 0.0, 0.0] + + distribute_demand_to_region(p, 100.0, 'r', DISTRIBUTION_TYPE_ADD) + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 212.0622112211221 + assert demands[2] == 139.66620217577312 + assert demands[3] == 143.60248991565823 + assert demands[4] == 58.82041804180418 + assert demands[5] == 238.66072607260728 + assert demands[6] == 145.80260848307051 + assert demands[7] == 58.566202175773135 + assert demands[8] == 123.44890722405573 + assert demands[9] == 177.02231145336756 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + distribute_demand_to_region(p, -100.0, 'r', DISTRIBUTION_TYPE_ADD) + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands == [0.0, 189.95, 133.2, 135.37, 54.64, 231.4, 141.94, 52.1, 117.71, 176.13, 0.0, 0.0, 0.0, 0.0, 0.0] + + distribute_demand_to_region(p, 100.0, 'r', DISTRIBUTION_TYPE_OVERRIDE) + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 22.112211221122113 + assert demands[2] == 6.466202175773133 + assert demands[3] == 8.232489915658233 + assert demands[4] == 4.180418041804181 + assert demands[5] == 7.260726072607261 + assert demands[6] == 3.862608483070529 + assert demands[7] == 6.466202175773133 + assert demands[8] == 5.738907224055739 + assert demands[9] == 0.892311453367559 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + distribute_demand_to_region(p, -100.0, 'r', DISTRIBUTION_TYPE_OVERRIDE) + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == -17.357291284684024 + assert demands[1] == -22.112211221122113 + assert demands[2] == -6.466202175773133 + assert demands[3] == -8.232489915658233 + assert demands[4] == -4.180418041804181 + assert demands[5] == -7.260726072607261 + assert demands[6] == -3.862608483070529 + assert demands[7] == -6.466202175773133 + assert demands[8] == -5.738907224055739 + assert demands[9] == -0.892311453367559 + assert demands[10] == -3.9665077618873 + assert demands[11] == -2.9275149737195942 + assert demands[12] == -2.1391027991688056 + assert demands[13] == -2.9275149737195942 + assert demands[14] == -5.469991443588803 + + self.leave(p) + + + def test_distribute_demand_to_region_op(self): + p = 'test_distribute_demand_to_region_op' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts'] + nodes = vds[0]['nodes'] + boundary = calculate_boundary(p, nodes) + boundary = inflate_boundary(p, boundary, 0.1) + add_region(p, ChangeSet({'id': 'r', 'boundary': boundary})) + + demands = [] + for node in nodes: + if not is_junction(p, node): + continue + ds = get_demand(p, node)['demands'] + demands.append(ds[0]['demand']) + assert demands == [0.0, 189.95, 133.2, 135.37, 54.64, 231.4, 141.94, 52.1, 117.71, 176.13, 0.0, 0.0, 0.0, 0.0, 0.0] + + cs = distribute_demand_to_region(p, 100.0, 'r', DISTRIBUTION_TYPE_ADD).operations + + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 212.0622112211221 + assert demands[2] == 139.66620217577312 + assert demands[3] == 143.60248991565823 + assert demands[4] == 58.82041804180418 + assert demands[5] == 238.66072607260728 + assert demands[6] == 145.80260848307051 + assert demands[7] == 58.566202175773135 + assert demands[8] == 123.44890722405573 + assert demands[9] == 177.02231145336756 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + cs = execute_undo(p).operations + + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + demands.reverse() + assert demands == [0.0, 189.95, 133.2, 135.37, 54.64, 231.4, 141.94, 52.1, 117.71, 176.13, 0.0, 0.0, 0.0, 0.0, 0.0] + + cs = execute_redo(p).operations + + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 212.0622112211221 + assert demands[2] == 139.66620217577312 + assert demands[3] == 143.60248991565823 + assert demands[4] == 58.82041804180418 + assert demands[5] == 238.66072607260728 + assert demands[6] == 145.80260848307051 + assert demands[7] == 58.566202175773135 + assert demands[8] == 123.44890722405573 + assert demands[9] == 177.02231145336756 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + cs = distribute_demand_to_region(p, 100.0, 'r', DISTRIBUTION_TYPE_OVERRIDE).operations + + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 22.112211221122113 + assert demands[2] == 6.466202175773133 + assert demands[3] == 8.232489915658233 + assert demands[4] == 4.180418041804181 + assert demands[5] == 7.260726072607261 + assert demands[6] == 3.862608483070529 + assert demands[7] == 6.466202175773133 + assert demands[8] == 5.738907224055739 + assert demands[9] == 0.892311453367559 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + cs = execute_undo(p).operations + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + demands.reverse() + assert demands[0] == 17.357291284684024 + assert demands[1] == 212.0622112211221 + assert demands[2] == 139.66620217577312 + assert demands[3] == 143.60248991565823 + assert demands[4] == 58.82041804180418 + assert demands[5] == 238.66072607260728 + assert demands[6] == 145.80260848307051 + assert demands[7] == 58.566202175773135 + assert demands[8] == 123.44890722405573 + assert demands[9] == 177.02231145336756 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + cs = execute_redo(p).operations + demands = [] + for value in cs: + ds = value['demands'] + demands.append(ds[0]['demand']) + assert demands[0] == 17.357291284684024 + assert demands[1] == 22.112211221122113 + assert demands[2] == 6.466202175773133 + assert demands[3] == 8.232489915658233 + assert demands[4] == 4.180418041804181 + assert demands[5] == 7.260726072607261 + assert demands[6] == 3.862608483070529 + assert demands[7] == 6.466202175773133 + assert demands[8] == 5.738907224055739 + assert demands[9] == 0.892311453367559 + assert demands[10] == 3.9665077618873 + assert demands[11] == 2.9275149737195942 + assert demands[12] == 2.1391027991688056 + assert demands[13] == 2.9275149737195942 + assert demands[14] == 5.469991443588803 + + self.leave(p) + # 37 virtual_district def test_virtual_district(self): diff --git a/tjnetwork.py b/tjnetwork.py index 8a78435..7c2fc73 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -151,18 +151,22 @@ SCADA_DEVICE_TYPE_LEVEL = api.SCADA_DEVICE_TYPE_LEVEL SCADA_DEVICE_TYPE_FLOW = api.SCADA_DEVICE_TYPE_FLOW -SCADA_MODEL_TYPE_JUNCTION = api.SCADA_MODEL_TYPE_JUNCTION +SCADA_MODEL_TYPE_JUNCTION = api.SCADA_MODEL_TYPE_JUNCTION SCADA_MODEL_TYPE_RESERVOIR = api.SCADA_MODEL_TYPE_RESERVOIR -SCADA_MODEL_TYPE_TANK = api.SCADA_MODEL_TYPE_TANK -SCADA_MODEL_TYPE_PIPE = api.SCADA_MODEL_TYPE_PIPE -SCADA_MODEL_TYPE_PUMP = api.SCADA_MODEL_TYPE_PUMP -SCADA_MODEL_TYPE_VALVE = api.SCADA_MODEL_TYPE_VALVE +SCADA_MODEL_TYPE_TANK = api.SCADA_MODEL_TYPE_TANK +SCADA_MODEL_TYPE_PIPE = api.SCADA_MODEL_TYPE_PIPE +SCADA_MODEL_TYPE_PUMP = api.SCADA_MODEL_TYPE_PUMP +SCADA_MODEL_TYPE_VALVE = api.SCADA_MODEL_TYPE_VALVE SCADA_ELEMENT_STATUS_ONLINE = api.SCADA_ELEMENT_STATUS_ONLINE SCADA_ELEMENT_STATUS_OFFLINE = api.SCADA_ELEMENT_STATUS_OFFLINE +DISTRIBUTION_TYPE_ADD = api.DISTRIBUTION_TYPE_ADD +DISTRIBUTION_TYPE_OVERRIDE = api.DISTRIBUTION_TYPE_OVERRIDE + + ############################################################ # project ############################################################ @@ -983,6 +987,12 @@ def delete_region(name: str, cs: ChangeSet) -> ChangeSet: # water_distribution 34 ############################################################ +def distribute_demand_to_nodes(name: str, demand: float, nodes: list[str], type: str = DISTRIBUTION_TYPE_ADD) -> ChangeSet: + return api.distribute_demand_to_nodes(name, demand, nodes, type) + +def distribute_demand_to_region(name: str, demand: float, region: str, type: str = DISTRIBUTION_TYPE_ADD) -> ChangeSet: + return api.distribute_demand_to_region(name, demand, region, type) + ############################################################ # district_metering_area 35 From b3a630c5e188f77cb08cd5a4ef8de5ebfebf608e Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 14:08:29 +0800 Subject: [PATCH 28/40] Fix test case name --- test_tjnetwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_tjnetwork.py b/test_tjnetwork.py index 724f013..2808ffb 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -5837,7 +5837,7 @@ class TestApi: def test_distribute_demand_to_nodes_op(self): - p = 'test_distribute_demand_to_nodes' + p = 'test_distribute_demand_to_nodes_op' read_inp(p, f'./inp/net3.inp', '3') open_project(p) From 9b53f7b51fcd587b6fc9ac9ec6d18e1bf5f9d7d7 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 17:44:18 +0800 Subject: [PATCH 29/40] Clean virtual district --- api/s37_virtual_district.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/api/s37_virtual_district.py b/api/s37_virtual_district.py index 4d2f824..b115814 100644 --- a/api/s37_virtual_district.py +++ b/api/s37_virtual_district.py @@ -1,9 +1,8 @@ from .database import * from .s0_base import get_node_links -#from .s32_region_util import calculate_boundary, calculate_convex_hull, inflate_boundary -def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: +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 @@ -62,9 +61,6 @@ def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: vds: list[dict[str, Any]] = [] for center, value in center_node.items(): - #p = calculate_boundary(name, value) - #b = inflate_boundary(name, p) - #c = calculate_convex_hull(name, value) vds.append({ 'center': center, 'nodes': value }) return { 'virtual_districts': vds, 'isolated_nodes': isolated_nodes } From ea60c36b4e14c8a85462df795254783ae74e100f Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 17:56:55 +0800 Subject: [PATCH 30/40] Fix angle --- api/s32_region_util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/s32_region_util.py b/api/s32_region_util.py index d193063..cab8ac6 100644 --- a/api/s32_region_util.py +++ b/api/s32_region_util.py @@ -73,11 +73,11 @@ 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.asin(v[1]) + math.pi * 0.5 + 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.asin(-v[1]) + math.pi * 1.5 + return math.pi * 2 - math.asin(-v[1]) return 0 From 23cb1fe0e8ed4efc7983f842413424ea32995d5c Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 18:00:23 +0800 Subject: [PATCH 31/40] Dump inp can not open an opening project --- api/inp_out.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/inp_out.py b/api/inp_out.py index 25568ca..38cbff2 100644 --- a/api/inp_out.py +++ b/api/inp_out.py @@ -40,7 +40,9 @@ def dump_inp(project: str, inp: str, version: str = '3'): if not have_project(project): return - if not is_project_open(project): + project_open = is_project_open(project) + + if not project_open: open_project(project) dir = os.getcwd() @@ -154,7 +156,8 @@ def dump_inp(project: str, inp: str, version: str = '3'): file.close() - close_project(project) + if not project_open: + close_project(project) def export_inp(project: str, version: str = '3') -> ChangeSet: From 0656dbe83b0ee467300e942ceef1218f23b9c5eb Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 18:32:05 +0800 Subject: [PATCH 32/40] Clean code --- test_tjnetwork.py | 2 ++ tjnetwork.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test_tjnetwork.py b/test_tjnetwork.py index 2808ffb..74df416 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -6216,8 +6216,10 @@ class TestApi: self.leave(p) + # 37 virtual_district + def test_virtual_district(self): p = 'test_virtual_district' read_inp(p, f'./inp/net3.inp', '3') diff --git a/tjnetwork.py b/tjnetwork.py index 7c2fc73..0517260 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -1008,6 +1008,5 @@ def distribute_demand_to_region(name: str, demand: float, region: str, type: str # virtual_district 37 ############################################################ -# parent is whole network -def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, Any]: +def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, list[Any]]: return api.calculate_virtual_district(name, centers) From 7a1a8a3a7bfa63874a0df1332e30eb39e6606416 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 18:43:44 +0800 Subject: [PATCH 33/40] Support service area calculation --- api/__init__.py | 2 + api/s36_service_area.py | 119 ++++++++++++++++++++++++++++++++++++++++ tjnetwork.py | 3 + 3 files changed, 124 insertions(+) create mode 100644 api/s36_service_area.py diff --git a/api/__init__.py b/api/__init__.py index c22bd76..bf65fe5 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -139,4 +139,6 @@ from .s33_region import get_region_schema, get_region, set_region, add_region, d from .s34_water_distribution import DISTRIBUTION_TYPE_ADD, DISTRIBUTION_TYPE_OVERRIDE from .s34_water_distribution import distribute_demand_to_nodes, distribute_demand_to_region +from .s36_service_area import calculate_service_area + from .s37_virtual_district import calculate_virtual_district diff --git a/api/s36_service_area.py b/api/s36_service_area.py new file mode 100644 index 0000000..ec012e5 --- /dev/null +++ b/api/s36_service_area.py @@ -0,0 +1,119 @@ +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, time_index: int = 0) -> dict[str, Any]: + inp = json.loads(run_project(name)) + + time_count = len(inp['node_results'][0]['result']) + if time_index >= time_count: + return {} + + 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) + + # 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 in node_wip: + ready = False + break + if ready: + for link_node in up_link_nodes: + 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) + + sas: list[dict[str, Any]] = [] + for source, nodes in source_to_main_node.items(): + sas.append({ 'source': source, 'nodes': nodes }) + + return { 'service_areas' : sas, 'concentrations': concentration_map } diff --git a/tjnetwork.py b/tjnetwork.py index 0517260..a30ee82 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -1003,6 +1003,9 @@ def distribute_demand_to_region(name: str, demand: float, region: str, type: str # service_area 36 ############################################################ +def calculate_service_area(name: str, time_index: int = 0) -> dict[str, Any]: + return api.calculate_service_area(name, time_index) + ############################################################ # virtual_district 37 From 7a03c0aa095180df9afb2637b463fe29eae97de0 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 18:46:49 +0800 Subject: [PATCH 34/40] Add test for service area --- test_tjnetwork.py | 58 ++++++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/test_tjnetwork.py b/test_tjnetwork.py index 74df416..8c48857 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -6217,6 +6217,30 @@ class TestApi: self.leave(p) + # 36 service_area + + + def test_calculate_service_area(self): + p = 'test_virtual_district' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + result = calculate_service_area(p, 1) + sas = result['service_areas'] + assert len(sas) == 3 + assert sas[0]['source'] == 'River' + assert sas[0]['nodes'] == ['River', '60', '61', '123', '601', '121', '120', '119', '125', '151', '157', '127', '153', '149', '159', '20', '129', '147', '161', '3', '131', '139', '145', '163', '195', '141', '164', '265', '143', '166', '169', '15', '167', '171', '269', '173', '271', '199', '181', '201', '273', '35', '177', '203', '275'] + assert sas[1]['source'] == 'Lake' + assert sas[1]['nodes'] == ['117', '115', '111', '113', '197', '193', '191', '267', '187', '189', '204', '183', '185', '179', '184', '40', '205', '1', '207', '206', '208', '209', '211', '213', '237', '215', '229', '239', '217', '231', '241', '249', '219', '225', '243', 'Lake', '10', '101', '103', '105', '109', '107', '263', '259', '261', '257'] + assert sas[2]['source'] == '2' + assert sas[2]['nodes'] == ['247', '2', '50', '255', '253', '251'] + + nss = result['concentrations'] + assert nss == {'River': {'River': 1.0}, '60': {'River': 1.0}, '61': {'River': 1.0}, '123': {'River': 1.0}, '601': {'River': 1.0}, '121': {'River': 1.0}, '120': {'River': 0.8482133936739926, 'Lake': 0.15178660632600738}, '119': {'River': 0.9996314569097752, 'Lake': 0.0003685430902247304}, '125': {'River': 1.0}, '117': {'River': 0.3008743130788367, 'Lake': 0.6991256869211634}, '151': {'River': 0.9997011779067223, 'Lake': 0.00029882209327779035}, '157': {'River': 0.9996314569097753, 'Lake': 0.00036854309022473045}, '127': {'River': 1.0}, '153': {'River': 1.0}, '115': {'River': 0.1925947954811129, 'Lake': 0.8074052045188872}, '149': {'River': 0.9997011779067222, 'Lake': 0.0002988220932777903}, '159': {'River': 0.9996314569097753, 'Lake': 0.00036854309022473045}, '20': {'River': 1.0}, '129': {'River': 1.0}, '111': {'River': 0.05905486831242531, 'Lake': 0.9409451316875747}, '113': {'River': 0.0957651873908324, 'Lake': 0.9042348126091676}, '147': {'River': 0.9997011779067222, 'Lake': 0.0002988220932777903}, '161': {'River': 0.9996314569097752, 'Lake': 0.0003685430902247304}, '3': {'River': 1.0}, '131': {'River': 1.0}, '139': {'River': 1.0}, '197': {'River': 0.05905486831242531, 'Lake': 0.9409451316875747}, '193': {'River': 0.09576518739083238, 'Lake': 0.9042348126091676}, '145': {'River': 0.9997011779067223, 'Lake': 0.0002988220932777903}, '163': {'River': 0.9996314569097753, 'Lake': 0.00036854309022473045}, '195': {'River': 0.9996314569097754, 'Lake': 0.00036854309022473045}, '141': {'River': 0.9998401927344082, 'Lake': 0.00015980726559174143}, '191': {'River': 0.06331784947523231, 'Lake': 0.9366821505247677}, '267': {'River': 0.09576518739083238, 'Lake': 0.9042348126091676}, '164': {'River': 0.9996314569097753, 'Lake': 0.00036854309022473045}, '265': {'River': 0.9906312413668901, 'Lake': 0.00936875863310988}, '143': {'River': 0.9998401927344083, 'Lake': 0.00015980726559174143}, '187': {'River': 0.06331784947523231, 'Lake': 0.9366821505247677}, '189': {'River': 0.09256760501206042, 'Lake': 0.9074323949879396}, '166': {'River': 0.9996314569097753, 'Lake': 0.00036854309022473045}, '169': {'River': 0.9906312413668901, 'Lake': 0.00936875863310988}, '15': {'River': 0.9998401927344083, 'Lake': 0.00015980726559174143}, '204': {'River': 0.06331784947523231, 'Lake': 0.9366821505247677}, '183': {'River': 0.09256760501206042, 'Lake': 0.9074323949879396}, '167': {'River': 0.9906312413668902, 'Lake': 0.009368758633109882}, '171': {'River': 0.9906312413668902, 'Lake': 0.009368758633109882}, '269': {'River': 0.9906312413668901, 'Lake': 0.00936875863310988}, '185': {'River': 0.06961984624591949, 'Lake': 0.9303801537540806}, '179': {'River': 0.09256760501206042, 'Lake': 0.9074323949879396}, '173': {'River': 0.9906312413668902, 'Lake': 0.009368758633109882}, '271': {'River': 0.9906312413668902, 'Lake': 0.009368758633109882}, '184': {'River': 0.06961984624591948, 'Lake': 0.9303801537540805}, '40': {'River': 0.09256760501206042, 'Lake': 0.9074323949879396}, '199': {'River': 0.9906312413668902, 'Lake': 0.009368758633109882}, '181': {'River': 0.9906312413668901, 'Lake': 0.009368758633109882}, '205': {'River': 0.06961984624591948, 'Lake': 0.9303801537540805}, '1': {'River': 0.09256760501206042, 'Lake': 0.9074323949879396}, '201': {'River': 0.9906312413668902, 'Lake': 0.009368758633109882}, '273': {'River': 0.9906312413668902, 'Lake': 0.009368758633109882}, '35': {'River': 0.9906312413668901, 'Lake': 0.009368758633109882}, '177': {'River': 0.9906312413668901, 'Lake': 0.009368758633109882}, '207': {'River': 0.06961984624591949, 'Lake': 0.9303801537540806}, '203': {'River': 0.9906312413668901, 'Lake': 0.00936875863310988}, '275': {'River': 0.9906312413668901, 'Lake': 0.009368758633109882}, '206': {'River': 0.06961984624591948, 'Lake': 0.9303801537540805}, '208': {'River': 0.06961984624591948, 'Lake': 0.9303801537540805}, '209': {'River': 0.06961984624591948, 'Lake': 0.9303801537540805}, '211': {'River': 0.06961984624591948, 'Lake': 0.9303801537540806}, '213': {'River': 0.06961984624591948, 'Lake': 0.9303801537540806}, '237': {'River': 0.06961984624591948, 'Lake': 0.9303801537540806}, '215': {'River': 0.06961984624591948, 'Lake': 0.9303801537540806}, '229': {'River': 0.06961984624591946, 'Lake': 0.9303801537540805}, '239': {'River': 0.06961984624591946, 'Lake': 0.9303801537540805}, '217': {'River': 0.06961984624591948, 'Lake': 0.9303801537540805}, '231': {'River': 0.06961984624591948, 'Lake': 0.9303801537540806}, '241': {'River': 0.06961984624591946, 'Lake': 0.9303801537540805}, '249': {'River': 0.06794376025334521, 'Lake': 0.9079814093218103, '2': 0.024074830424844474}, '219': {'River': 0.06961984624591948, 'Lake': 0.9303801537540805}, '225': {'River': 0.06961984624591948, 'Lake': 0.9303801537540806}, '243': {'River': 0.06961984624591946, 'Lake': 0.9303801537540805}, '247': {'River': 0.030503133134582493, 'Lake': 0.40763534000762625, '2': 0.5618615268577912}, 'Lake': {'Lake': 1.0}, '10': {'Lake': 1.0}, '101': {'Lake': 1.0}, '103': {'Lake': 1.0}, '105': {'Lake': 1.0}, '109': {'Lake': 1.0}, '107': {'Lake': 1.0}, '263': {'Lake': 1.0}, '259': {'Lake': 1.0}, '261': {'Lake': 1.0}, '257': {'Lake': 1.0}, '2': {'2': 1.0}, '50': {'2': 1.0}, '255': {'2': 1.0}, '253': {'2': 1.0}, '251': {'2': 1.0}} + + self.leave(p) + + # 37 virtual_district @@ -6238,40 +6262,6 @@ class TestApi: assert vds[3]['center'] == '211' assert vds[3]['nodes'] == ['50', '199', '201', '203', '205', '206', '207', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '2'] - #assert result == { - # 'virtual_districts': - # [ - # { - # 'center': '107', - # 'nodes': ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake'], - # 'boundary': [(23.7, 22.76), (22.08, 23.1), (21.17, 23.32), (20.8, 23.4), (20.32, 21.57), (16.97, 21.28), (13.81, 22.94), (9.0, 27.85), (8.0, 27.53), (9.0, 27.85), (13.81, 22.94), (12.96, 21.31), (17.64, 18.92), (20.21, 17.53), (20.98, 19.18), (21.69, 21.28), (22.08, 23.1)], - # 'inflated_boundary': [(20.57, 17.12), (21.44, 18.98), (21.45, 19.01), (22.17, 21.13), (22.18, 21.16), (22.46, 22.5), (24.09, 22.17), (24.29, 23.150000000000002), (22.19, 23.580000000000002), (22.2, 23.59), (21.28, 23.81), (20.71, 23.93), (20.37, 23.72), (19.92, 22.03), (17.06, 21.79), (14.120000000000001, 23.330000000000002), (9.26, 28.3), (8.98, 28.37), (7.37, 27.85), (7.68, 26.900000000000002), (8.85, 27.27), (13.19, 22.84), (12.42, 21.36), (12.55, 20.96), (17.41, 18.47), (20.16, 16.990000000000002), (20.57, 17.12)], - # 'convex_hull': [(20.21, 17.53), (12.96, 21.31), (8.0, 27.53), (9.0, 27.85), (23.7, 22.76), (20.21, 17.53)] - # }, - # { - # 'center': '139', - # 'nodes': ['15', '20', '60', '601', '61', '121', '123', '125', '127', '129', '131', '139', '141', '143', '145', '147', '149', '151', '153', 'River', '3'], - # 'boundary': [(38.68, 23.76), (37.47, 21.97), (35.68, 23.08), (33.28, 24.54), (30.32, 26.39), (37.89, 29.55), (30.32, 26.39), (29.29, 26.4), (29.44, 26.91), (29.41, 27.27), (29.44, 26.91), (29.29, 26.4), (24.59, 25.64), (23.54, 25.5), (23.37, 27.31), (23.71, 29.03), (23.9, 29.94), (24.15, 31.06), (23.9, 29.94), (23.0, 29.49), (23.71, 29.03), (23.37, 27.31), (23.54, 25.5), (24.59, 25.64), (28.13, 22.63), (28.29, 21.39), (29.62, 20.74), (30.24, 20.38), (33.02, 19.29), (35.68, 23.08), (37.47, 21.97)], - # 'inflated_boundary': [(33.33, 18.86), (35.82, 22.400000000000002), (37.38, 21.44), (37.77, 21.52), (39.37, 23.89), (38.550000000000004, 24.45), (37.32, 22.64), (35.94, 23.51), (33.54, 24.97), (33.53, 24.96), (31.39, 26.29), (38.54, 29.28), (38.160000000000004, 30.2), (30.22, 26.89), (29.93, 26.89), (29.94, 26.91), (29.87, 27.810000000000002), (28.87, 27.73), (28.93, 26.95), (28.89, 26.830000000000002), (24.52, 26.13), (24.52, 26.14), (23.990000000000002, 26.060000000000002), (23.87, 27.29), (24.2, 28.93), (24.39, 29.830000000000002), (24.75, 31.44), (23.77, 31.66), (23.46, 30.28), (22.52, 29.810000000000002), (22.48, 29.23), (23.150000000000002, 28.79), (22.87, 27.37), (22.87, 27.3), (23.06, 25.240000000000002), (23.400000000000002, 24.98), (24.44, 25.11), (27.650000000000002, 22.38), (27.810000000000002, 21.2), (27.96, 21.0), (29.400000000000002, 20.29), (30.01, 19.94), (30.04, 19.92), (33.0, 18.76), (33.33, 18.86)], - # 'convex_hull': [(33.02, 19.29), (30.24, 20.38), (28.29, 21.39), (23.54, 25.5), (23.0, 29.49), (24.15, 31.06), (37.89, 29.55), (38.68, 23.76), (37.47, 21.97), (33.02, 19.29)] - # }, - # { - # 'center': '267', - # 'nodes': ['35', '40', '113', '157', '159', '161', '163', '164', '166', '167', '169', '171', '173', '177', '179', '181', '183', '184', '185', '187', '189', '191', '193', '195', '197', '204', '265', '267', '269', '271', '1'], - # 'boundary': [(27.46, 9.84), (27.02, 9.81), (25.71, 10.4), (25.45, 10.18), (24.15, 11.37), (25.03, 12.14), (25.97, 11.0), (25.72, 10.74), (25.46, 10.52), (25.72, 10.74), (25.97, 11.0), (26.65, 11.8), (26.87, 11.59), (26.65, 11.8), (25.68, 12.74), (25.88, 12.98), (25.68, 12.74), (25.39, 13.6), (25.39, 14.98), (25.98, 15.14), (26.48, 15.13), (25.98, 15.14), (25.39, 14.98), (25.1, 15.28), (23.12, 17.5), (24.85, 20.16), (23.12, 17.5), (25.1, 15.28), (23.18, 14.72), (22.88, 14.35), (22.04, 16.61), (22.88, 14.35), (22.1, 14.07), (20.97, 15.18), (22.1, 14.07), (23.64, 11.04), (23.8, 10.9), (25.01, 9.67), (25.15, 9.52), (25.01, 9.67), (25.45, 10.18), (25.71, 10.4), (27.02, 9.81)], - # 'inflated_boundary': [(25.86, 9.5), (25.68, 9.68), (25.79, 9.8), (26.87, 9.33), (26.990000000000002, 9.31), (27.990000000000002, 9.38), (27.92, 10.370000000000001), (27.12, 10.31), (26.34, 10.66), (26.35, 10.67), (26.68, 11.07), (26.89, 10.88), (27.580000000000002, 11.61), (27.0, 12.16), (26.36, 12.77), (26.580000000000002, 13.040000000000001), (25.91, 13.6), (25.89, 13.67), (25.89, 14.6), (26.03, 14.63), (26.97, 14.620000000000001), (26.990000000000002, 15.620000000000001), (25.95, 15.64), (25.88, 15.63), (25.54, 15.530000000000001), (25.46, 15.63), (25.45, 15.620000000000001), (23.740000000000002, 17.54), (25.54, 20.31), (24.7, 20.85), (22.61, 17.63), (22.63, 17.29), (24.2, 15.530000000000001), (23.09, 15.21), (22.330000000000002, 17.25), (21.400000000000002, 16.9), (22.23, 14.64), (22.22, 14.64), (20.96, 15.89), (20.26, 15.17), (21.68, 13.77), (23.22, 10.77), (23.27, 10.700000000000001), (23.47, 10.52), (24.650000000000002, 9.32), (25.13, 8.81), (25.86, 9.5)], - # 'convex_hull': [(25.15, 9.52), (23.64, 11.04), (20.97, 15.18), (22.04, 16.61), (24.85, 20.16), (26.48, 15.13), (27.46, 9.84), (25.15, 9.52)] - # }, - # { - # 'center': '211', - # 'nodes': ['50', '199', '201', '203', '205', '206', '207', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '2'], - # 'boundary': [(44.86, 9.32), (42.11, 8.67), (39.95, 8.73), (35.26, 6.16), (34.2, 5.54), (33.76, 6.59), (32.54, 6.81), (31.66, 6.64), (31.0, 6.61), (31.07, 8.29), (30.89, 8.57), (31.14, 8.89), (30.89, 8.57), (29.42, 8.44), (29.16, 7.38), (29.2, 6.46), (31.0, 6.61), (31.66, 6.64), (32.54, 6.81), (33.76, 6.59), (34.2, 5.54), (35.37, 3.08), (35.76, 2.31), (35.02, 1.81), (35.02, 2.05), (35.87, 2.11), (37.04, 0.0), (35.87, 2.11), (35.76, 2.31), (35.37, 3.08), (36.16, 3.49), (38.38, 2.54), (36.16, 3.49), (35.26, 6.16), (39.95, 8.73), (42.11, 8.67), (43.53, 7.38), (42.11, 8.67)], - # 'inflated_boundary': [(37.72, -0.19), (36.31, 2.35), (36.2, 2.5300000000000002), (36.21, 2.54), (36.04, 2.86), (36.17, 2.93), (38.64, 1.8800000000000001), (39.04, 2.8000000000000003), (36.550000000000004, 3.86), (35.86, 5.91), (40.07, 8.22), (41.9, 8.17), (43.56, 6.67), (44.24, 7.41), (43.15, 8.39), (45.46, 8.950000000000001), (45.230000000000004, 9.92), (42.04, 9.17), (39.9, 9.23), (39.77, 9.200000000000001), (35.02, 6.6000000000000005), (35.01, 6.59), (34.43, 6.25), (34.17, 6.9), (33.97, 7.0600000000000005), (32.58, 7.3100000000000005), (32.49, 7.3100000000000005), (31.61, 7.13), (31.52, 7.13), (31.57, 8.35), (31.53, 8.5), (31.5, 8.540000000000001), (31.84, 8.98), (31.05, 9.59), (30.63, 9.040000000000001), (29.22, 8.92), (28.97, 8.71), (28.67, 7.46), (28.66, 7.390000000000001), (28.71, 6.23), (29.03, 5.94), (31.04, 6.11), (31.03, 6.11), (31.7, 6.140000000000001), (31.740000000000002, 6.15), (32.54, 6.3), (33.4, 6.140000000000001), (33.74, 5.34), (33.75, 5.33), (34.92, 2.86), (35.11, 2.47), (34.74, 2.22), (35.02, 1.81), (35.04, 1.81), (35.06, 1.55), (35.58, 1.58), (36.85, -0.68), (37.72, -0.19)], - # 'convex_hull': [(37.04, 0.0), (34.15, 1.1), (32.17, 1.88), (29.2, 6.46), (29.16, 7.38), (29.42, 8.44), (31.14, 8.89), (44.86, 9.32), (43.53, 7.38), (37.04, 0.0)] - # } - # ], - # 'isolated_nodes': []} - self.leave(p) From 7abae713514a4336841e0ec80d99335505e1db21 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 22:09:38 +0800 Subject: [PATCH 35/40] Prevent hardcode enum --- api/s32_region_util.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/s32_region_util.py b/api/s32_region_util.py index cab8ac6..195e9db 100644 --- a/api/s32_region_util.py +++ b/api/s32_region_util.py @@ -200,8 +200,10 @@ def inflate_boundary(name: str, boundary: list[tuple[float, float]], delta: floa c_path[i] = xy[1] i += 1 c_delta = ctypes.c_double(delta) - c_jt = ctypes.c_int(0) - c_et = ctypes.c_int(0) + 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) From 703a2db7245c2a8b9914fe80073575c3eee6711b Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 23:57:25 +0800 Subject: [PATCH 36/40] Add metis wrapper lib --- api/CMetis.dll | Bin 0 -> 150016 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 api/CMetis.dll diff --git a/api/CMetis.dll b/api/CMetis.dll new file mode 100644 index 0000000000000000000000000000000000000000..3385f4c80c6253d40de1cfeea97bc4fb037a6ac4 GIT binary patch literal 150016 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 NX^DL9_kXnp{tIIe4j}*l literal 0 HcmV?d00001 From 2abf861584b838f2d7d488f1ae252e824bd8eb80 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 23:57:55 +0800 Subject: [PATCH 37/40] Add more info to topology --- api/s32_region_util.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/s32_region_util.py b/api/s32_region_util.py index 195e9db..eee3d85 100644 --- a/api/s32_region_util.py +++ b/api/s32_region_util.py @@ -94,16 +94,19 @@ 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 = {} + self._link_list: list[str] = [] for node in self._nodes: for link in get_node_links(db, node): candidate = True @@ -115,7 +118,7 @@ class Topology: 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']: @@ -124,11 +127,17 @@ class Topology: 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(name: str, nodes: list[str]) -> list[tuple[float, float]]: @@ -141,6 +150,7 @@ def calculate_boundary(name: str, nodes: list[str]) -> list[tuple[float, float]] paths: list[str] = [] while True: + #print(cursor) paths.append(cursor) sorted_links = [] overlapped_link = '' From 98b63541652880aaeb7fec2dc3d169ccb5dcfa79 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Sun, 30 Apr 2023 23:58:22 +0800 Subject: [PATCH 38/40] Support dma calculation --- api/__init__.py | 3 ++ api/s35_district_metering_area.py | 85 +++++++++++++++++++++++++++++++ tjnetwork.py | 7 +++ 3 files changed, 95 insertions(+) create mode 100644 api/s35_district_metering_area.py diff --git a/api/__init__.py b/api/__init__.py index bf65fe5..4d98d94 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -139,6 +139,9 @@ from .s33_region import get_region_schema, get_region, set_region, add_region, d from .s34_water_distribution import DISTRIBUTION_TYPE_ADD, DISTRIBUTION_TYPE_OVERRIDE from .s34_water_distribution import distribute_demand_to_nodes, distribute_demand_to_region +from .s35_district_metering_area import PARTITION_TYPE_RB, PARTITION_TYPE_KWAY +from .s35_district_metering_area import calculate_district_metering_area + from .s36_service_area import calculate_service_area from .s37_virtual_district import calculate_virtual_district diff --git a/api/s35_district_metering_area.py b/api/s35_district_metering_area.py new file mode 100644 index 0000000..d907374 --- /dev/null +++ b/api/s35_district_metering_area.py @@ -0,0 +1,85 @@ +import ctypes +import os +from .database import * +from .s32_region_util import Topology + + +PARTITION_TYPE_RB = 0 +PARTITION_TYPE_KWAY = 1 + + +def calculate_district_metering_area(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 diff --git a/tjnetwork.py b/tjnetwork.py index a30ee82..522f931 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -167,6 +167,10 @@ DISTRIBUTION_TYPE_ADD = api.DISTRIBUTION_TYPE_ADD DISTRIBUTION_TYPE_OVERRIDE = api.DISTRIBUTION_TYPE_OVERRIDE +PARTITION_TYPE_RB = api.PARTITION_TYPE_RB +PARTITION_TYPE_KWAY = api.PARTITION_TYPE_KWAY + + ############################################################ # project ############################################################ @@ -998,6 +1002,9 @@ def distribute_demand_to_region(name: str, demand: float, region: str, type: str # district_metering_area 35 ############################################################ +def calculate_district_metering_area(name: str, nodes: list[str], part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]: + return api.calculate_district_metering_area(name, nodes, part_count, part_type) + ############################################################ # service_area 36 From 06c8f366fda01de1f0c51d6faae00c26ec33dac8 Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Mon, 1 May 2023 00:08:56 +0800 Subject: [PATCH 39/40] Add test for dma --- test_tjnetwork.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/test_tjnetwork.py b/test_tjnetwork.py index 8c48857..f3556e7 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -6217,11 +6217,28 @@ class TestApi: self.leave(p) + # 35 district_metering_area + + + def test_calculate_district_metering_area(self): + p = 'test_calculate_district_metering_area' + read_inp(p, f'./inp/net3.inp', '3') + open_project(p) + + dmas = calculate_district_metering_area(p, get_nodes(p), 3) + assert len(dmas) == 3 + assert dmas[0] == ['173', '184', '185', '199', '2', '201', '203', '205', '206', '207', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '50'] + assert dmas[1] == ['1', '10', '101', '103', '109', '111', '113', '161', '163', '164', '166', '167', '169', '171', '179', '181', '183', '187', '189', '191', '193', '195', '197', '204', '265', '267', '269', '271', '35', '40', 'Lake'] + assert dmas[2] == ['105', '107', '115', '117', '119', '120', '121', '123', '125', '127', '129', '131', '139', '141', '143', '145', '147', '149', '15', '151', '153', '157', '159', '20', '257', '259', '261', '263', '3', '60', '601', '61', 'River'] + + self.leave(p) + + # 36 service_area def test_calculate_service_area(self): - p = 'test_virtual_district' + p = 'test_calculate_service_area' read_inp(p, f'./inp/net3.inp', '3') open_project(p) @@ -6244,8 +6261,8 @@ class TestApi: # 37 virtual_district - def test_virtual_district(self): - p = 'test_virtual_district' + def test_calculate_virtual_district(self): + p = 'test_calculate_virtual_district' read_inp(p, f'./inp/net3.inp', '3') open_project(p) From 8a1a49bf6b735659f35311a6fd09a238ab766eaa Mon Sep 17 00:00:00 2001 From: "WQY\\qiong" Date: Tue, 2 May 2023 17:47:50 +0800 Subject: [PATCH 40/40] Add test for UTF8 --- api/__init__.py | 1 + test_tjnetwork.py | 15 +++++++++++++++ tjnetwork.py | 12 ++++++++++++ 3 files changed, 28 insertions(+) diff --git a/api/__init__.py b/api/__init__.py index 4d98d94..4fa3293 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -18,6 +18,7 @@ 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 diff --git a/test_tjnetwork.py b/test_tjnetwork.py index f3556e7..a287a7d 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -19,6 +19,21 @@ class TestApi: delete_project(p) + # encoding + + + def test_utf8(self): + p = 'test_utf8' + self.enter(p) + + write(p, f"create table {p} (a varchar(32))") + write(p, f"insert into {p} values ('你好')") + result = read_all(p, f"select * from { p}") + assert result == [{'a': '你好'}] + + self.leave(p) + + # project diff --git a/tjnetwork.py b/tjnetwork.py index 522f931..208fe9c 100644 --- a/tjnetwork.py +++ b/tjnetwork.py @@ -308,6 +308,18 @@ def set_restore_operation_to_current(name: str) -> None: def restore(name: str, discard: bool = False) -> ChangeSet: return api.restore(name, discard) +def read(name: str, sql: str): + return api.read(name, sql) + +def try_read(name: str, sql: str): + return api.try_read(name, sql) + +def read_all(name: str, sql: str): + return api.read_all(name, sql) + +def write(name: str, sql: str): + return api.write(name, sql) + ############################################################ # type