refactor(db)!: adopt project-routed pooled databases

Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
This commit is contained in:
2026-08-25 18:35:05 +08:00
parent fdbcc5c033
commit fa188af0b1
181 changed files with 8446 additions and 33546 deletions
+1
View File
@@ -0,0 +1 @@
"""Water-network model persistence grouped by domain entity."""
+54
View File
@@ -0,0 +1,54 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
)
def get_control_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'controls' : {'type': 'str_list' , 'optional': False , 'readonly': False} }
def get_control(name: str) -> dict[str, Any]:
cs = read_all(name, "select line from network.controls order by sequence_no")
ds = []
for c in cs:
ds.append(c['line'])
return { 'controls': ds }
def _set_control(name: str, cs: ChangeSet) -> DatabaseCommand:
statement = 'delete from network.controls;'
for sequence_no, line in enumerate(cs.operations[0]['controls']):
statement += f"\ninsert into network.controls (sequence_no, line) values ({sequence_no}, {sql_literal(line)});"
change = g_update_prefix | { 'type': 'control', 'controls': cs.operations[0]['controls'] }
return DatabaseCommand(statement, [change])
def set_control(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_control(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3]
# LINK linkID setting IF NODE nodeID {BELOW/ABOVE} level
# LINK linkID setting AT TIME value (units)
# LINK linkID setting AT CLOCKTIME value (units)
# (0) (1) (2) (3) (4) (5) (6) (7)
# todo...
#--------------------------------------------------------------
def inp_in_control(line: str) -> str:
return str(f"insert into network.controls (sequence_no, line) values ((select coalesce(max(sequence_no) + 1, 0) from network.controls), {sql_literal(line)});")
def inp_out_control(name: str) -> list[str]:
return get_control(name)['controls']
+174
View File
@@ -0,0 +1,174 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
CURVE_TYPE_PUMP = 'PUMP'
CURVE_TYPE_EFFICIENCY = 'EFFICIENCY'
CURVE_TYPE_VOLUME = 'VOLUME'
CURVE_TYPE_HEADLOSS = 'HEADLOSS'
curve_types = [CURVE_TYPE_PUMP, CURVE_TYPE_EFFICIENCY, CURVE_TYPE_VOLUME, CURVE_TYPE_HEADLOSS]
def get_curve_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
'c_type' : {'type': 'str' , 'optional': False , 'readonly': False},
'coords' : {'type': 'list' , 'optional': False , 'readonly': False,
'element': { 'x' : {'type': 'float' , 'optional': False , 'readonly': False },
'y' : {'type': 'float' , 'optional': False , 'readonly': False } }}}
def get_curve(name: str, id: str) -> dict[str, Any]:
c_one = try_read(name, "select id, curve_type from network.curves where id = %s", (id,))
if c_one == None:
return {}
cus = read_all(name, "select x, y from network.curve_points where curve_id = %s order by sequence_no", (id,))
cs = []
for r in cus:
cs.append({ 'x': float(r['x']), 'y': float(r['y']) })
d = {}
d['id'] = id
d['c_type'] = c_one['curve_type']
d['coords'] = cs
return d
def _set_curve(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
f_id = sql_literal(id)
old = get_curve(name, id)
new = { 'id': id }
if 'coords' in cs.operations[0]:
new['coords'] = cs.operations[0]['coords']
else:
new['coords'] = old['coords']
if 'c_type' in cs.operations[0]:
new['c_type'] = cs.operations[0]['c_type']
else:
new['c_type'] = old['c_type']
new_f_type = sql_literal(new['c_type'])
statement = f"delete from network.curve_points where curve_id = {f_id};"
statement += f"\nupdate network.curves set curve_type = {new_f_type} where id = {f_id};"
for sequence_no, xy in enumerate(new['coords']):
f_x, f_y = sql_literal(xy['x']), sql_literal(xy['y'])
statement += f"\ninsert into network.curve_points (curve_id, sequence_no, x, y) values ({f_id}, {sequence_no}, {f_x}, {f_y});"
change = g_update_prefix | { 'type': 'curve' } | new
return DatabaseCommand(statement, [change])
def set_curve(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_curve(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _set_curve(name, cs))
def _add_curve(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
f_id = sql_literal(id)
new = { 'id': id, 'c_type': cs.operations[0]['c_type'], 'coords': [] }
new_f_type = sql_literal(new['c_type'])
statement = f"insert into network.curves (id, curve_type) values ({f_id}, {new_f_type});"
for sequence_no, xy in enumerate(cs.operations[0]['coords']):
x, y = float(xy['x']), float(xy['y'])
f_x, f_y = sql_literal(x), sql_literal(y)
statement += f"\ninsert into network.curve_points (curve_id, sequence_no, x, y) values ({f_id}, {sequence_no}, {f_x}, {f_y});"
new['coords'].append({ 'x': x, 'y': y })
change = g_add_prefix | { 'type': 'curve' } | new
return DatabaseCommand(statement, [change])
def add_curve(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_curve(name, cs.operations[0]['id']) != {}:
return ChangeSet()
return execute_command(name, _add_curve(name, cs))
def _delete_curve(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
f_id = sql_literal(id)
statement = f"delete from network.curves where id = {f_id};"
change = g_delete_prefix | { 'type': 'curve' } | { 'id' : id }
return DatabaseCommand(statement, [change])
def delete_curve(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_curve(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _delete_curve(name, cs))
#--------------------------------------------------------------
# [EPA2][IN][OUT]
# ;type: desc
# id x y
#--------------------------------------------------------------
#--------------------------------------------------------------
# [EPA3][IN][OUT]
# id type
# id x y
#--------------------------------------------------------------
def inp_in_curve(line: str) -> str:
tokens = line.split()
curve_id = sql_literal(tokens[0])
return str(f"insert into network.curve_points (curve_id, sequence_no, x, y) values ({curve_id}, (select coalesce(max(sequence_no) + 1, 0) from network.curve_points where curve_id = {curve_id}), {sql_literal(float(tokens[1]))}, {sql_literal(float(tokens[2]))});")
def inp_out_curve(name: str) -> list[str]:
lines = []
types = read_all(name, "select id, curve_type as type from network.curves order by id")
for type in types:
id = type['id']
# ;type: desc
lines.append(f";{type['type']}:")
objs = read_all(name, "select curve_id as id, x, y from network.curve_points where curve_id = %s order by sequence_no", (id,))
for obj in objs:
id = obj['id']
x = obj['x']
y = obj['y']
lines.append(f'{id} {x} {y}')
return lines
def inp_out_curve_v3(name: str) -> list[str]:
lines = []
types = read_all(name, "select id, curve_type as type from network.curves order by id")
for type in types:
id = type['id']
# id type
lines.append(f"{id} {type['type']}")
objs = read_all(name, "select curve_id as id, x, y from network.curve_points where curve_id = %s order by sequence_no", (id,))
for obj in objs:
id = obj['id']
x = obj['x']
y = obj['y']
lines.append(f'{id} {x} {y}')
return lines
+109
View File
@@ -0,0 +1,109 @@
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
from typing import Any
def get_demand_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'junction' : {'type': 'str' , 'optional': False , 'readonly': True },
'demands' : {'type': 'list' , 'optional': False , 'readonly': False,
'element': { 'demand' : {'type': 'float' , 'optional': False , 'readonly': False },
'pattern' : {'type': 'str' , 'optional': True , 'readonly': False },
'category': {'type': 'str' , 'optional': True , 'readonly': False }}}}
def get_demand(name: str, junction: str) -> dict[str, Any]:
des = read_all(name, "select base_demand as demand, pattern_id as pattern, category from network.demands where junction_id = %s order by sequence_no", (junction,))
ds = []
for r in des:
d = {}
d['demand'] = float(r['demand'])
d['pattern'] = str(r['pattern']) if r['pattern'] != None else None
d['category'] = str(r['category']) if r['category'] != None else None
ds.append(d)
return { 'junction': junction, 'demands': ds }
def _set_demand(name: str, cs: ChangeSet) -> DatabaseCommand:
junction = cs.operations[0]['junction']
new = { 'junction': junction, 'demands': [] }
f_junction = sql_literal(junction)
statement = f"delete from network.demands where junction_id = {f_junction};"
for sequence_no, r in enumerate(cs.operations[0]['demands']):
demand = float(r['demand'])
pattern = str(r['pattern']) if 'pattern' in r and r['pattern'] != None else None
category = str(r['category']) if 'category' in r and r['category'] != None else None
f_demand = sql_literal(demand)
f_pattern = sql_literal(pattern)
f_category = sql_literal(category)
statement += f"\ninsert into network.demands (junction_id, sequence_no, base_demand, pattern_id, category) values ({f_junction}, {sequence_no}, {f_demand}, {f_pattern}, {f_category});"
new['demands'].append({ 'demand': demand, 'pattern': pattern, 'category': category })
change = g_update_prefix | { 'type': 'demand' } | new
return DatabaseCommand(statement, [change])
def set_demand(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_demand(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# node base_demand (pattern) ;category
#--------------------------------------------------------------
def inp_in_demand(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
junction = str(tokens[0])
demand = float(tokens[1])
pattern = str(tokens[2]) if num_without_desc >= 3 else None
category = str(tokens[3]) if num_without_desc >= 4 else None
junction_sql = sql_literal(junction)
return str(f"insert into network.demands (junction_id, sequence_no, base_demand, pattern_id, category) values ({junction_sql}, (select coalesce(max(sequence_no) + 1, 0) from network.demands where junction_id = {junction_sql}), {sql_literal(demand)}, {sql_literal(pattern)}, {sql_literal(category)});")
def inp_out_demand(name: str) -> list[str]:
lines = []
objs = read_all(name, "select junction_id as junction, base_demand as demand, pattern_id as pattern, category from network.demands order by junction_id, sequence_no")
for obj in objs:
junction = obj['junction']
demand = obj['demand']
pattern = obj['pattern'] if obj['pattern'] is not None else ''
category = f";{obj['category']}" if obj['category'] is not None else ';'
lines.append(f'{junction} {demand} {pattern} {category}')
return lines
def delete_demand_by_junction(name: str, junction: str) -> ChangeSet:
row = try_read(name, "select 1 from network.demands where junction_id = %s", (junction,))
if row is None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type': 'demand', 'junction': junction, 'demands': []})
def unset_demand_by_pattern(name: str, pattern: str) -> ChangeSet:
cs = ChangeSet()
rows = read_all(name, "select distinct junction_id as junction from network.demands where pattern_id = %s", (pattern,))
for row in rows:
ds = get_demand(name, row['junction'])
for d in ds['demands']:
d['pattern'] = None
cs.append(g_update_prefix | {'type': 'demand', 'junction': row['junction'], 'demands': ds['demands']})
return cs
+282
View File
@@ -0,0 +1,282 @@
from typing import Any
from psycopg import sql
from psycopg.rows import Row, dict_row
from ..core.connection import project_connection
from ..core.database import read
_NODE = "network.nodes"
_LINK = "network.links"
_CURVE = "network.curves"
_PATTERN = "network.patterns"
_REGION = "gis.regions"
JUNCTION = "junction"
RESERVOIR = "reservoir"
TANK = "tank"
PIPE = "pipe"
PUMP = "pump"
VALVE = "valve"
PATTERN = "pattern"
CURVE = "curve"
REGION = "region"
ELEMENT_TYPES: dict[str, int] = {
RESERVOIR: 0,
TANK: 1,
JUNCTION: 2,
PIPE: 3,
PUMP: 4,
VALVE: 5,
}
def _table_identifier(table: str):
return sql.Identifier(*table.split("."))
def _get_from(name: str, element_id: str, table: str) -> Row | None:
query = sql.SQL("SELECT * FROM {} WHERE id = %s").format(
_table_identifier(table)
)
with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
cur.execute(query, (element_id,))
return cur.fetchone()
def is_node(name: str, element_id: str) -> bool:
return _get_from(name, element_id, _NODE) is not None
def is_junction(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _NODE)
return row is not None and row["node_type"] == JUNCTION
def is_reservoir(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _NODE)
return row is not None and row["node_type"] == RESERVOIR
def is_tank(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _NODE)
return row is not None and row["node_type"] == TANK
def is_link(name: str, element_id: str) -> bool:
return _get_from(name, element_id, _LINK) is not None
def is_pipe(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _LINK)
return row is not None and row["link_type"] == PIPE
def is_pump(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _LINK)
return row is not None and row["link_type"] == PUMP
def is_valve(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _LINK)
return row is not None and row["link_type"] == VALVE
def get_node_type(name: str, node_id: str) -> str:
row = _get_from(name, node_id, _NODE)
if row is None:
raise LookupError(node_id)
return row["node_type"]
def get_link_type(name: str, link_id: str) -> str:
row = _get_from(name, link_id, _LINK)
if row is None:
raise LookupError(link_id)
return row["link_type"]
def get_element_type(name: str, element_id: str) -> str | None:
if is_node(name, element_id):
return get_node_type(name, element_id)
if is_link(name, element_id):
return get_link_type(name, element_id)
return None
def get_element_type_value(name: str, element_id: str) -> int:
element_type = get_element_type(name, element_id)
if element_type is None:
raise LookupError(element_id)
return ELEMENT_TYPES[element_type]
def is_curve(name: str, element_id: str) -> bool:
return _get_from(name, element_id, _CURVE) is not None
def is_pattern(name: str, element_id: str) -> bool:
return _get_from(name, element_id, _PATTERN) is not None
def is_region(name: str, element_id: str) -> bool:
return _get_from(name, element_id, _REGION) is not None
def _get_all(name: str, table: str) -> list[str]:
query = sql.SQL("SELECT id FROM {} ORDER BY id").format(_table_identifier(table))
with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
cur.execute(query)
return [row["id"] for row in cur]
def _get_nodes_by_type(name: str, node_type: str) -> list[str]:
rows = read_all_typed(
name,
"SELECT id FROM network.nodes WHERE node_type = %s ORDER BY id",
(node_type,),
)
return [row["id"] for row in rows]
def _get_links_by_type(name: str, link_type: str) -> list[str]:
rows = read_all_typed(
name,
"SELECT id FROM network.links WHERE link_type = %s ORDER BY id",
(link_type,),
)
return [row["id"] for row in rows]
def read_all_typed(name: str, query: str, params: tuple[Any, ...]) -> list[Row]:
with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
cur.execute(query, params)
return cur.fetchall()
def get_nodes(name: str) -> list[str]:
return _get_all(name, _NODE)
def get_nodes_id_and_type(name: str) -> dict[str, str]:
rows = read_all_typed(name, "SELECT id, node_type FROM network.nodes", ())
return {row["id"]: row["node_type"] for row in rows}
def get_major_nodes(name: str, diameter: int) -> list[str]:
rows = read_all_typed(
name,
"""
SELECT DISTINCT endpoint
FROM network.links AS l
JOIN network.pipes AS p ON p.link_id = l.id
CROSS JOIN LATERAL (VALUES (l.start_node_id), (l.end_node_id)) AS e(endpoint)
WHERE p.diameter > %s
""",
(diameter,),
)
return [row["endpoint"] for row in rows]
def get_junctions(name: str) -> list[str]:
return _get_nodes_by_type(name, JUNCTION)
def get_reservoirs(name: str) -> list[str]:
return _get_nodes_by_type(name, RESERVOIR)
def get_tanks(name: str) -> list[str]:
return _get_nodes_by_type(name, TANK)
def get_links(name: str) -> list[str]:
return _get_all(name, _LINK)
def get_links_id_and_type(name: str) -> dict[str, str]:
rows = read_all_typed(name, "SELECT id, link_type FROM network.links", ())
return {row["id"]: row["link_type"] for row in rows}
def get_major_pipes(name: str, diameter: int) -> list[str]:
rows = read_all_typed(
name,
"SELECT link_id FROM network.pipes WHERE diameter > %s ORDER BY link_id",
(diameter,),
)
return [row["link_id"] for row in rows]
def get_pipes(name: str) -> list[str]:
return _get_links_by_type(name, PIPE)
def get_pumps(name: str) -> list[str]:
return _get_links_by_type(name, PUMP)
def get_valves(name: str) -> list[str]:
return _get_links_by_type(name, VALVE)
def get_curves(name: str) -> list[str]:
return _get_all(name, _CURVE)
def get_patterns(name: str) -> list[str]:
return _get_all(name, _PATTERN)
def get_regions(name: str) -> list[str]:
return _get_all(name, _REGION)
def get_node_links(name: str, node_id: str) -> list[str]:
rows = read_all_typed(
name,
"""
SELECT id FROM network.links
WHERE start_node_id = %s OR end_node_id = %s
ORDER BY id
""",
(node_id, node_id),
)
return [row["id"] for row in rows]
def get_all_node_links(name: str) -> dict[str, list[str]]:
"""Build the node adjacency map with one scan of the link table."""
rows = read_all_typed(
name,
"SELECT id, start_node_id, end_node_id FROM network.links ORDER BY id",
(),
)
result: dict[str, list[str]] = {}
for row in rows:
link_id = str(row["id"])
result.setdefault(str(row["start_node_id"]), []).append(link_id)
result.setdefault(str(row["end_node_id"]), []).append(link_id)
return result
def get_link_nodes(name: str, link_id: str) -> list[str]:
row = read(
name,
"""
SELECT start_node_id, end_node_id
FROM network.links WHERE id = %s
""",
(link_id,),
)
return [str(row["start_node_id"]), str(row["end_node_id"])]
def get_region_type(name: str, region_id: str) -> str:
row = read(
name,
"SELECT region_type FROM gis.regions WHERE id = %s",
(region_id,),
)
return row["region_type"]
+102
View File
@@ -0,0 +1,102 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
def get_emitter_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'junction' : {'type': 'str' , 'optional': False , 'readonly': True },
'coefficient' : {'type': 'float' , 'optional': True , 'readonly': False} }
def get_emitter(name: str, junction: str) -> dict[str, Any]:
e = try_read(name, "select junction_id as junction, coefficient from network.emitters where junction_id = %s", (junction,))
if e == None:
return { 'junction': junction, 'coefficient': None }
d = {}
d['junction'] = str(e['junction'])
d['coefficient'] = float(e['coefficient']) if e['coefficient'] != None else None
return d
class Emitter(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'emitter'
self.junction = str(input['junction'])
self.coefficient = float(input['coefficient']) if 'coefficient' in input and input['coefficient'] != None else None
self.f_type = sql_literal(self.type)
self.f_junction = sql_literal(self.junction)
self.f_coefficient = sql_literal(self.coefficient)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'junction': self.junction, 'coefficient': self.coefficient }
def _set_emitter(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_emitter(name, cs.operations[0]['junction'])
new_dict = cs.operations[0]
schema = get_emitter_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Emitter(raw_new)
statement = f"delete from network.emitters where junction_id = {new.f_junction};"
if new.coefficient != None:
statement += f"\ninsert into network.emitters (junction_id, coefficient) values ({new.f_junction}, {new.f_coefficient});"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_emitter(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_emitter(name, cs))
#--------------------------------------------------------------
# [EPA2][IN][OUT]
# node Ke
#--------------------------------------------------------------
# [EPA3][IN][OUT]
# node Ke (exponent pattern)
#--------------------------------------------------------------
def inp_in_emitter(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
junction = str(tokens[0])
coefficient = float(tokens[1])
return str(f"insert into network.emitters (junction_id, coefficient) values ({sql_literal(junction)}, {sql_literal(coefficient)});")
def inp_out_emitter(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select junction_id as junction, coefficient from network.emitters order by junction_id')
for obj in objs:
junction = obj['junction']
coefficient = obj['coefficient']
lines.append(f'{junction} {coefficient}')
return lines
def delete_emitter_by_junction(name: str, junction: str) -> ChangeSet:
row = try_read(name, "select 1 from network.emitters where junction_id = %s", (junction,))
if row == None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type' : 'emitter', 'junction': junction, 'coefficient': None})
+220
View File
@@ -0,0 +1,220 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
element_schema = {'type': 'str' , 'optional': True , 'readonly': False}
def get_energy_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'GLOBAL PRICE' : element_schema,
'GLOBAL PATTERN' : element_schema,
'GLOBAL EFFIC' : element_schema,
'DEMAND CHARGE' : element_schema }
def get_energy(name: str) -> dict[str, Any]:
ts = read_all(name, "select key, value from network.energy_settings")
d = {}
for e in ts:
d[e['key']] = str(e['value'])
return d
def _set_energy(name: str, cs: ChangeSet) -> DatabaseCommand:
new = {}
new_dict = cs.operations[0]
schema = get_energy_schema(name)
for key in schema.keys():
if key in new_dict:
new[key] = str(new_dict[key])
change = g_update_prefix | { 'type' : 'energy' }
statement = ''
for key, value in new.items():
if statement != '':
statement += '\n'
statement += f"update network.energy_settings set value = {sql_literal(value)} where key = {sql_literal(key)};"
change |= { key: value }
return DatabaseCommand(statement, [change])
def set_energy(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_energy(name, cs))
def get_pump_energy_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'pump' : {'type': 'str' , 'optional': False , 'readonly': True },
'price' : {'type': 'float' , 'optional': True , 'readonly': False},
'pattern' : {'type': 'str' , 'optional': True , 'readonly': False},
'effic' : {'type': 'str' , 'optional': True , 'readonly': False} }
def get_pump_energy(name: str, pump: str) -> dict[str, Any]:
d = {}
d['pump'] = pump
pe = try_read(name, "select price, pattern_id as pattern, efficiency_curve_id as effic from network.pump_energy_settings where pump_id = %s", (pump,))
d['price'] = float(pe['price']) if pe is not None and pe['price'] is not None else None
d['pattern'] = str(pe['pattern']) if pe is not None and pe['pattern'] is not None else None
d['effic'] = str(pe['effic']) if pe is not None and pe['effic'] is not None else None
return d
class PumpEnergy(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'pump_energy'
self.pump = str(input['pump'])
self.price = float(input['price']) if 'price' in input and input['price'] != None else None
self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None
self.effic = str(input['effic']) if 'effic' in input and input['effic'] != None else None
self.f_type = sql_literal(self.type)
self.f_pump = sql_literal(self.pump)
self.f_price = sql_literal(self.price)
self.f_pattern = sql_literal(self.pattern)
self.f_effic = sql_literal(self.effic)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'pump': self.pump, 'price': self.price, 'pattern': self.pattern, 'effic': self.effic }
def _set_pump_energy(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_pump_energy(name, cs.operations[0]['pump'])
new_dict = cs.operations[0]
schema = get_pump_energy_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = PumpEnergy(raw_new)
statement = f"delete from network.pump_energy_settings where pump_id = {new.f_pump};"
if new.price is not None or new.pattern is not None or new.effic is not None:
statement += f"\ninsert into network.pump_energy_settings (pump_id, efficiency_curve_id, pattern_id, price) values ({new.f_pump}, {new.f_effic}, {new.f_pattern}, {new.f_price});"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_pump_energy(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_pump_energy(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# GLOBAL {PRICE/PATTERN/EFFIC} value
# PUMP id {PRICE/PATTERN/EFFIC} value
# DEMAND CHARGE value
#--------------------------------------------------------------
def inp_in_energy(line: str) -> str:
tokens = line.split()
if tokens[0].upper() == 'PUMP':
pump = tokens[1]
key = tokens[2].lower()
value = tokens[3]
if key == 'price':
value = float(value)
if key == 'efficiency':
key = 'effic'
column = {'price': 'price', 'pattern': 'pattern_id', 'effic': 'efficiency_curve_id'}[key]
return str(f"insert into network.pump_energy_settings (pump_id, {column}) values ({sql_literal(pump)}, {sql_literal(value)}) on conflict (pump_id) do update set {column} = excluded.{column};")
else:
line = line.upper().strip()
for key in get_energy_schema('').keys():
if line.startswith(key):
value = line.removeprefix(key).strip()
# exception here
if line.startswith('GLOBAL EFFICIENCY'):
value = line.removeprefix('GLOBAL EFFICIENCY').strip()
return str(f"update network.energy_settings set value = {sql_literal(value)} where key = {sql_literal(key)};")
return str('')
def inp_out_energy(name: str) -> list[str]:
lines = []
objs = read_all(name, "select key, value from network.energy_settings order by key")
for obj in objs:
key = obj['key']
value = obj['value']
if value.strip() != '':
lines.append(f'{key} {value}')
objs = read_all(name, "select pump_id as pump, price, pattern_id as pattern, efficiency_curve_id as effic from network.pump_energy_settings order by pump_id")
for obj in objs:
pump = obj['pump']
if obj['price'] is not None:
lines.append(f"PUMP {pump} PRICE {obj['price']}")
if obj['pattern'] is not None:
lines.append(f"PUMP {pump} PATTERN {obj['pattern']}")
if obj['effic'] is not None:
lines.append(f"PUMP {pump} EFFIC {obj['effic']}")
return lines
def delete_pump_energy_by_pump(name: str, pump: str) -> ChangeSet:
row = try_read(
name,
"select pump_id from network.pump_energy_settings where pump_id = %s",
(pump,),
)
if row is None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': None, 'pattern': None, 'effic': None})
def unset_pump_energy_by_pattern(name: str, pattern: str) -> ChangeSet:
cs = ChangeSet()
rows = read_all(
name,
"select pump_id as pump, price, efficiency_curve_id as effic "
"from network.pump_energy_settings where pattern_id = %s",
(pattern,),
)
for row in rows:
pump = row['pump']
price = float(row['price']) if row['price'] is not None else None
effic = str(row['effic']) if row['effic'] is not None else None
cs.append(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': price, 'pattern': None, 'effic': effic})
return cs
def unset_pump_energy_by_curve(name: str, curve: str) -> ChangeSet:
cs = ChangeSet()
rows = read_all(
name,
"select pump_id as pump, price, pattern_id as pattern "
"from network.pump_energy_settings where efficiency_curve_id = %s",
(curve,),
)
for row in rows:
pump = row['pump']
price = float(row['price']) if row['price'] is not None else None
pattern = str(row['pattern']) if row['pattern'] is not None else None
cs.append(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': price, 'pattern': pattern, 'effic': None})
return cs
+206
View File
@@ -0,0 +1,206 @@
from typing import Any
from psycopg import sql
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
from ..gis.coordinates import sql_delete_coord, sql_insert_coord, sql_update_coord
from .elements import get_all_node_links
def get_junction_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
'x' : {'type': 'float' , 'optional': False , 'readonly': False},
'y' : {'type': 'float' , 'optional': False , 'readonly': False},
'elevation' : {'type': 'float' , 'optional': False , 'readonly': False},
'links' : {'type': 'str_list' , 'optional': False , 'readonly': True } }
def get_junction(name: str, id: str) -> dict[str, Any]:
j = try_read(
name,
"""
SELECT n.id, j.elevation, ST_X(g.geom) AS x, ST_Y(g.geom) AS y,
COALESCE(array_agg(l.id ORDER BY l.id)
FILTER (WHERE l.id IS NOT NULL), '{}') AS links
FROM network.nodes AS n
JOIN network.junctions AS j ON j.node_id = n.id
LEFT JOIN gis.node_geometries AS g ON g.node_id = n.id
LEFT JOIN network.links AS l
ON l.start_node_id = n.id OR l.end_node_id = n.id
WHERE n.id = %s
GROUP BY n.id, j.elevation, g.geom
""",
(id,),
)
if j == None:
return {}
d = {}
d['id'] = str(j['id'])
d['x'] = float(j['x'] or 0.0)
d['y'] = float(j['y'] or 0.0)
d['elevation'] = float(j['elevation'])
d['links'] = list(j['links'])
return d
# DingZQ, 2025-03-29
def get_all_junctions(name: str) -> list[dict[str, Any]]:
rows = read_all(
name,
"""
SELECT id, elevation, x, y
FROM gis.junctions
ORDER BY id
""",
)
if rows == None:
return []
links_by_node = get_all_node_links(name)
result = []
for row in rows:
d = {}
id = str(row['id'])
d['id'] = id
d['x'] = float(row['x'] or 0.0)
d['y'] = float(row['y'] or 0.0)
d['elevation'] = float(row['elevation'])
d['links'] = links_by_node.get(id, [])
result.append(d)
return result
class Junction(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'junction'
self.id = str(input['id'])
self.x = float(input['x'])
self.y = float(input['y'])
self.elevation = float(input['elevation'])
self.f_type = sql_literal(self.type)
self.f_id = sql_literal(self.id)
self.f_elevation = sql_literal(self.elevation)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'elevation': self.elevation }
def _set_junction(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_junction(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
schema = get_junction_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Junction(raw_new)
statement = f"update network.junctions set elevation = {new.f_elevation} where node_id = {new.f_id};"
statement += f"\n{sql_update_coord(new.id, new.x, new.y)}"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_junction(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_junction(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _set_junction(name, cs))
def _add_junction(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Junction(cs.operations[0])
statement = f"insert into network.nodes (id, node_type) values ({new.f_id}, {new.f_type});"
statement += f"\ninsert into network.junctions (node_id, elevation) values ({new.f_id}, {new.f_elevation});"
statement += f"\n{sql_insert_coord(new.id, new.x, new.y)}"
change = g_add_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def add_junction(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_junction(name, cs.operations[0]['id']) != {}:
return ChangeSet()
return execute_command(name, _add_junction(name, cs))
def _delete_junction(name: str, cs: ChangeSet) -> DatabaseCommand:
element_id = str(cs.operations[0]['id'])
f_id = sql_literal(element_id)
statement = sql_delete_coord(element_id)
statement += f"\ndelete from network.nodes where id = {f_id};"
change = g_delete_prefix | {'type': 'junction', 'id': element_id}
return DatabaseCommand(statement, [change])
def delete_junction(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_junction(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _delete_junction(name, cs))
#--------------------------------------------------------------
# [EPA2]
# [IN]
# id elev. (demand) (demand pattern) ;desc
# [OUT]
# id elev. ;desc
#--------------------------------------------------------------
# [EPA3]
# [IN]
# id elev. (demand) (demand pattern)
# [OUT]
# id elev. * * minpressure fullpressure
#--------------------------------------------------------------
def inp_in_junction(line: str, demand_outside: bool) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
id = str(tokens[0])
elevation = float(tokens[1])
demand = float(tokens[2]) if num_without_desc >= 3 and tokens[2] != '*' else None
pattern = str(tokens[3]) if num_without_desc >= 4 and tokens[3] != '*' else None
desc = str(tokens[-1]) if has_desc else None
sql = f"insert into network.nodes (id, node_type) values ({sql_literal(id)}, 'junction');insert into network.junctions (node_id, elevation) values ({sql_literal(id)}, {sql_literal(elevation)});"
if demand != None and demand_outside == False:
sql += f"insert into network.demands (junction_id, sequence_no, base_demand, pattern_id) values ({sql_literal(id)}, 0, {sql_literal(demand)}, {sql_literal(pattern)});"
return str(sql)
def inp_out_junction(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select node_id as id, elevation from network.junctions order by node_id')
for obj in objs:
id = obj['id']
elev = obj['elevation']
desc = ';'
lines.append(f'{id} {elev} {desc}')
return lines
+149
View File
@@ -0,0 +1,149 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
MIXING_MODEL_MIXED = 'MIXED'
MIXING_MODEL_2COMP = '2COMP'
MIXING_MODEL_FIFO = 'FIFO'
MIXING_MODEL_LIFO = 'LIFO'
def get_mixing_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'tank' : {'type': 'str' , 'optional': False , 'readonly': True },
'model' : {'type': 'str' , 'optional': False , 'readonly': False},
'value' : {'type': 'float' , 'optional': True , 'readonly': False} }
def get_mixing(name: str, tank: str) -> dict[str, Any]:
m = try_read(name, "select tank_id as tank, model, value from network.tank_mixing where tank_id = %s", (tank,))
if m == None:
return {}
d = {}
d['tank'] = str(m['tank'])
d['model'] = str(m['model'])
d['value'] = float(m['value']) if m['value'] != None else None
return d
class Mixing(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'mixing'
self.tank = str(input['tank'])
self.model = str(input['model'])
self.value = float(input['value']) if 'value' in input and input['value'] != None else None
self.f_type = sql_literal(self.type)
self.f_tank = sql_literal(self.tank)
self.f_model = sql_literal(self.model)
self.f_value = sql_literal(self.value)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'tank': self.tank, 'model': self.model, 'value': self.value }
def _set_mixing(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_mixing(name, cs.operations[0]['tank'])
new_dict = cs.operations[0]
schema = get_mixing_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Mixing(raw_new)
statement = f"update network.tank_mixing set model = {new.f_model}, value = {new.f_value} where tank_id = {new.f_tank};"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_mixing(name: str, cs: ChangeSet) -> ChangeSet:
if 'tank' not in cs.operations[0]:
return ChangeSet()
if get_mixing(name, cs.operations[0]['tank']) == {}:
return ChangeSet()
return execute_command(name, _set_mixing(name, cs))
def _add_mixing(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Mixing(cs.operations[0])
statement = f"insert into network.tank_mixing (tank_id, model, value) values ({new.f_tank}, {new.f_model}, {new.f_value});"
change = g_add_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def add_mixing(name: str, cs: ChangeSet) -> ChangeSet:
if 'tank' not in cs.operations[0]:
return ChangeSet()
if get_mixing(name, cs.operations[0]['tank']) != {}:
return ChangeSet()
return execute_command(name, _add_mixing(name, cs))
def _delete_mixing(name: str, cs: ChangeSet) -> DatabaseCommand:
tank = str(cs.operations[0]['tank'])
f_tank = sql_literal(tank)
statement = f"delete from network.tank_mixing where tank_id = {f_tank};"
change = g_delete_prefix | {'type': 'mixing', 'tank': tank}
return DatabaseCommand(statement, [change])
def delete_mixing(name: str, cs: ChangeSet) -> ChangeSet:
if 'tank' not in cs.operations[0]:
return ChangeSet()
if get_mixing(name, cs.operations[0]['tank']) == {}:
return ChangeSet()
return execute_command(name, _delete_mixing(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# TankID MixModel FractVolume
# FractVolume if type == MIX2
#--------------------------------------------------------------
def inp_in_mixing(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
tank = str(tokens[0])
model = str(tokens[1].upper())
value = float(tokens[3]) if num_without_desc >= 4 else None
return str(f"insert into network.tank_mixing (tank_id, model, value) values ({sql_literal(tank)}, {sql_literal(model)}, {sql_literal(value)});")
def inp_out_mixing(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select tank_id as tank, model, value from network.tank_mixing order by tank_id')
for obj in objs:
tank = obj['tank']
model = obj['model']
value = obj['value'] if obj['value'] != None else ''
lines.append(f'{tank} {model} {value}')
return lines
def delete_mixing_by_tank(name: str, tank: str) -> ChangeSet:
row = try_read(name, "select 1 from network.tank_mixing where tank_id = %s", (tank,))
if row == None:
return ChangeSet()
return ChangeSet(g_delete_prefix | {'type' : 'mixing', 'tank': tank})
+383
View File
@@ -0,0 +1,383 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
)
#--------------------------------------------------------------
# [EPANET2][IN][OUT]
# UNITS CFS/GPM/MGD/IMGD/AFD/LPS/LPM/MLD/CMH/CMD/SI
# PRESSURE PSI/KPA/M
# HEADLOSS H-W/D-W/C-M
# QUALITY NONE/AGE/TRACE/CHEMICAL (TraceNode)
# UNBALANCED STOP/CONTINUE {Niter}
# PATTERN id
# DEMAND MODEL DDA/PDA
# DEMAND MULTIPLIER value
# EMITTER EXPONENT value
# VISCOSITY value
# DIFFUSIVITY value
# SPECIFIC GRAVITY value
# TRIALS value
# ACCURACY value#
# HEADERROR value
# FLOWCHANGE value
# MINIMUM PRESSURE value
# REQUIRED PRESSURE value
# PRESSURE EXPONENT value#
# TOLERANCE value
# HTOL value
# QTOL value
# RQTOL value
# CHECKFREQ value
# MAXCHECK value
# DAMPLIMIT value
# ---- Unsupported Options -----
# HYDRAULICS USE/SAVE filename
# MAP filename
#--------------------------------------------------------------
element_schema = {'type': 'str' , 'optional': True , 'readonly': False}
OPTION_UNITS_CFS = 'CFS'
OPTION_UNITS_GPM = 'GPM'
OPTION_UNITS_MGD = 'MGD'
OPTION_UNITS_IMGD = 'IMGD'
OPTION_UNITS_AFD = 'AFD'
OPTION_UNITS_LPS = 'LPS'
OPTION_UNITS_LPM = 'LPM'
OPTION_UNITS_MLD = 'MLD'
OPTION_UNITS_CMH = 'CMH'
OPTION_UNITS_CMD = 'CMD'
OPTION_PRESSURE_PSI = 'PSI'
OPTION_PRESSURE_KPA = 'KPA'
OPTION_PRESSURE_METERS = 'METERS'
OPTION_HEADLOSS_HW = 'H-W'
OPTION_HEADLOSS_DW = 'D-W'
OPTION_HEADLOSS_CM = 'C-M'
OPTION_UNBALANCED_STOP = 'STOP'
OPTION_UNBALANCED_CONTINUE = 'CONTINUE'
OPTION_DEMAND_MODEL_DDA = 'DDA'
OPTION_DEMAND_MODEL_PDA = 'PDA'
OPTION_QUALITY_NONE = 'NONE'
OPTION_QUALITY_CHEMICAL = 'CHEMICAL'
OPTION_QUALITY_AGE = 'AGE'
OPTION_QUALITY_TRACE = 'TRACE'
def get_option_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'UNITS' : element_schema,
'PRESSURE' : element_schema,
'HEADLOSS' : element_schema,
'QUALITY' : element_schema,
'UNBALANCED' : element_schema,
'PATTERN' : element_schema,
'DEMAND MODEL' : element_schema,
'DEMAND MULTIPLIER' : element_schema,
'EMITTER EXPONENT' : element_schema,
'VISCOSITY' : element_schema,
'DIFFUSIVITY' : element_schema,
'SPECIFIC GRAVITY' : element_schema,
'TRIALS' : element_schema,
'ACCURACY' : element_schema,
'HEADERROR' : element_schema,
'FLOWCHANGE' : element_schema,
'MINIMUM PRESSURE' : element_schema,
'REQUIRED PRESSURE' : element_schema,
'PRESSURE EXPONENT' : element_schema,
'TOLERANCE' : element_schema,
'HTOL' : element_schema,
'QTOL' : element_schema,
'RQTOL' : element_schema,
'CHECKFREQ' : element_schema,
'MAXCHECK' : element_schema,
'DAMPLIMIT' : element_schema }
def get_option(name: str) -> dict[str, Any]:
ts = read_all(name, "select key, value from network.simulation_settings where engine_version = 'legacy'")
d = {}
for e in ts:
d[e['key']] = str(e['value'])
return d
def _set_option(name: str, cs: ChangeSet) -> DatabaseCommand:
new = {}
new_dict = cs.operations[0]
schema = get_option_schema(name)
for key in schema.keys():
if key in new_dict:
new[key] = str(new_dict[key])
change = g_update_prefix | { 'type' : 'option' }
statement = ''
for key, value in new.items():
if statement != '':
statement += '\n'
statement += f"update network.simulation_settings set value = {sql_literal(value)} where engine_version = 'legacy' and key = {sql_literal(key)};"
change |= { key: value }
return DatabaseCommand(statement, [change])
def set_option(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_option(name, cs))
OPTION_V3_FLOW_UNITS_CFS = OPTION_UNITS_CFS
OPTION_V3_FLOW_UNITS_GPM = OPTION_UNITS_GPM
OPTION_V3_FLOW_UNITS_MGD = OPTION_UNITS_MGD
OPTION_V3_FLOW_UNITS_IMGD = OPTION_UNITS_IMGD
OPTION_V3_FLOW_UNITS_AFD = OPTION_UNITS_AFD
OPTION_V3_FLOW_UNITS_LPS = OPTION_UNITS_LPS
OPTION_V3_FLOW_UNITS_LPM = OPTION_UNITS_LPM
OPTION_V3_FLOW_UNITS_MLD = OPTION_UNITS_MLD
OPTION_V3_FLOW_UNITS_CMH = OPTION_UNITS_CMH
OPTION_V3_FLOW_UNITS_CMD = OPTION_UNITS_CMD
OPTION_V3_PRESSURE_UNITS_PSI = OPTION_PRESSURE_PSI
OPTION_V3_PRESSURE_UNITS_KPA = OPTION_PRESSURE_KPA
OPTION_V3_PRESSURE_UNITS_METERS = OPTION_PRESSURE_METERS
OPTION_V3_HEADLOSS_MODEL_HW = OPTION_HEADLOSS_HW
OPTION_V3_HEADLOSS_MODEL_DW = OPTION_HEADLOSS_DW
OPTION_V3_HEADLOSS_MODEL_CM = OPTION_HEADLOSS_CM
OPTION_V3_STEP_SIZING_FULL = 'FULL'
OPTION_V3_STEP_SIZING_RELAXATION = 'RELAXATION'
OPTION_V3_STEP_SIZING_LINESEARCH = 'LINESEARCH'
OPTION_V3_IF_UNBALANCED_STOP = OPTION_UNBALANCED_STOP
OPTION_V3_IF_UNBALANCED_CONTINUE = OPTION_UNBALANCED_CONTINUE
OPTION_V3_DEMAND_MODEL_FIXED = 'FIXED'
OPTION_V3_DEMAND_MODEL_CONSTRAINED = 'CONSTRAINED'
OPTION_V3_DEMAND_MODEL_POWER = 'POWER'
OPTION_V3_DEMAND_MODEL_LOGISTIC = 'LOGISTIC'
OPTION_V3_LEAKAGE_MODEL_NONE = 'NONE'
OPTION_V3_LEAKAGE_MODEL_POWER = 'POWER'
OPTION_V3_LEAKAGE_MODEL_FAVAD = 'FAVAD'
OPTION_V3_QUALITY_MODEL_NONE = OPTION_QUALITY_NONE
OPTION_V3_QUALITY_MODEL_CHEMICAL = OPTION_QUALITY_CHEMICAL
OPTION_V3_QUALITY_MODEL_AGE = OPTION_QUALITY_AGE
OPTION_V3_QUALITY_MODEL_TRACE = OPTION_QUALITY_TRACE
OPTION_V3_QUALITY_UNITS_HRS = 'HRS'
OPTION_V3_QUALITY_UNITS_PCNT = 'PCNT'
OPTION_V3_QUALITY_UNITS_MGL = 'MG/L'
OPTION_V3_QUALITY_UNITS_UGL = 'UG/L'
def get_option_v3_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'FLOW_UNITS' : element_schema,
'PRESSURE_UNITS' : element_schema,
'HEADLOSS_MODEL' : element_schema,
'SPECIFIC_GRAVITY' : element_schema,
'SPECIFIC_VISCOSITY' : element_schema,
'MAXIMUM_TRIALS' : element_schema,
'HEAD_TOLERANCE' : element_schema,
'FLOW_TOLERANCE' : element_schema,
'FLOW_CHANGE_LIMIT' : element_schema,
'RELATIVE_ACCURACY' : element_schema,
'TIME_WEIGHT' : element_schema,
'STEP_SIZING' : element_schema,
'IF_UNBALANCED' : element_schema,
'DEMAND_MODEL' : element_schema,
'DEMAND_PATTERN' : element_schema,
'DEMAND_MULTIPLIER' : element_schema,
'MINIMUM_PRESSURE' : element_schema,
'SERVICE_PRESSURE' : element_schema,
'PRESSURE_EXPONENT' : element_schema,
'LEAKAGE_MODEL' : element_schema,
'LEAKAGE_COEFF1' : element_schema,
'LEAKAGE_COEFF2' : element_schema,
'EMITTER_EXPONENT' : element_schema,
'QUALITY_MODEL' : element_schema,
'QUALITY_NAME' : element_schema,
'QUALITY_UNITS' : element_schema,
'TRACE_NODE' : element_schema,
'SPECIFIC_DIFFUSIVITY' : element_schema,
'QUALITY_TOLERANCE' : element_schema }
def get_option_v3(name: str) -> dict[str, Any]:
ts = read_all(name, "select key, value from network.simulation_settings where engine_version = 'v3'")
d = {}
for e in ts:
d[e['key']] = str(e['value'])
return d
def _set_option_v3(name: str, cs: ChangeSet) -> DatabaseCommand:
new = {}
new_dict = cs.operations[0]
schema = get_option_v3_schema(name)
for key in schema.keys():
if key in new_dict:
new[key] = str(new_dict[key])
change = g_update_prefix | { 'type' : 'option_v3' }
statement = ''
for key, value in new.items():
if statement != '':
statement += '\n'
statement += f"update network.simulation_settings set value = {sql_literal(value)} where engine_version = 'v3' and key = {sql_literal(key)};"
change |= { key: value }
return DatabaseCommand(statement, [change])
def set_option_v3(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_option_v3(name, cs))
_key_map_23 = {
'UNITS' : 'FLOW_UNITS',
'PRESSURE' : 'PRESSURE_UNITS',
'HEADLOSS' : 'HEADLOSS_MODEL',
'QUALITY' : 'QUALITY_MODEL',
'UNBALANCED' : 'IF_UNBALANCED',
'PATTERN' : 'DEMAND_PATTERN',
'DEMAND MODEL' : 'DEMAND_MODEL',
'DEMAND MULTIPLIER' : 'DEMAND_MULTIPLIER',
'EMITTER EXPONENT' : 'EMITTER_EXPONENT',
'VISCOSITY' : 'SPECIFIC_VISCOSITY',
'DIFFUSIVITY' : 'SPECIFIC_DIFFUSIVITY',
'SPECIFIC GRAVITY' : 'SPECIFIC_GRAVITY',
'TRIALS' : 'MAXIMUM_TRIALS',
'ACCURACY' : 'RELATIVE_ACCURACY',
#'HEADERROR' : '',
'FLOWCHANGE' : 'FLOW_CHANGE_LIMIT',
'MINIMUM PRESSURE' : 'MINIMUM_PRESSURE',
'REQUIRED PRESSURE' : 'SERVICE_PRESSURE',
'PRESSURE EXPONENT' : 'PRESSURE_EXPONENT',
'TOLERANCE' : 'QUALITY_TOLERANCE',
'HTOL' : 'HEAD_TOLERANCE',
'QTOL' : 'FLOW_TOLERANCE',
#'RQTOL' : '',
#'CHECKFREQ' : '',
#'MAXCHECK' : '',
#'DAMPLIMIT' : '',
}
_key_map_32 = {
'FLOW_UNITS' : 'UNITS',
'PRESSURE_UNITS' : 'PRESSURE',
'HEADLOSS_MODEL' : 'HEADLOSS',
'SPECIFIC_GRAVITY' : 'SPECIFIC GRAVITY',
'SPECIFIC_VISCOSITY' : 'VISCOSITY',
'MAXIMUM_TRIALS' : 'TRIALS',
'HEAD_TOLERANCE' : 'HTOL',
'FLOW_TOLERANCE' : 'QTOL',
'FLOW_CHANGE_LIMIT' : 'FLOWCHANGE',
'RELATIVE_ACCURACY' : 'ACCURACY',
#'TIME_WEIGHT' : '',
#'STEP_SIZING' : '',
'IF_UNBALANCED' : 'UNBALANCED',
'DEMAND_MODEL' : 'DEMAND MODEL',
'DEMAND_PATTERN' : 'PATTERN',
'DEMAND_MULTIPLIER' : 'DEMAND MULTIPLIER',
'MINIMUM_PRESSURE' : 'MINIMUM PRESSURE',
'SERVICE_PRESSURE' : 'REQUIRED PRESSURE',
'PRESSURE_EXPONENT' : 'PRESSURE EXPONENT',
#'LEAKAGE_MODEL' : '',
#'LEAKAGE_COEFF1' : '',
#'LEAKAGE_COEFF2' : '',
'EMITTER_EXPONENT' : 'EMITTER EXPONENT',
'QUALITY_MODEL' : 'QUALITY',
#'QUALITY_NAME' : '',
#'QUALITY_UNITS' : '',
#'TRACE_NODE' : '',
'SPECIFIC_DIFFUSIVITY' : 'DIFFUSIVITY',
'QUALITY_TOLERANCE' : 'TOLERANCE'
}
def generate_v2(cs: ChangeSet) -> ChangeSet:
op = cs.operations[0]
if op['type'] == 'option':
return cs
map = _key_map_32
cs_v2 = {}
for key in op:
if key == 'operation' or key == 'type':
continue
if key in map.keys():
if key != 'QUALITY_MODEL' and key != 'DEMAND_MODEL':
cs_v2 |= { map[key] : op[key] }
elif key == 'QUALITY_MODEL':
if str(op[key]).upper() == OPTION_QUALITY_TRACE and 'TRACE_NODE' in op.keys():
cs_v2 |= { map[key] : f"{OPTION_QUALITY_TRACE} {op['TRACE_NODE']}" }
else:
cs_v2 |= { map[key] : str(op[key]).upper() }
elif key == 'DEMAND_MODEL':
if op[key] == OPTION_V3_DEMAND_MODEL_FIXED:
cs_v2 |= { map[key] : OPTION_DEMAND_MODEL_DDA }
else:
cs_v2 |= { map[key] : OPTION_DEMAND_MODEL_PDA }
if len(cs_v2) > 0:
cs_v2 |= g_update_prefix | { 'type' : 'option' }
return ChangeSet(cs_v2)
return ChangeSet()
def generate_v3(cs: ChangeSet) -> ChangeSet:
op = cs.operations[0]
if op['type'] == 'option_v3':
return cs
map = _key_map_23
cs_v3 = {}
for key in op:
if key == 'operation' or key == 'type':
continue
if key in map.keys():
if key != 'QUALITY' and key != 'DEMAND MODEL':
cs_v3 |= { map[key] : op[key] }
elif key == 'QUALITY':
tokens = str(op[key]).split()
if len(tokens) >= 1:
cs_v3 |= { map[key] : tokens[0].upper() }
if tokens[0].upper() == OPTION_QUALITY_TRACE and len(tokens) >= 2:
cs_v3 |= { 'TRACE_NODE' : tokens[1] }
elif key == 'DEMAND MODEL':
if op[key] == OPTION_DEMAND_MODEL_DDA:
cs_v3 |= { map[key] : OPTION_V3_DEMAND_MODEL_FIXED }
else:
cs_v3 |= { map[key] : OPTION_V3_DEMAND_MODEL_POWER }
if len(cs_v3) > 0:
cs_v3 |= g_update_prefix | { 'type' : 'option_v3' }
return ChangeSet(cs_v3)
return ChangeSet()
+83
View File
@@ -0,0 +1,83 @@
from psycopg import sql
from ..core.database import ChangeSet, g_update_prefix, read_all, sql_literal
from .options import get_option_schema, generate_v3
def _inp_in_option(section: list[str]) -> ChangeSet:
if len(section) <= 0:
return ChangeSet()
cs = g_update_prefix | { 'type' : 'option' }
for s in section:
if s.startswith(';'):
continue
tokens = s.strip().split()
if tokens[0].upper() == 'PATTERN': # can not upper id
value = tokens[1] if len(tokens) > 1 else ''
cs |= { 'PATTERN' : value }
elif tokens[0].upper() == 'QUALITY': # can not upper trace node
value = tokens[1] if len(tokens) > 1 else ''
if len(tokens) > 2:
value += f' {tokens[2]}'
cs |= { 'QUALITY' : value }
else:
line = s.upper().strip()
for key in get_option_schema('').keys():
if line.startswith(key):
value = line.removeprefix(key).strip()
cs |= { key : value }
result = ChangeSet(cs)
result.merge(generate_v3(result))
return result
def inp_in_option(section: list[str]) -> str:
sql = ''
result = _inp_in_option(section)
for op in result.operations:
for key in op.keys():
if key == 'operation' or key == 'type':
continue
if op['type'] == 'option':
sql += f"update network.simulation_settings set value = {sql_literal(op[key])} where engine_version = 'legacy' and key = {sql_literal(key)};"
else:
sql += f"update network.simulation_settings set value = {sql_literal(op[key])} where engine_version = 'v3' and key = {sql_literal(key)};"
return sql
def inp_out_option(name: str) -> list[str]:
lines = []
objs = read_all(name, "select key, value from network.simulation_settings where engine_version = 'legacy' order by key")
is_dda = False
for obj in objs:
if obj['key'] == 'DEMAND MODEL':
is_dda = obj['value'] == 'DDA'
dda_ignore = [
'HEADERROR', # TODO: default is 0 which is conflict with PDA
'FLOWCHANGE', # TODO: default is 0 which is conflict with PDA
'MINIMUM PRESSURE',
'REQUIRED PRESSURE',
'PRESSURE EXPONENT'
]
for obj in objs:
key = obj['key']
# why write this ?
if key == 'PRESSURE':
continue
# release version does not support new keys and has error message
if key == 'HTOL' or key == 'QTOL' or key == 'RQTOL':
continue
# ignore some weird settings for DDA
if is_dda and key in dda_ignore:
continue
value = obj['value']
if str(value).strip() != '':
lines.append(f'{key} {value}')
return lines
+81
View File
@@ -0,0 +1,81 @@
from psycopg import sql
from ..core.database import ChangeSet, g_update_prefix, read_all, sql_literal
from .options import get_option_schema, get_option_v3_schema, generate_v2, generate_v3
def _parse_v2(v2_lines: list[str]) -> dict[str, str]:
cs_v2 = g_update_prefix | { 'type' : 'option' }
for s in v2_lines:
tokens = s.split()
if tokens[0].upper() == 'PATTERN': # can not upper id
value = tokens[1] if len(tokens) > 1 else ''
cs_v2 |= { 'PATTERN' : value }
elif tokens[0].upper() == 'QUALITY': # can not upper trace node
value = tokens[1]
if len(tokens) > 2:
value += f' {tokens[2]}'
cs_v2 |= { 'QUALITY' : value }
else:
line = s.upper().strip()
for key in get_option_schema('').keys():
if line.startswith(key):
value = line.removeprefix(key).strip()
cs_v2 |= { key : value }
return cs_v2
def _inp_in_option_v3(section: list[str]) -> ChangeSet:
if len(section) <= 0:
return ChangeSet()
cs_v3 = g_update_prefix | { 'type' : 'option_v3' }
v2_lines = []
for s in section:
if s.startswith(';'):
continue
tokens = s.strip().split()
key = tokens[0]
if key in get_option_v3_schema('').keys():
value = ''
if len(tokens) == 2:
value = tokens[1]
elif len(tokens) > 2:
value = ' '.join(tokens[1:])
cs_v3 |= { key : value }
else:
v2_lines.append(s.strip())
# unlikely...
cs_v2 = _parse_v2(v2_lines)
result = ChangeSet(cs_v3)
result.merge(generate_v3(ChangeSet(cs_v2)))
result.merge(generate_v2(result))
return result
def inp_in_option_v3(section: list[str]) -> str:
sql = ''
result = _inp_in_option_v3(section)
for op in result.operations:
for key in op.keys():
if key == 'operation' or key == 'type':
continue
if op['type'] == 'option_v3':
sql += f"update network.simulation_settings set value = {sql_literal(op[key])} where engine_version = 'v3' and key = {sql_literal(key)};"
else:
sql += f"update network.simulation_settings set value = {sql_literal(op[key])} where engine_version = 'legacy' and key = {sql_literal(key)};"
return sql
def inp_out_option_v3(name: str) -> list[str]:
lines = []
objs = read_all(name, "select key, value from network.simulation_settings where engine_version = 'v3' order by key")
for obj in objs:
key = obj['key']
value = obj['value']
if str(value).strip() != '':
lines.append(f'{key} {value}')
return lines
+162
View File
@@ -0,0 +1,162 @@
from typing import Any
from psycopg import sql
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
PATTERN_V3_TYPE_FIXED = 'FIXED'
PATTERN_V3_TYPE_VARIABLE = 'VARIABLE'
pattern_v3_types = [PATTERN_V3_TYPE_FIXED, PATTERN_V3_TYPE_VARIABLE]
def get_pattern_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
'factors' : {'type': 'float_list' , 'optional': False , 'readonly': False } }
def get_pattern(name: str, id: str) -> dict[str, Any]:
p_one = try_read(name, "select id from network.patterns where id = %s", (id,))
if p_one == None:
return {}
pas = read_all(name, "select factor from network.pattern_values where pattern_id = %s order by sequence_no", (id,))
ps = []
for r in pas:
ps.append(float(r['factor']))
return { 'id': id, 'factors': ps }
def _set_pattern(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
f_id = sql_literal(id)
old = get_pattern(name, id)
new = { 'id': id }
if 'factors' in cs.operations[0]:
new['factors'] = cs.operations[0]['factors']
else:
new['factors'] = old['factors']
statement = f"delete from network.pattern_values where pattern_id = {f_id};"
for sequence_no, factor in enumerate(new['factors']):
f_factor = sql_literal(factor)
statement += f"\ninsert into network.pattern_values (pattern_id, sequence_no, factor) values ({f_id}, {sequence_no}, {f_factor});"
change = g_update_prefix | { 'type': 'pattern' } | new
return DatabaseCommand(statement, [change])
def set_pattern(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_pattern(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _set_pattern(name, cs))
def _add_pattern(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
f_id = sql_literal(id)
new = { 'id': id, 'factors': cs.operations[0]['factors'] }
statement = f"insert into network.patterns (id) values ({f_id});"
for sequence_no, factor in enumerate(new['factors']):
f_factor = sql_literal(factor)
statement += f"\ninsert into network.pattern_values (pattern_id, sequence_no, factor) values ({f_id}, {sequence_no}, {f_factor});"
change = g_add_prefix | { 'type': 'pattern' } | new
return DatabaseCommand(statement, [change])
def add_pattern(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_pattern(name, cs.operations[0]['id']) != {}:
return ChangeSet()
return execute_command(name, _add_pattern(name, cs))
def _delete_pattern(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
f_id = sql_literal(id)
statement = f"delete from network.patterns where id = {f_id};"
change = g_delete_prefix | { 'type': 'pattern' } | { 'id': id }
return DatabaseCommand(statement, [change])
def delete_pattern(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_pattern(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _delete_pattern(name, cs))
#--------------------------------------------------------------
# [EPA2][IN][OUT]
# ;desc
# id mult1 mult2 .....
#--------------------------------------------------------------
#--------------------------------------------------------------
# [EPA3][IN][OUT]
# id FIXED (interval)
# id factor1 factor2 ...
# id VARIABLE
# id time1 factor1 time2 factor2 ...
#--------------------------------------------------------------
def inp_in_pattern(line: str, fixed: bool = True) -> str:
tokens = line.split()
sql = ''
pattern_id = sql_literal(tokens[0])
if fixed:
for token in tokens[1:]:
factor = sql_literal(float(token))
sql += f"insert into network.pattern_values (pattern_id, sequence_no, factor) values ({pattern_id}, (select coalesce(max(sequence_no) + 1, 0) from network.pattern_values where pattern_id = {pattern_id}), {factor});"
else:
for token in tokens[1::2]:
factor = sql_literal(float(token))
sql += f"insert into network.pattern_values (pattern_id, sequence_no, factor) values ({pattern_id}, (select coalesce(max(sequence_no) + 1, 0) from network.pattern_values where pattern_id = {pattern_id}), {factor});"
return sql
def inp_out_pattern(name: str) -> list[str]:
lines = []
objs = read_all(name, "select pattern_id as id, factor from network.pattern_values order by pattern_id, sequence_no")
for obj in objs:
id = obj['id']
factor = obj['factor']
lines.append(f'{id} {factor}')
return lines
def inp_out_pattern_v3(name: str) -> list[str]:
lines = []
objs = read_all(name, "select pattern_id as id, factor from network.pattern_values order by pattern_id, sequence_no")
ids = []
for obj in objs:
id = obj['id']
if id not in ids:
# for EPA3, ignore time of variable pattern...
lines.append(f'{id} FIXED')
ids.append(id)
factor = obj['factor']
lines.append(f'{id} {factor}')
return lines
+254
View File
@@ -0,0 +1,254 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
PIPE_STATUS_OPEN = 'OPEN'
PIPE_STATUS_CLOSED = 'CLOSED'
PIPE_STATUS_CV = 'CV'
def get_pipe_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
'node1' : {'type': 'str' , 'optional': False , 'readonly': False},
'node2' : {'type': 'str' , 'optional': False , 'readonly': False},
'length' : {'type': 'float' , 'optional': False , 'readonly': False},
'diameter' : {'type': 'float' , 'optional': False , 'readonly': False},
'roughness' : {'type': 'float' , 'optional': False , 'readonly': False},
'minor_loss' : {'type': 'float' , 'optional': False , 'readonly': False},
'status' : {'type': 'str' , 'optional': False , 'readonly': False} }
def get_pipe(name: str, id: str) -> dict[str, Any]:
p = try_read(name, "select l.id, l.start_node_id as node1, l.end_node_id as node2, p.length, p.diameter, p.roughness, p.minor_loss, p.status from network.links l join network.pipes p on p.link_id = l.id where l.id = %s", (id,))
if p == None:
return {}
d = {}
d['id'] = str(p['id'])
d['node1'] = str(p['node1'])
d['node2'] = str(p['node2'])
d['length'] = float(p['length'])
d['diameter'] = float(p['diameter'])
d['roughness'] = float(p['roughness'])
d['minor_loss'] = float(p['minor_loss'])
d['status'] = str(p['status'])
return d
# DingZQ, 2025-03-29
def get_all_pipes(name: str) -> list[dict[str, Any]]:
rows = read_all(
name,
"SELECT id, start_node_id AS node1, end_node_id AS node2, length, "
"diameter, roughness, minor_loss, status FROM gis.pipes ORDER BY id",
)
if rows == None:
return []
result = []
for row in rows:
d = {}
d['id'] = str(row['id'])
d['node1'] = str(row['node1'])
d['node2'] = str(row['node2'])
d['length'] = float(row['length'])
d['diameter'] = float(row['diameter'])
d['roughness'] = float(row['roughness'])
d['minor_loss'] = float(row['minor_loss'])
d['status'] = str(row['status'])
result.append(d)
return result
def get_pipes_by_property(
name: str,
fields: list[str] | None = None,
property_conditions: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
if not fields:
fields = [
'id',
'node1',
'node2',
'length',
'diameter',
'roughness',
'minor_loss',
'status',
]
rows = read_all(
name,
"SELECT id, start_node_id AS node1, end_node_id AS node2, length, "
"diameter, roughness, minor_loss, status FROM gis.pipes ORDER BY id",
)
if rows == None:
return []
result = []
for row in rows:
if property_conditions:
matched = True
for key, value in property_conditions.items():
if row[key] != value:
matched = False
break
if not matched:
continue
d = {}
for field in fields:
value = row[field]
if field in ('length', 'diameter', 'roughness', 'minor_loss') and value is not None:
d[field] = float(value)
elif field in ('id', 'node1', 'node2', 'status') and value is not None:
d[field] = str(value)
else:
d[field] = value
result.append(d)
return result
class Pipe(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'pipe'
self.id = str(input['id'])
self.node1 = str(input['node1'])
self.node2 = str(input['node2'])
self.length = float(input['length'])
self.diameter = float(input['diameter'])
self.roughness = float(input['roughness'])
self.minor_loss = float(input['minor_loss'])
self.status = str(input['status'])
self.f_type = sql_literal(self.type)
self.f_id = sql_literal(self.id)
self.f_node1 = sql_literal(self.node1)
self.f_node2 = sql_literal(self.node2)
self.f_length = sql_literal(self.length)
self.f_diameter = sql_literal(self.diameter)
self.f_roughness = sql_literal(self.roughness)
self.f_minor_loss = sql_literal(self.minor_loss)
self.f_status = sql_literal(self.status)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'node1': self.node1, 'node2': self.node2, 'length': self.length, 'diameter': self.diameter, 'roughness': self.roughness, 'minor_loss': self.minor_loss, 'status': self.status }
def _set_pipe(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_pipe(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
schema = get_pipe_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Pipe(raw_new)
statement = f"update network.links set start_node_id = {new.f_node1}, end_node_id = {new.f_node2} where id = {new.f_id};"
statement += f"\nupdate network.pipes set length = {new.f_length}, diameter = {new.f_diameter}, roughness = {new.f_roughness}, minor_loss = {new.f_minor_loss}, status = {new.f_status} where link_id = {new.f_id};"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_pipe(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_pipe(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _set_pipe(name, cs))
def _add_pipe(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Pipe(cs.operations[0])
statement = f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({new.f_id}, {new.f_type}, {new.f_node1}, {new.f_node2});"
statement += f"\ninsert into network.pipes (link_id, length, diameter, roughness, minor_loss, status) values ({new.f_id}, {new.f_length}, {new.f_diameter}, {new.f_roughness}, {new.f_minor_loss}, {new.f_status});"
change = g_add_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def add_pipe(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_pipe(name, cs.operations[0]['id']) != {}:
return ChangeSet()
return execute_command(name, _add_pipe(name, cs))
def _delete_pipe(name: str, cs: ChangeSet) -> DatabaseCommand:
element_id = str(cs.operations[0]['id'])
f_id = sql_literal(element_id)
statement = f"delete from network.links where id = {f_id};"
change = g_delete_prefix | {'type': 'pipe', 'id': element_id}
return DatabaseCommand(statement, [change])
def delete_pipe(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_pipe(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _delete_pipe(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3]
# [IN]
# id node1 node2 length diam rcoeff (lcoeff status) ;desc
# [OUT]
# id node1 node2 length diam rcoeff lcoeff (status) ;desc
#--------------------------------------------------------------
def inp_in_pipe(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
id = str(tokens[0])
node1 = str(tokens[1])
node2 = str(tokens[2])
length = float(tokens[3])
diameter = float(tokens[4])
roughness = float(tokens[5])
minor_loss = float(tokens[6])
# status is must-have, here fix input
status = str(tokens[7].upper()) if num_without_desc >= 8 else PIPE_STATUS_OPEN
desc = str(tokens[-1]) if has_desc else None
return str(f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({sql_literal(id)}, 'pipe', {sql_literal(node1)}, {sql_literal(node2)});insert into network.pipes (link_id, length, diameter, roughness, minor_loss, status) values ({sql_literal(id)}, {sql_literal(length)}, {sql_literal(diameter)}, {sql_literal(roughness)}, {sql_literal(minor_loss)}, {sql_literal(status)});")
def inp_out_pipe(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select l.id, l.start_node_id as node1, l.end_node_id as node2, p.length, p.diameter, p.roughness, p.minor_loss, p.status from network.links l join network.pipes p on p.link_id = l.id order by l.id')
for obj in objs:
id = obj['id']
node1 = obj['node1']
node2 = obj['node2']
length = obj['length']
diameter = obj['diameter']
roughness = obj['roughness']
minor_loss = obj['minor_loss']
status = obj['status']
desc = ';'
lines.append(f'{id} {node1} {node2} {length} {diameter} {roughness} {minor_loss} {status} {desc}')
return lines
+218
View File
@@ -0,0 +1,218 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
def get_pump_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
'node1' : {'type': 'str' , 'optional': False , 'readonly': False},
'node2' : {'type': 'str' , 'optional': False , 'readonly': False},
'power' : {'type': 'float' , 'optional': True , 'readonly': False},
'head' : {'type': 'str' , 'optional': True , 'readonly': False},
'speed' : {'type': 'float' , 'optional': True , 'readonly': False},
'pattern' : {'type': 'str' , 'optional': True , 'readonly': False} }
def get_pump(name: str, id: str) -> dict[str, Any]:
p = try_read(name, "select l.id, l.start_node_id as node1, l.end_node_id as node2, p.power, p.head_curve_id as head, p.speed, p.pattern_id as pattern from network.links l join network.pumps p on p.link_id = l.id where l.id = %s", (id,))
if p == None:
return {}
d = {}
d['id'] = str(p['id'])
d['node1'] = str(p['node1'])
d['node2'] = str(p['node2'])
d['power'] = float(p['power']) if p['power'] != None else None
d['head'] = str(p['head']) if p['head'] != None else None
d['speed'] = float(p['speed']) if p['speed'] != None else None
d['pattern'] = str(p['pattern']) if p['pattern'] != None else None
return d
# DingZQ, 2025-03-29
def get_all_pumps(name: str) -> list[dict[str, Any]]:
rows = read_all(
name,
"SELECT id, start_node_id AS node1, end_node_id AS node2, power, "
"head_curve_id AS head, speed, pattern_id AS pattern "
"FROM gis.pumps ORDER BY id",
)
if rows == None:
return []
result = []
for row in rows:
d = {}
d['id'] = str(row['id'])
d['node1'] = str(row['node1'])
d['node2'] = str(row['node2'])
d['power'] = float(row['power']) if row['power'] != None else None
d['head'] = str(row['head']) if row['head'] != None else None
d['speed'] = float(row['speed']) if row['speed'] != None else None
d['pattern'] = str(row['pattern']) if row['pattern'] != None else None
result.append(d)
return result
class Pump(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'pump'
self.id = str(input['id'])
self.node1 = str(input['node1'])
self.node2 = str(input['node2'])
self.power = float(input['power']) if 'power' in input and input['power'] != None else None
self.head = str(input['head']) if 'head' in input and input['head'] != None else None
self.speed = float(input['speed']) if 'speed' in input and input['speed'] != None else None
self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None
self.f_type = sql_literal(self.type)
self.f_id = sql_literal(self.id)
self.f_node1 = sql_literal(self.node1)
self.f_node2 = sql_literal(self.node2)
self.f_power = sql_literal(self.power)
self.f_head = sql_literal(self.head)
self.f_speed = sql_literal(self.speed)
self.f_pattern = sql_literal(self.pattern)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'node1': self.node1, 'node2': self.node2, 'power': self.power, 'head': self.head, 'speed': self.speed, 'pattern': self.pattern }
def _set_pump(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_pump(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
schema = get_pump_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Pump(raw_new)
statement = f"update network.links set start_node_id = {new.f_node1}, end_node_id = {new.f_node2} where id = {new.f_id};"
statement += f"\nupdate network.pumps set power = {new.f_power}, head_curve_id = {new.f_head}, speed = {new.f_speed}, pattern_id = {new.f_pattern} where link_id = {new.f_id};"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_pump(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_pump(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _set_pump(name, cs))
def _add_pump(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Pump(cs.operations[0])
statement = f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({new.f_id}, {new.f_type}, {new.f_node1}, {new.f_node2});"
statement += f"\ninsert into network.pumps (link_id, power, head_curve_id, speed, pattern_id) values ({new.f_id}, {new.f_power}, {new.f_head}, {new.f_speed}, {new.f_pattern});"
change = g_add_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def add_pump(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_pump(name, cs.operations[0]['id']) != {}:
return ChangeSet()
return execute_command(name, _add_pump(name, cs))
def _delete_pump(name: str, cs: ChangeSet) -> DatabaseCommand:
element_id = str(cs.operations[0]['id'])
f_id = sql_literal(element_id)
statement = f"delete from network.links where id = {f_id};"
change = g_delete_prefix | {'type': 'pump', 'id': element_id}
return DatabaseCommand(statement, [change])
def delete_pump(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_pump(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _delete_pump(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# id node1 node2 KEYWORD value {KEYWORD value ...} ;desc
# where KEYWORD = [POWER,HEAD,PATTERN,SPEED]
#--------------------------------------------------------------
def inp_in_pump(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
id = str(tokens[0])
node1 = str(tokens[1])
node2 = str(tokens[2])
props = {}
for i in range(3, num_without_desc, 2):
props |= { tokens[i].lower(): tokens[i + 1] }
power = float(props['power']) if 'power' in props else None
head = str(props['head']) if 'head' in props else None
speed = float(props['speed']) if 'speed' in props else None
pattern = str(props['pattern']) if 'pattern' in props else None
desc = str(tokens[-1]) if has_desc else None
return str(f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({sql_literal(id)}, 'pump', {sql_literal(node1)}, {sql_literal(node2)});insert into network.pumps (link_id, power, head_curve_id, speed, pattern_id) values ({sql_literal(id)}, {sql_literal(power)}, {sql_literal(head)}, {sql_literal(speed)}, {sql_literal(pattern)});")
def inp_out_pump(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select l.id, l.start_node_id as node1, l.end_node_id as node2, p.power, p.head_curve_id as head, p.speed, p.pattern_id as pattern from network.links l join network.pumps p on p.link_id = l.id order by l.id')
for obj in objs:
id = obj['id']
node1 = obj['node1']
node2 = obj['node2']
power = f"POWER {obj['power']}" if obj['power'] != None else ''
head = f"HEAD {obj['head']}" if obj['head'] != None else ''
speed = f"SPEED {obj['speed']}" if obj['speed'] != None else ''
pattern = f"PATTERN {obj['pattern']}" if obj['pattern'] != None else ''
desc = ';'
lines.append(f'{id} {node1} {node2} {power} {head} {speed} {pattern} {desc}')
return lines
def unset_pump_by_curve(name: str, curve: str) -> ChangeSet:
cs = ChangeSet()
rows = read_all(name, "select link_id as id, power from network.pumps where head_curve_id = %s", (curve,))
for row in rows:
if row['power'] != None:
cs.append(g_update_prefix | {'type': 'pump', 'id': row['id'], 'head': None})
else: # workaround to prevent pump deletion... and I don't want to remove constraint...
cs.append(g_update_prefix | {'type': 'pump', 'id': row['id'], 'head': None, 'power': 0.0})
return cs
def unset_pump_by_pattern(name: str, pattern: str) -> ChangeSet:
cs = ChangeSet()
rows = read_all(name, "select link_id as id from network.pumps where pattern_id = %s", (pattern,))
for row in rows:
cs.append(g_update_prefix | {'type': 'pump', 'id': row['id'], 'pattern': None})
return cs
+99
View File
@@ -0,0 +1,99 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
def get_quality_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'node' : {'type': 'str' , 'optional': False , 'readonly': True },
'quality' : {'type': 'float' , 'optional': True , 'readonly': False} }
def get_quality(name: str, node: str) -> dict[str, Any]:
e = try_read(name, "select node_id as node, value as quality from network.initial_quality where node_id = %s", (node,))
if e == None:
return { 'node': node, 'quality': None }
d = {}
d['node'] = str(e['node'])
d['quality'] = float(e['quality']) if e['quality'] != None else None
return d
class Quality(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'quality'
self.node = str(input['node'])
self.quality = float(input['quality']) if 'quality' in input and input['quality'] != None else None
self.f_type = sql_literal(self.type)
self.f_node = sql_literal(self.node)
self.f_quality = sql_literal(self.quality)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'node': self.node, 'quality': self.quality }
def _set_quality(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_quality(name, cs.operations[0]['node'])
new_dict = cs.operations[0]
schema = get_quality_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Quality(raw_new)
statement = f"delete from network.initial_quality where node_id = {new.f_node};"
if new.quality != None:
statement += f"\ninsert into network.initial_quality (node_id, value) values ({new.f_node}, {new.f_quality});"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_quality(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_quality(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# node initqual
#--------------------------------------------------------------
def inp_in_quality(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
node = str(tokens[0])
quality = float(tokens[1])
return str(f"insert into network.initial_quality (node_id, value) values ({sql_literal(node)}, {sql_literal(quality)});")
def inp_out_quality(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select node_id as node, value as quality from network.initial_quality order by node_id')
for obj in objs:
node = obj['node']
quality = obj['quality']
lines.append(f'{node} {quality}')
return lines
def delete_quality_by_node(name: str, node: str) -> ChangeSet:
row = try_read(name, "select 1 from network.initial_quality where node_id = %s", (node,))
if row == None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type' : 'quality', 'node': node, 'quality': None})
+243
View File
@@ -0,0 +1,243 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
element_schema = {'type': 'str' , 'optional': True , 'readonly': False}
def get_reaction_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'ORDER BULK' : element_schema,
'ORDER WALL' : element_schema,
'ORDER TANK' : element_schema,
'GLOBAL BULK' : element_schema,
'GLOBAL WALL' : element_schema,
'LIMITING POTENTIAL' : element_schema,
'ROUGHNESS CORRELATION' : element_schema }
def get_reaction(name: str) -> dict[str, Any]:
ts = read_all(name, "select key, value from network.reaction_settings")
d = {}
for e in ts:
d[e['key']] = str(e['value'])
return d
def _set_reaction(name: str, cs: ChangeSet) -> DatabaseCommand:
new = {}
new_dict = cs.operations[0]
schema = get_reaction_schema(name)
for key in schema.keys():
if key in new_dict:
new[key] = str(new_dict[key])
change = g_update_prefix | { 'type' : 'reaction' }
statement = ''
for key, value in new.items():
if statement != '':
statement += '\n'
statement += f"update network.reaction_settings set value = {sql_literal(value)} where key = {sql_literal(key)};"
change |= { key: value }
return DatabaseCommand(statement, [change])
def set_reaction(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_reaction(name, cs))
def get_pipe_reaction_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'pipe' : {'type': 'str' , 'optional': False , 'readonly': True },
'bulk' : {'type': 'float' , 'optional': True , 'readonly': False},
'wall' : {'type': 'float' , 'optional': True , 'readonly': False} }
def get_pipe_reaction(name: str, pipe: str) -> dict[str, Any]:
d = {}
d['pipe'] = pipe
pr = try_read(name, "select bulk_coefficient as bulk, wall_coefficient as wall from network.pipe_reaction_coefficients where pipe_id = %s", (pipe,))
d['bulk'] = float(pr['bulk']) if pr is not None and pr['bulk'] is not None else None
d['wall'] = float(pr['wall']) if pr is not None and pr['wall'] is not None else None
return d
class PipeReaction(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'pipe_reaction'
self.pipe = str(input['pipe'])
self.bulk = float(input['bulk']) if 'bulk' in input and input['bulk'] != None else None
self.wall = float(input['wall']) if 'wall' in input and input['wall'] != None else None
self.f_type = sql_literal(self.type)
self.f_pipe = sql_literal(self.pipe)
self.f_bulk = sql_literal(self.bulk)
self.f_wall = sql_literal(self.wall)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'pipe': self.pipe, 'bulk': self.bulk, 'wall': self.wall }
def _set_pipe_reaction(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_pipe_reaction(name, cs.operations[0]['pipe'])
new_dict = cs.operations[0]
schema = get_pipe_reaction_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = PipeReaction(raw_new)
statement = f"delete from network.pipe_reaction_coefficients where pipe_id = {new.f_pipe};"
if new.bulk is not None or new.wall is not None:
statement += f"\ninsert into network.pipe_reaction_coefficients (pipe_id, bulk_coefficient, wall_coefficient) values ({new.f_pipe}, {new.f_bulk}, {new.f_wall});"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_pipe_reaction(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_pipe_reaction(name, cs))
def get_tank_reaction_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'tank' : {'type': 'str' , 'optional': False , 'readonly': True },
'value' : {'type': 'float' , 'optional': True , 'readonly': False} }
def get_tank_reaction(name: str, tank: str) -> dict[str, Any]:
d = {}
d['tank'] = tank
pr = try_read(name, "select coefficient as value from network.tank_reaction_coefficients where tank_id = %s", (tank,))
d['value'] = float(pr['value']) if pr is not None else None
return d
class TankReaction(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'tank_reaction'
self.tank = str(input['tank'])
self.value = float(input['value']) if 'value' in input and input['value'] != None else None
self.f_type = sql_literal(self.type)
self.f_tank = sql_literal(self.tank)
self.f_value = sql_literal(self.value)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'tank': self.tank, 'value': self.value }
def _set_tank_reaction(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_tank_reaction(name, cs.operations[0]['tank'])
new_dict = cs.operations[0]
schema = get_tank_reaction_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = TankReaction(raw_new)
statement = f"delete from network.tank_reaction_coefficients where tank_id = {new.f_tank};"
if new.value != None:
statement += f"\ninsert into network.tank_reaction_coefficients (tank_id, coefficient) values ({new.f_tank}, {new.f_value});"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_tank_reaction(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_tank_reaction(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# ORDER {BULK/WALL/TANK} value
# GLOBAL BULK coeff
# GLOBAL WALL coeff
# BULK link1 (link2) coeff
# WALL link1 (link2) coeff
# TANK node1 (node2) coeff
# LIMITING POTENTIAL value
# ROUGHNESS CORRELATION value
#--------------------------------------------------------------
def inp_in_reaction(line: str) -> str:
tokens = line.split()
token0 = tokens[0].upper()
if token0 == 'BULK' or token0 == 'WALL':
pipe = tokens[1]
key = token0.lower()
value = float(tokens[2])
column = 'bulk_coefficient' if key == 'bulk' else 'wall_coefficient'
return str(f"insert into network.pipe_reaction_coefficients (pipe_id, {column}) values ({sql_literal(pipe)}, {sql_literal(value)}) on conflict (pipe_id) do update set {column} = excluded.{column};")
elif token0 == 'TANK':
tank = tokens[1]
value = float(tokens[2])
return str(f"insert into network.tank_reaction_coefficients (tank_id, coefficient) values ({sql_literal(tank)}, {sql_literal(value)});")
else:
line = line.upper().strip()
for key in get_reaction_schema('').keys():
if line.startswith(key):
value = line.removeprefix(key).strip()
return str(f"update network.reaction_settings set value = {sql_literal(value)} where key = {sql_literal(key)};")
return str('')
def inp_out_reaction(name: str) -> list[str]:
lines = []
objs = read_all(name, "select key, value from network.reaction_settings order by key")
for obj in objs:
key = obj['key']
value = obj['value']
lines.append(f'{key} {value}')
objs = read_all(name, "select pipe_id as pipe, bulk_coefficient as value from network.pipe_reaction_coefficients where bulk_coefficient is not null order by pipe_id")
for obj in objs:
pipe = obj['pipe']
value = obj['value']
lines.append(f'BULK {pipe} {value}')
objs = read_all(name, "select pipe_id as pipe, wall_coefficient as value from network.pipe_reaction_coefficients where wall_coefficient is not null order by pipe_id")
for obj in objs:
pipe = obj['pipe']
value = obj['value']
lines.append(f'WALL {pipe} {value}')
objs = read_all(name, "select tank_id as tank, coefficient as value from network.tank_reaction_coefficients order by tank_id")
for obj in objs:
tank = obj['tank']
value = obj['value']
lines.append(f'TANK {tank} {value}')
return lines
def delete_pipe_reaction_by_pipe(name: str, pipe: str) -> ChangeSet:
row = try_read(name, "select 1 from network.pipe_reaction_coefficients where pipe_id = %s", (pipe,))
if row is None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type': 'pipe_reaction', 'pipe': pipe, 'bulk': None, 'wall': None})
def delete_tank_reaction_by_tank(name: str, tank: str) -> ChangeSet:
row = try_read(name, "select 1 from network.tank_reaction_coefficients where tank_id = %s", (tank,))
if row == None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type': 'tank_reaction', 'tank': tank, 'value': None})
+34
View File
@@ -0,0 +1,34 @@
from ..core.database import read_all
#--------------------------------------------------------------
# [EPA2]
# PAGE linesperpage
# STATUS {NONE/YES/FULL}
# SUMMARY {YES/NO}
# MESSAGES {YES/NO}
# ENERGY {NO/YES}
# NODES {NONE/ALL}
# NODES node1 node2 ...
# LINKS {NONE/ALL}
# LINKS link1 link2 ...
# FILE filename
# variable {YES/NO}
# variable {BELOW/ABOVE/PRECISION} value
# [EPA3][NOT SUPPORT]
# TRIALS {YES/NO}
#--------------------------------------------------------------
def inp_in_report(section: list[str]) -> str:
return ''
def inp_out_report(name: str) -> list[str]:
lines = []
objs = read_all(name, "select key, value from network.report_settings order by key")
for obj in objs:
key = obj['key']
value = obj['value']
lines.append(f'{key} {value}')
return lines
+194
View File
@@ -0,0 +1,194 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
from ..gis.coordinates import (
get_node_coord,
sql_delete_coord,
sql_insert_coord,
sql_update_coord,
)
from .elements import get_all_node_links, get_node_links
def get_reservoir_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
'x' : {'type': 'float' , 'optional': False , 'readonly': False},
'y' : {'type': 'float' , 'optional': False , 'readonly': False},
'head' : {'type': 'float' , 'optional': False , 'readonly': False},
'pattern' : {'type': 'str' , 'optional': True , 'readonly': False},
'links' : {'type': 'str_list' , 'optional': False , 'readonly': True } }
def get_reservoir(name: str, id: str) -> dict[str, Any]:
r = try_read(name, "select node_id as id, head, pattern_id as pattern from network.reservoirs where node_id = %s", (id,))
if r == None:
return {}
xy = get_node_coord(name, id)
d = {}
d['id'] = str(r['id'])
d['x'] = float(xy['x'])
d['y'] = float(xy['y'])
d['head'] = float(r['head'])
d['pattern'] = str(r['pattern']) if r['pattern'] != None else None
d['links'] = get_node_links(name, id)
return d
# DingZQ, 2025-03-29
def get_all_reservoirs(name: str) -> list[dict[str, Any]]:
rows = read_all(
name,
"SELECT id, head, pattern_id AS pattern, x, y "
"FROM gis.reservoirs ORDER BY id",
)
if rows == None:
return []
links_by_node = get_all_node_links(name)
result = []
for row in rows:
d = {}
id = str(row['id'])
d['id'] = id
d['x'] = float(row['x'] or 0.0)
d['y'] = float(row['y'] or 0.0)
d['head'] = float(row['head']) if row['head'] != None else None
d['pattern'] = str(row['pattern']) if row['pattern'] != None else None
d['links'] = links_by_node.get(id, [])
result.append(d)
return result
class Reservoir(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'reservoir'
self.id = str(input['id'])
self.x = float(input['x'])
self.y = float(input['y'])
self.head = float(input['head'])
self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None
self.f_type = sql_literal(self.type)
self.f_id = sql_literal(self.id)
self.f_head = sql_literal(self.head)
self.f_pattern = sql_literal(self.pattern)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'head': self.head, 'pattern': self.pattern }
def _set_reservoir(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_reservoir(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
schema = get_reservoir_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Reservoir(raw_new)
statement = f"update network.reservoirs set head = {new.f_head}, pattern_id = {new.f_pattern} where node_id = {new.f_id};"
statement += f"\n{sql_update_coord(new.id, new.x, new.y)}"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_reservoir(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _set_reservoir(name, cs))
def _add_reservoir(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Reservoir(cs.operations[0])
statement = f"insert into network.nodes (id, node_type) values ({new.f_id}, {new.f_type});"
statement += f"\ninsert into network.reservoirs (node_id, head, pattern_id) values ({new.f_id}, {new.f_head}, {new.f_pattern});"
statement += f"\n{sql_insert_coord(new.id, new.x, new.y)}"
change = g_add_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def add_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_reservoir(name, cs.operations[0]['id']) != {}:
return ChangeSet()
return execute_command(name, _add_reservoir(name, cs))
def _delete_reservoir(name: str, cs: ChangeSet) -> DatabaseCommand:
element_id = str(cs.operations[0]['id'])
f_id = sql_literal(element_id)
statement = sql_delete_coord(element_id)
statement += f"\ndelete from network.nodes where id = {f_id};"
change = g_delete_prefix | {'type': 'reservoir', 'id': element_id}
return DatabaseCommand(statement, [change])
def delete_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_reservoir(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _delete_reservoir(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# id elev (pattern) ;desc
#--------------------------------------------------------------
def inp_in_reservoir(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
id = str(tokens[0])
head = float(tokens[1])
pattern = str(tokens[2]) if num_without_desc >= 3 else None
desc = str(tokens[-1]) if has_desc else None
return str(f"insert into network.nodes (id, node_type) values ({sql_literal(id)}, 'reservoir');insert into network.reservoirs (node_id, head, pattern_id) values ({sql_literal(id)}, {sql_literal(head)}, {sql_literal(pattern)});")
def inp_out_reservoir(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select node_id as id, head, pattern_id as pattern from network.reservoirs order by node_id')
for obj in objs:
id = obj['id']
head = obj['head']
pattern = obj['pattern'] if obj['pattern'] != None else ''
desc = ';'
lines.append(f'{id} {head} {pattern} {desc}')
return lines
def unset_reservoir_by_pattern(name: str, pattern: str) -> ChangeSet:
cs = ChangeSet()
rows = read_all(name, "select node_id as id from network.reservoirs where pattern_id = %s", (pattern,))
for row in rows:
cs.append(g_update_prefix | {'type': 'reservoir', 'id': row['id'], 'pattern': None})
return cs
+50
View File
@@ -0,0 +1,50 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
)
def get_rule_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'rules' : {'type': 'str_list' , 'optional': False , 'readonly': False} }
def get_rule(name: str) -> dict[str, Any]:
cs = read_all(name, "select line from network.rules order by sequence_no")
ds = []
for c in cs:
ds.append(c['line'])
return { 'rules': ds }
def _set_rule(name: str, cs: ChangeSet) -> DatabaseCommand:
statement = 'delete from network.rules;'
for sequence_no, line in enumerate(cs.operations[0]['rules']):
statement += f"\ninsert into network.rules (sequence_no, line) values ({sequence_no}, {sql_literal(line)});"
change = g_update_prefix | { 'type': 'rule', 'rules': cs.operations[0]['rules'] }
return DatabaseCommand(statement, [change])
def set_rule(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_rule(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3]
# TODO...
#--------------------------------------------------------------
def inp_in_rule(line: str) -> str:
return str(f"insert into network.rules (sequence_no, line) values ((select coalesce(max(sequence_no) + 1, 0) from network.rules), {sql_literal(line)});")
def inp_out_rule(name: str) -> list[str]:
return get_rule(name)['rules']
+152
View File
@@ -0,0 +1,152 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
SOURCE_TYPE_CONCEN = 'CONCEN'
SOURCE_TYPE_MASS = 'MASS'
SOURCE_TYPE_FLOWPACED = 'FLOWPACED'
SOURCE_TYPE_SETPOINT = 'SETPOINT'
def get_source_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'node' : {'type': 'str' , 'optional': False , 'readonly': True },
's_type' : {'type': 'str' , 'optional': False , 'readonly': False},
'strength' : {'type': 'float' , 'optional': False , 'readonly': False},
'pattern' : {'type': 'str' , 'optional': True , 'readonly': False} }
def get_source(name: str, node: str) -> dict[str, Any]:
s = try_read(name, "select node_id as node, source_type as s_type, strength, pattern_id as pattern from network.sources where node_id = %s", (node,))
if s == None:
return {}
d = {}
d['node'] = str(s['node'])
d['s_type'] = str(s['s_type'])
d['strength'] = float(s['strength'])
d['pattern'] = str(s['pattern']) if s['pattern'] != None else None
return d
class Source(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'source'
self.node = str(input['node'])
self.s_type = str(input['s_type'])
self.strength = float(input['strength'])
self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None
self.f_type = sql_literal(self.type)
self.f_node = sql_literal(self.node)
self.f_s_type = sql_literal(self.s_type)
self.f_strength = sql_literal(self.strength)
self.f_pattern = sql_literal(self.pattern)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'node': self.node, 's_type': self.s_type, 'strength': self.strength, 'pattern': self.pattern }
def _set_source(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_source(name, cs.operations[0]['node'])
new_dict = cs.operations[0]
schema = get_source_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Source(raw_new)
statement = f"update network.sources set source_type = {new.f_s_type}, strength = {new.f_strength}, pattern_id = {new.f_pattern} where node_id = {new.f_node};"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_source(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_source(name, cs))
def _add_source(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Source(cs.operations[0])
statement = f"insert into network.sources (node_id, source_type, strength, pattern_id) values ({new.f_node}, {new.f_s_type}, {new.f_strength}, {new.f_pattern});"
change = g_add_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def add_source(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_source(name, cs))
def _delete_source(name: str, cs: ChangeSet) -> DatabaseCommand:
node = str(cs.operations[0]['node'])
f_node = sql_literal(node)
statement = f"delete from network.sources where node_id = {f_node};"
change = g_delete_prefix | {'type': 'source', 'node': node}
return DatabaseCommand(statement, [change])
def delete_source(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _delete_source(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# node sourcetype quality (pattern)
#--------------------------------------------------------------
def inp_in_source(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
node = str(tokens[0])
s_type = str(tokens[1].upper())
strength = float(tokens[2])
pattern = str(tokens[3]) if num_without_desc >= 4 else None
return str(f"insert into network.sources (node_id, source_type, strength, pattern_id) values ({sql_literal(node)}, {sql_literal(s_type)}, {sql_literal(strength)}, {sql_literal(pattern)});")
def inp_out_source(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select node_id as node, source_type as s_type, strength, pattern_id as pattern from network.sources order by node_id')
for obj in objs:
node = obj['node']
s_type = obj['s_type']
strength = obj['strength']
pattern = obj['pattern'] if obj['pattern'] != None else ''
lines.append(f'{node} {s_type} {strength} {pattern}')
return lines
def delete_source_by_node(name: str, node: str) -> ChangeSet:
row = try_read(name, "select 1 from network.sources where node_id = %s", (node,))
if row == None:
return ChangeSet()
return ChangeSet(g_delete_prefix | {'type' : 'source', 'node': node})
def unset_source_by_pattern(name: str, pattern: str) -> ChangeSet:
cs = ChangeSet()
rows = read_all(name, "select node_id as node from network.sources where pattern_id = %s", (pattern,))
for row in rows:
cs.append(g_update_prefix | {'type': 'source', 'node': row['node'], 'pattern': None})
return cs
+114
View File
@@ -0,0 +1,114 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
LINK_STATUS_OPEN = 'OPEN'
LINK_STATUS_CLOSED = 'CLOSED'
LINK_STATUS_ACTIVE = 'ACTIVE'
def get_status_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'link' : {'type': 'str' , 'optional': False , 'readonly': True },
'status' : {'type': 'str' , 'optional': True , 'readonly': False},
'setting' : {'type': 'float' , 'optional': True , 'readonly': False} }
def get_status(name: str, link: str) -> dict[str, Any]:
s = try_read(name, "select link_id as link, status, setting from network.link_initial_settings where link_id = %s", (link,))
if s == None:
return { 'link': link, 'status': None, 'setting': None }
d = {}
d['link'] = str(s['link'])
d['status'] = str(s['status']) if s['status'] != None else None
d['setting'] = float(s['setting']) if s['setting'] != None else None
return d
class Status(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'status'
self.link = str(input['link'])
self.status = str(input['status']) if 'status' in input and input['status'] != None else None
self.setting = float(input['setting']) if 'setting' in input and input['setting'] != None else None
self.f_type = sql_literal(self.type)
self.f_link = sql_literal(self.link)
self.f_status = sql_literal(self.status)
self.f_setting = sql_literal(self.setting)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'link': self.link, 'status': self.status, 'setting': self.setting }
def _set_status(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_status(name, cs.operations[0]['link'])
new_dict = cs.operations[0]
schema = get_status_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Status(raw_new)
statement = f"delete from network.link_initial_settings where link_id = {new.f_link};"
if new.status != None or new.setting != None:
statement += f"\ninsert into network.link_initial_settings (link_id, status, setting) values ({new.f_link}, {new.f_status}, {new.f_setting});"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_status(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_status(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# link value
#--------------------------------------------------------------
def inp_in_status(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
link = str(tokens[0])
value = tokens[1].upper()
if value == LINK_STATUS_OPEN or value == LINK_STATUS_CLOSED or value == LINK_STATUS_ACTIVE:
return str(f"insert into network.link_initial_settings (link_id, status, setting) values ({sql_literal(link)}, {sql_literal(value)}, null);")
else:
return str(f"insert into network.link_initial_settings (link_id, status, setting) values ({sql_literal(link)}, null, {sql_literal(float(value))});")
def inp_out_status(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select link_id as link, status, setting from network.link_initial_settings order by link_id')
for obj in objs:
link = obj['link']
status = obj['status'] if obj['status'] != None else ''
setting = obj['setting'] if obj['setting'] != None else ''
if status != '':
lines.append(f'{link} {status}')
if setting != '':
lines.append(f'{link} {setting}')
return lines
def delete_status_by_link(name: str, link: str) -> ChangeSet:
row = try_read(name, "select 1 from network.link_initial_settings where link_id = %s", (link,))
if row == None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type': 'status', 'link': link, 'status': None, 'setting': None})
+123
View File
@@ -0,0 +1,123 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
TAG_TYPE_NODE = "NODE"
TAG_TYPE_LINK = "LINK"
def get_tag_schema(name: str) -> dict[str, dict[str, Any]]:
return {
"t_type": {"type": "str", "optional": False, "readonly": False},
"id": {"type": "str", "optional": False, "readonly": False},
"tag": {"type": "str", "optional": True, "readonly": False},
}
def get_tags(name: str) -> list[dict[str, Any]]:
rows = read_all(
name,
"""
select 'NODE' as t_type, node_id as id, tag from network.node_tags
union all
select 'LINK' as t_type, link_id as id, tag from network.link_tags
order by t_type, id
""",
)
return [
{
"t_type": str(row["t_type"]),
"id": str(row["id"]),
"tag": str(row["tag"]) if row["tag"] is not None else None,
}
for row in rows
]
def _tag_table(t_type: str) -> tuple[str, str]:
if t_type == TAG_TYPE_NODE:
return "network.node_tags", "node_id"
if t_type == TAG_TYPE_LINK:
return "network.link_tags", "link_id"
raise ValueError("Only NODE and LINK tags are supported")
def get_tag(name: str, t_type: str, id: str) -> dict[str, Any]:
table, id_column = _tag_table(t_type)
row = try_read(
name,
f"select {id_column} as id, tag from {table} where {id_column} = %s",
(id,),
)
return {
"t_type": t_type,
"id": id,
"tag": str(row["tag"]) if row and row["tag"] is not None else None,
}
def _replace_tag_sql(t_type: str, element_id: str, tag: str | None) -> str:
table, id_column = _tag_table(t_type)
element_sql = sql_literal(element_id)
statements = [f"delete from {table} where {id_column} = {element_sql};"]
if tag is not None:
statements.append(
f"insert into {table} ({id_column}, tag) "
f"values ({element_sql}, {sql_literal(tag)});"
)
return "\n".join(statements)
def _set_tag(name: str, cs: ChangeSet) -> DatabaseCommand:
operation = cs.operations[0]
t_type = str(operation["t_type"]).upper()
element_id = str(operation["id"])
new = {"t_type": t_type, "id": element_id, "tag": operation.get("tag")}
return DatabaseCommand(
_replace_tag_sql(t_type, element_id, new["tag"]),
[g_update_prefix | {"type": "tag"} | new],
)
def set_tag(name: str, cs: ChangeSet) -> ChangeSet:
if not {"t_type", "id", "tag"} <= cs.operations[0].keys():
return ChangeSet()
return execute_command(name, _set_tag(name, cs))
def inp_in_tag(line: str) -> str:
tokens = line.split()
if len(tokens) < 3:
return ""
return _replace_tag_sql(tokens[0].upper(), tokens[1], tokens[2])
def inp_out_tag(name: str) -> list[str]:
return [f"{row['t_type']} {row['id']} {row['tag']}" for row in get_tags(name)]
def delete_tag_by_node(name: str, node: str) -> ChangeSet:
row = get_tag(name, TAG_TYPE_NODE, node)
return (
ChangeSet(g_update_prefix | {"type": "tag"} | row | {"tag": None})
if row["tag"] is not None
else ChangeSet()
)
def delete_tag_by_link(name: str, link: str) -> ChangeSet:
row = get_tag(name, TAG_TYPE_LINK, link)
return (
ChangeSet(g_update_prefix | {"type": "tag"} | row | {"tag": None})
if row["tag"] is not None
else ChangeSet()
)
+252
View File
@@ -0,0 +1,252 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
from ..gis.coordinates import (
get_node_coord,
sql_delete_coord,
sql_insert_coord,
sql_update_coord,
)
from .elements import get_all_node_links, get_node_links
OVERFLOW_YES = 'YES'
OVERFLOW_NO = 'NO'
def get_tank_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
'x' : {'type': 'float' , 'optional': False , 'readonly': False},
'y' : {'type': 'float' , 'optional': False , 'readonly': False},
'elevation' : {'type': 'float' , 'optional': False , 'readonly': False},
'init_level' : {'type': 'float' , 'optional': False , 'readonly': False},
'min_level' : {'type': 'float' , 'optional': False , 'readonly': False},
'max_level' : {'type': 'float' , 'optional': False , 'readonly': False},
'diameter' : {'type': 'float' , 'optional': False , 'readonly': False},
'min_vol' : {'type': 'float' , 'optional': False , 'readonly': False},
'vol_curve' : {'type': 'str' , 'optional': True , 'readonly': False},
'overflow' : {'type': 'str' , 'optional': True , 'readonly': False},
'links' : {'type': 'str_list' , 'optional': False , 'readonly': True } }
def get_tank(name: str, id: str) -> dict[str, Any]:
t = try_read(name, "select node_id as id, elevation, initial_level as init_level, minimum_level as min_level, maximum_level as max_level, diameter, minimum_volume as min_vol, volume_curve_id as vol_curve, overflow from network.tanks where node_id = %s", (id,))
if t == None:
return {}
xy = get_node_coord(name, id)
d = {}
d['id'] = str(t['id'])
d['x'] = float(xy['x'])
d['y'] = float(xy['y'])
d['elevation'] = float(t['elevation'])
d['init_level'] = float(t['init_level'])
d['min_level'] = float(t['min_level'])
d['max_level'] = float(t['max_level'])
d['diameter'] = float(t['diameter'])
d['min_vol'] = float(t['min_vol'])
d['vol_curve'] = str(t['vol_curve']) if t['vol_curve'] != None else None
d['overflow'] = str(t['overflow']) if t['overflow'] != None else None
d['links'] = get_node_links(name, id)
return d
# DingZQ, 2025-03-29
def get_all_tanks(name: str) -> list[dict[str, Any]]:
rows = read_all(
name,
"SELECT id, elevation, initial_level AS init_level, "
"minimum_level AS min_level, maximum_level AS max_level, diameter, "
"minimum_volume AS min_vol, volume_curve_id AS vol_curve, overflow, "
"x, y FROM gis.tanks ORDER BY id",
)
if rows == None:
return []
links_by_node = get_all_node_links(name)
result = []
for row in rows:
d = {}
id = str(row['id'])
d['id'] = id
d['x'] = float(row['x'] or 0.0)
d['y'] = float(row['y'] or 0.0)
d['elevation'] = float(row['elevation'])
d['init_level'] = float(row['init_level'])
d['min_level'] = float(row['min_level'])
d['max_level'] = float(row['max_level'])
d['diameter'] = float(row['diameter'])
d['min_vol'] = float(row['min_vol'])
d['vol_curve'] = str(row['vol_curve']) if row['vol_curve'] != None else None
d['overflow'] = str(row['overflow']) if row['overflow'] != None else None
d['links'] = links_by_node.get(id, [])
result.append(d)
return result
class Tank(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'tank'
self.id = str(input['id'])
self.x = float(input['x'])
self.y = float(input['y'])
self.elevation = float(input['elevation'])
self.init_level = float(input['init_level'])
self.min_level = float(input['min_level'])
self.max_level = float(input['max_level'])
self.diameter = float(input['diameter'])
self.min_vol = float(input['min_vol'])
self.vol_curve = str(input['vol_curve']) if 'vol_curve' in input and input['vol_curve'] != None else None
self.overflow = str(input['overflow']) if 'overflow' in input and input['overflow'] != None else None
self.f_type = sql_literal(self.type)
self.f_id = sql_literal(self.id)
self.f_elevation = sql_literal(self.elevation)
self.f_init_level = sql_literal(self.init_level)
self.f_min_level = sql_literal(self.min_level)
self.f_max_level = sql_literal(self.max_level)
self.f_diameter = sql_literal(self.diameter)
self.f_min_vol = sql_literal(self.min_vol)
self.f_vol_curve = sql_literal(self.vol_curve)
self.f_overflow = sql_literal(self.overflow)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'elevation': self.elevation, 'init_level': self.init_level, 'min_level': self.min_level, 'max_level': self.max_level, 'diameter': self.diameter, 'min_vol': self.min_vol, 'vol_curve': self.vol_curve, 'overflow': self.overflow }
def _set_tank(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_tank(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
schema = get_tank_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Tank(raw_new)
statement = f"update network.tanks set elevation = {new.f_elevation}, initial_level = {new.f_init_level}, minimum_level = {new.f_min_level}, maximum_level = {new.f_max_level}, diameter = {new.f_diameter}, minimum_volume = {new.f_min_vol}, volume_curve_id = {new.f_vol_curve}, overflow = {new.f_overflow} where node_id = {new.f_id};"
statement += f"\n{sql_update_coord(new.id, new.x, new.y)}"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_tank(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_tank(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _set_tank(name, cs))
def _add_tank(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Tank(cs.operations[0])
statement = f"insert into network.nodes (id, node_type) values ({new.f_id}, {new.f_type});"
statement += f"\ninsert into network.tanks (node_id, elevation, initial_level, minimum_level, maximum_level, diameter, minimum_volume, volume_curve_id, 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});"
statement += f"\n{sql_insert_coord(new.id, new.x, new.y)}"
change = g_add_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def add_tank(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_tank(name, cs.operations[0]['id']) != {}:
return ChangeSet()
return execute_command(name, _add_tank(name, cs))
def _delete_tank(name: str, cs: ChangeSet) -> DatabaseCommand:
element_id = str(cs.operations[0]['id'])
f_id = sql_literal(element_id)
statement = sql_delete_coord(element_id)
statement += f"\ndelete from network.nodes where id = {f_id};"
change = g_delete_prefix | {'type': 'tank', 'id': element_id}
return DatabaseCommand(statement, [change])
def delete_tank(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_tank(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _delete_tank(name, cs))
#--------------------------------------------------------------
# [EPA2]
# [IN]
# id elev initlevel minlevel maxlevel diam (minvol vcurve overflow) ;desc
# xxx
# * YES
# [OUT]
# id elev initlevel minlevel maxlevel diam minvol (vcurve overflow) ;desc
#--------------------------------------------------------------
# [EPA3]
# id elev initlevel minlevel maxlevel diam minvol (vcurve)
#--------------------------------------------------------------
def inp_in_tank(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
id = str(tokens[0])
elevation = float(tokens[1])
init_level = float(tokens[2])
min_level = float(tokens[3])
max_level = float(tokens[4])
diameter = float(tokens[5])
min_vol = float(tokens[6]) if num_without_desc >= 7 else 0.0
vol_curve = str(tokens[7]) if num_without_desc >= 8 and tokens[7] != '*' else None
overflow = str(tokens[8].upper()) if num_without_desc >= 9 else None
desc = str(tokens[-1]) if has_desc else None
return str(f"insert into network.nodes (id, node_type) values ({sql_literal(id)}, 'tank');insert into network.tanks (node_id, elevation, initial_level, minimum_level, maximum_level, diameter, minimum_volume, volume_curve_id, overflow) values ({sql_literal(id)}, {sql_literal(elevation)}, {sql_literal(init_level)}, {sql_literal(min_level)}, {sql_literal(max_level)}, {sql_literal(diameter)}, {sql_literal(min_vol)}, {sql_literal(vol_curve)}, {sql_literal(overflow)});")
def inp_out_tank(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select node_id as id, elevation, initial_level as init_level, minimum_level as min_level, maximum_level as max_level, diameter, minimum_volume as min_vol, volume_curve_id as vol_curve, overflow from network.tanks order by node_id')
for obj in objs:
id = obj['id']
elevation = obj['elevation']
init_level = obj['init_level']
min_level = obj['min_level']
max_level = obj['max_level']
diameter = obj['diameter']
min_vol = obj['min_vol']
vol_curve = obj['vol_curve'] if obj['vol_curve'] != None else ''
overflow = obj['overflow'] if obj['overflow'] != None else ''
if vol_curve == '' and overflow != '':
vol_curve = '*'
desc = ';'
lines.append(f'{id} {elevation} {init_level} {min_level} {max_level} {diameter} {min_vol} {vol_curve} {overflow} {desc}')
return lines
def unset_tank_by_curve(name: str, curve: str) -> ChangeSet:
cs = ChangeSet()
rows = read_all(name, "select node_id as id from network.tanks where volume_curve_id = %s", (curve,))
for row in rows:
cs.append(g_update_prefix | {'type': 'tank', 'id': row['id'], 'vol_curve': None})
return cs
+110
View File
@@ -0,0 +1,110 @@
from typing import Any
from psycopg import sql
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
)
TIME_STATISTIC_NONE = 'NONE'
TIME_STATISTIC_AVERAGED = 'AVERAGED'
TIME_STATISTIC_MINIMUM = 'MINIMUM'
TIME_STATISTIC_MAXIMUM = 'MAXIMUM'
TIME_STATISTIC_RANGE = 'RANGE'
element_schema = {'type': 'str' , 'optional': True , 'readonly': False}
def get_time_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'DURATION' : element_schema,
'HYDRAULIC TIMESTEP' : element_schema,
'QUALITY TIMESTEP' : element_schema,
'RULE TIMESTEP' : element_schema,
'PATTERN TIMESTEP' : element_schema,
'PATTERN START' : element_schema,
'REPORT TIMESTEP' : element_schema,
'REPORT START' : element_schema,
'START CLOCKTIME' : element_schema,
'STATISTIC' : element_schema}
def get_time(name: str) -> dict[str, Any]:
ts = read_all(name, "select key, value from network.time_settings")
d = {}
for e in ts:
d[e['key']] = str(e['value'])
return d
def _set_time(name: str, cs: ChangeSet) -> DatabaseCommand:
new = {}
new_dict = cs.operations[0]
schema = get_time_schema(name)
for key in schema.keys():
if key in new_dict:
new[key] = str(new_dict[key])
change = g_update_prefix | { 'type' : 'time' }
statement = ''
for key, value in new.items():
if statement != '':
statement += '\n'
statement += f"update network.time_settings set value = {sql_literal(value)} where key = {sql_literal(key)};"
change |= { key: value }
return DatabaseCommand(statement, [change])
def set_time(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_time(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3]
# STATISTIC {NONE/AVERAGE/MIN/MAX/RANGE}
# DURATION value (units)
# HYDRAULIC TIMESTEP value (units)
# QUALITY TIMESTEP value (units)
# RULE TIMESTEP value (units)
# PATTERN TIMESTEP value (units)
# PATTERN START value (units)
# REPORT TIMESTEP value (units)
# REPORT START value (units)
# START CLOCKTIME value (AM PM)
# [EPA3] supports [EPA2] keyword
#--------------------------------------------------------------
def inp_in_time(section: list[str]) -> str:
sql = ''
for s in section:
if s.startswith(';'):
continue
line = s.upper().strip()
# TOTAL DURATION => DURATION
if line.startswith('TOTAL DURATION'):
line = line.replace('TOTAL DURATION', 'DURATION')
for key in get_time_schema('').keys():
if line.startswith(key):
value = line.removeprefix(key).strip()
sql += f"update network.time_settings set value = {sql_literal(value)} where key = {sql_literal(key)};"
return sql
def inp_out_time(name: str) -> list[str]:
lines = []
objs = read_all(name, "select key, value from network.time_settings order by key")
for obj in objs:
key = obj['key']
value = obj['value']
lines.append(f'{key} {value}')
return lines
+52
View File
@@ -0,0 +1,52 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read_all,
sql_literal,
)
def get_title_schema(name: str) -> dict[str, dict[str, Any]]:
return {"value": {"type": "str", "optional": False, "readonly": False}}
def get_title(name: str) -> dict[str, Any]:
rows = read_all(
name,
"select value from network.model_titles order by sequence_no",
)
return {"value": "\n".join(str(row["value"]) for row in rows)}
def _replace_title_sql(value: str) -> str:
statements = ["delete from network.model_titles;"]
for sequence_no, line in enumerate(value.split("\n")):
statements.append(
"insert into network.model_titles (sequence_no, value) "
f"values ({sequence_no}, {sql_literal(line)});"
)
return "\n".join(statements)
def _set_title(name: str, cs: ChangeSet) -> DatabaseCommand:
new = str(cs.operations[0]["value"])
return DatabaseCommand(
_replace_title_sql(new),
[g_update_prefix | {"type": "title", "value": new}],
)
def set_title(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_title(name, cs))
def inp_in_title(section: list[str]) -> str:
return _replace_title_sql("\n".join(section))
def inp_out_title(name: str) -> list[str]:
return str(get_title(name)["value"]).split("\n")
+200
View File
@@ -0,0 +1,200 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
VALVES_TYPE_PRV = 'PRV'
VALVES_TYPE_PSV = 'PSV'
VALVES_TYPE_PBV = 'PBV'
VALVES_TYPE_FCV = 'FCV'
VALVES_TYPE_TCV = 'TCV'
VALVES_TYPE_GPV = 'GPV'
def get_valve_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
'node1' : {'type': 'str' , 'optional': False , 'readonly': False},
'node2' : {'type': 'str' , 'optional': False , 'readonly': False},
'diameter' : {'type': 'float' , 'optional': False , 'readonly': False},
'v_type' : {'type': 'str' , 'optional': False , 'readonly': False},
'setting' : {'type': 'str' , 'optional': False , 'readonly': False},
'minor_loss' : {'type': 'float' , 'optional': False , 'readonly': False} }
def get_valve(name: str, id: str) -> dict[str, Any]:
p = try_read(name, "select l.id, l.start_node_id as node1, l.end_node_id as node2, v.diameter, v.valve_type as v_type, v.setting, v.minor_loss from network.links l join network.valves v on v.link_id = l.id where l.id = %s", (id,))
if p == None:
return {}
d = {}
d['id'] = str(p['id'])
d['node1'] = str(p['node1'])
d['node2'] = str(p['node2'])
d['diameter'] = float(p['diameter'])
d['v_type'] = str(p['v_type'])
d['setting'] = str(p['setting'])
d['minor_loss'] = float(p['minor_loss'])
return d
def get_all_valves(name: str) -> list[dict[str, Any]]:
rows = read_all(
name,
"SELECT id, start_node_id AS node1, end_node_id AS node2, diameter, "
"valve_type AS v_type, setting, minor_loss FROM gis.valves ORDER BY id",
)
if rows == None:
return []
result = []
for row in rows:
d = {}
d['id'] = str(row['id'])
d['node1'] = str(row['node1'])
d['node2'] = str(row['node2'])
d['diameter'] = float(row['diameter'])
d['v_type'] = str(row['v_type'])
d['setting'] = str(row['setting'])
d['minor_loss'] = float(row['minor_loss'])
result.append(d)
return result
class Valve(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'valve'
self.id = str(input['id'])
self.node1 = str(input['node1'])
self.node2 = str(input['node2'])
self.diameter = float(input['diameter'])
self.v_type = str(input['v_type'])
self.setting = str(input['setting'])
self.minor_loss = float(input['minor_loss'])
self.f_type = sql_literal(self.type)
self.f_id = sql_literal(self.id)
self.f_node1 = sql_literal(self.node1)
self.f_node2 = sql_literal(self.node2)
self.f_diameter = sql_literal(self.diameter)
self.f_v_type = sql_literal(self.v_type)
self.f_setting = sql_literal(self.setting)
self.f_minor_loss = sql_literal(self.minor_loss)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'node1': self.node1, 'node2': self.node2, 'diameter': self.diameter, 'v_type': self.v_type, 'setting': self.setting, 'minor_loss': self.minor_loss }
def _set_valve(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_valve(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
schema = get_valve_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Valve(raw_new)
statement = f"update network.links set start_node_id = {new.f_node1}, end_node_id = {new.f_node2} where id = {new.f_id};"
statement += f"\nupdate network.valves set diameter = {new.f_diameter}, valve_type = {new.f_v_type}, setting = {new.f_setting}, minor_loss = {new.f_minor_loss} where link_id = {new.f_id};"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_valve(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_valve(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _set_valve(name, cs))
def _add_valve(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Valve(cs.operations[0])
statement = f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({new.f_id}, {new.f_type}, {new.f_node1}, {new.f_node2});"
statement += f"\ninsert into network.valves (link_id, diameter, valve_type, setting, minor_loss) values ({new.f_id}, {new.f_diameter}, {new.f_v_type}, {new.f_setting}, {new.f_minor_loss});"
change = g_add_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def add_valve(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_valve(name, cs.operations[0]['id']) != {}:
return ChangeSet()
return execute_command(name, _add_valve(name, cs))
def _delete_valve(name: str, cs: ChangeSet) -> DatabaseCommand:
element_id = str(cs.operations[0]['id'])
f_id = sql_literal(element_id)
statement = f"delete from network.links where id = {f_id};"
change = g_delete_prefix | {'type': 'valve', 'id': element_id}
return DatabaseCommand(statement, [change])
def delete_valve(name: str, cs: ChangeSet) -> ChangeSet:
if 'id' not in cs.operations[0]:
return ChangeSet()
if get_valve(name, cs.operations[0]['id']) == {}:
return ChangeSet()
return execute_command(name, _delete_valve(name, cs))
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# id node1 node2 diam type setting (lcoeff lcurve)
# for GPV, setting is string = head curve id
# [NOT SUPPORT] for PCV, add loss curve if present
#--------------------------------------------------------------
def inp_in_valve(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
id = str(tokens[0])
node1 = str(tokens[1])
node2 = str(tokens[2])
diameter = float(tokens[3])
v_type = str(tokens[4].upper())
setting = str(tokens[5])
minor_loss = float(tokens[6]) if len(tokens) >= 7 else 0.0
desc = str(tokens[-1]) if has_desc else None
return str(f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({sql_literal(id)}, 'valve', {sql_literal(node1)}, {sql_literal(node2)});insert into network.valves (link_id, diameter, valve_type, setting, minor_loss) values ({sql_literal(id)}, {sql_literal(diameter)}, {sql_literal(v_type)}, {sql_literal(setting)}, {sql_literal(minor_loss)});")
def inp_out_valve(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select l.id, l.start_node_id as node1, l.end_node_id as node2, v.diameter, v.valve_type as v_type, v.setting, v.minor_loss from network.links l join network.valves v on v.link_id = l.id order by l.id')
for obj in objs:
id = obj['id']
node1 = obj['node1']
node2 = obj['node2']
diameter = obj['diameter']
v_type = obj['v_type']
setting = obj['setting']
minor_loss = obj['minor_loss']
desc = ';'
lines.append(f'{id} {node1} {node2} {diameter} {v_type} {setting} {minor_loss} {desc}')
return lines