DNV Water Distribution Network Base Demand Assignment Workflow
Overview
This document describes the complete workflow for assigning base demand values to junctions in a water distribution network model. The workflow begins with spatial joins in QGIS to establish relationships between network components (buildings, parcels, service connections, mains, and water meters), then traces demand from buildings (with assigned building types) through parcels, service connections, mains, and finally to network junctions.
Part 1: Completed Steps - From Buildings to Mains
Step 0: Spatial Joins to Establish Parcel Relationships
Objective: Establish spatial relationships between all network components (buildings, service connections, mains, water meters) and parcels, creating a foundation for demand aggregation.
Source Data (from shp_v2/ directory):
- Buildings (
1_BldBuilding_shp/BldBuilding.shp): Polygon layer containing building footprints with building type attributes (BLDGTYPE,BLDGCLASS_) - Parcels (
2_CadParcels_shp/CadParcels.shp): Polygon layer containing cadastral parcel boundaries with unique parcel IDs (DBLINK) - Service Connections (
3_WtrServiceConnection_shp/WtrServiceConnection.shp): Line layer representing water service connection pipes - Water Mains (
6_WtrMain_shp_v2/WtrMain.shp): Line layer representing water distribution mains - Water Meters (
4_5_water_meter_dnv/water_meters_export.shp): Point layer containing water meter locations with consumption data
Spatial Join Strategy: The workflow used QGIS to perform spatial joins, following a two-phase approach:
Phase 1: Spatial Joins to Parcels (establishing spatial relationships):
- All layers were spatially joined to the parcels layer to assign a
ParcelID(from parcelsDBLINKfield) to each feature - This created a common reference system where all network components could be linked through their parcel associations
- All layers were spatially joined to the parcels layer to assign a
Phase 2: Attribute Joins (after spatial relationships established):
- Once spatial relationships were established, attribute joins were performed using the parcel IDs
- This allowed buildings, service connections, and meters to be associated with each other through their shared parcel relationships
Spatial Join Operations Performed:
Buildings → Parcels:
- Method: Polygon-to-polygon spatial join (intersection)
- Result:
D_BuildingWithParcelId.csv- Each building assigned to its containing parcel viaDBLINKfield
Service Connections → Parcels:
- Method: Line-to-polygon spatial join (intersection or proximity)
- Result:
B_SCwithParcelId.csv- Each service connection assigned to nearest/intersecting parcel viaDBLINK_1field
Service Connections → Mains:
- Method: Line-to-line spatial join (proximity/intersection)
- Result:
A_SCwithMainId.csv- Each service connection assigned to nearest main viaASSET_ID_1field
Water Meters → Parcels:
- Method: Point-to-polygon spatial join using two approaches:
- Closest 5m: Nearest neighbor join within 5-meter buffer (used in final analysis)
- Intersect 1m: Intersection join within 1-meter buffer (alternative method, not used)
- Result:
C_WMwithParcelId_Closest_5m.csv(used) andC_WMwithParcelId_Intersect_1m.csv(alternative) - Final Selection: The "closest 5m" method was selected for the final analysis as it provided better coverage and more reliable associations
- Method: Point-to-polygon spatial join using two approaches:
Key Rationale:
- Parcels serve as the fundamental spatial unit linking all network components
- By joining everything to parcels first, the workflow creates a hierarchical relationship: Parcels → Buildings, Service Connections, Meters
- This parcel-centric approach enables demand aggregation from buildings through parcels to service connections and ultimately to mains
Output Files Created:
D_BuildingWithParcelId.csv- Buildings with parcel IDsB_SCwithParcelId.csv- Service connections with parcel IDsA_SCwithMainId.csv- Service connections with main IDsC_WMwithParcelId_Closest_5m.csv- Water meters with parcel IDs (used in analysis)C_WMwithParcelId_Intersect_1m.csv- Alternative water meter to parcel join (not used)
Step 1: Building Type Base Demand Calculation
Objective: Calculate average demand per building type using real meter data from parcels with single building types.
Prerequisites: Spatial joins from Step 0 must be completed to establish parcel relationships.
Process:
Identify Single-Type Parcels: Filter parcels that contain only buildings of a single
BLDGTYPE+BLDGCLASS_combination- Source:
D_BuildingWithParcelIdtable (created in Step 0 via spatial join) - Logic: Parcels where
COUNT(DISTINCT BLDGTYPE || '|' || BLDGCLASS_) = 1
- Source:
Associate Meters with Parcels:
- Used
C_WMwithParcelId_Closest_5m.csv(created in Step 0 via spatial join - meters within 5m of parcels) - Alternative:
C_WMwithParcelId_Intersect_1m.csv(alternative spatial join method, not used) - Result:
meters_with_single_building_typetable
- Used
Extract Meter Data:
- Query
DeviceTagDatafor meters withUnits = 'LPS' - Filter to meters in single-type parcels
- Join with building type/class information
- Result:
meter_timeseries_with_bldg_type_class.csv
- Query
Calculate Average Demand by Building Type:
- Aggregate meter data by
BLDGTYPE - Calculate average demand (LPS) for each building type
- Result:
july_2023_avg_demand_by_bldg_type.csvwith 11 building types:- CHURCH: 0.024974261 LPS
- COMMERCIAL BUILDING: 0.04553726 LPS
- GOVERNMENT HERITAGE BUILDING: 0.005024497 LPS
- INDUSTRIAL BUILDING: 0.026891404544580184 LPS
- MIXED USE: 0.058638603867555684 LPS
- MULTI FAMILY: 0.2143962256022281 LPS
- OTHER: 0.11843456465664087 LPS
- RECREATION FACILITY: 0.29122198106779584 LPS
- SCHOOL: 0.04053746 LPS
- SINGLE FAMILY: 0.023858310290417913 LPS
- SKI: 2.1797343286124495 LPS
- Aggregate meter data by
Files Created:
sql/meters_single_building_type.sqlsql/get_meter_data.sqlmeters_with_single_building_type.csvjuly_2023_avg_demand_by_bldg_type.csv
Step 2: Assign Base Demand to Buildings
Objective: Add BaseDemand column to buildings table and populate based on building type.
Process:
- Add Column: Add
BaseDemandNUMERIC column toD_BuildingWithParcelIdtable - Update Values: Update each building's
BaseDemandbased on itsBLDGTYPEmatching the values from Step 1
SQL Script: sql/add_base_demand_column.sql
Result: All buildings in D_BuildingWithParcelId now have a BaseDemand value assigned based on their building type.
Step 3: Aggregate Parcel Demands
Objective: Calculate total base demand for each parcel by summing all building demands within that parcel.
Process:
- Create ParcelsWithDemand Table:
- Group buildings by
DBLINK(parcel ID) - Sum
BaseDemandfor all buildings in each parcel - Result:
ParcelsWithDemandtable with columns:ParcelId(DBLINK)TotalBaseDemand(sum of all building demands)
- Group buildings by
SQL Script: sql/create_parcels_with_demand.sql
Key Logic:
SELECT DBLINK as ParcelId, SUM(BaseDemand) as TotalBaseDemand
FROM D_BuildingWithParcelId
WHERE DBLINK != '' AND BaseDemand IS NOT NULL
GROUP BY DBLINK
Step 4: Assign Demand to Service Connections
Objective: Distribute parcel demand equally among all service connections in that parcel.
Process:
Join Service Connections with Parcels:
- Source:
B_SCwithParcelId(service connections with parcel IDs viaDBLINK_1) - Join with
ParcelsWithDemandon parcel ID
- Source:
Calculate Demand per Service Connection:
- For each parcel, count the number of service connections
- Divide parcel's total demand equally:
ParcelTotalDemand / ServiceConnectionCount - This ensures all service connections in a parcel sum to the parcel's total demand
SQL Script: sql/create_sc_with_base_demand.sql
Result: SCwithBaseDemand table with columns:
ServiceConnectionId(ASSET_ID)TotalBaseDemand(divided equally among SCs in parcel)ParcelId(DBLINK_1)ServiceConnectionsInParcel(count)ParcelTotalDemand(original parcel total)
Key Logic:
-- Count SCs per parcel, then divide parcel demand equally
TotalBaseDemand = ParcelTotalDemand / ServiceConnectionCount
Step 5: Aggregate Main Demands
Objective: Sum base demand from all service connections attached to each water main.
Process:
Join Service Connections with Mains:
- Source:
A_SCwithMainId(service connections with main IDs viaASSET_ID_1) - Join with
SCwithBaseDemandon service connection ID (ASSET_ID)
- Source:
Sum Demand per Main:
- Group by main ID (
ASSET_ID_1) - Sum
TotalBaseDemandfrom all service connections - Count number of service connections per main
- Group by main ID (
SQL Script: sql/create_mains_with_total_base_demand.sql
Result: MainsWithTotalBaseDemand table with columns:
MainId(ASSET_ID_1)TotalBaseDemand(sum of all SC demands)ServiceConnectionCount(number of SCs on this main)
Key Logic:
SELECT ASSET_ID_1 as MainId, SUM(TotalBaseDemand) as TotalBaseDemand
FROM A_SCwithMainId scm
JOIN SCwithBaseDemand scd ON scm.ASSET_ID = scd.ServiceConnectionId
GROUP BY ASSET_ID_1
Part 2: Next Steps - From Mains to Junctions
Step 6: Determine Flow Direction for Each Main
Objective: Determine the downstream (assignment) node for each main using hydraulic simulation results.
Process:
Access Hydraulic Results:
- Query
LinkHydrstable forScenarioId = 1 - Each record contains:
Id: Link/Main ID (foreign key toLinkstable)Flow: Flow rate (positive = FromNode→ToNode, negative = ToNode→FromNode)SimUnixTime: Simulation timestampScenarioId: Scenario identifier (use 1)
- Query
Get Link Topology:
- Join
LinkHydrswithLinkstable to get:FromNode: Upstream node IDToNode: Downstream node ID
- Note:
Linkstable contains all link types (Pipes, Pumps, Valves) - Filter to
Pipesonly (mains) if needed
- Join
Calculate Average Flow Direction:
- Critical: Some pipes experience flow direction oscillation during simulation (flow alternates between positive and negative values)
- Using a single timestamp (e.g., the last timestamp) would arbitrarily assign direction based on the final state, which may not represent the dominant flow pattern
- Solution: Calculate the average flow across all timestamps for each main
- This identifies the dominant flow direction over the entire simulation period
- If average flow is positive → dominant direction is FromNode→ToNode
- If average flow is negative → dominant direction is ToNode→FromNode
- If average flow is zero → no dominant direction, default to ToNode
Determine Assignment Node:
- If
AvgFlow > 0: Dominant flow is FromNode → ToNode, so assignment node =ToNode - If
AvgFlow < 0: Dominant flow is ToNode → FromNode, so assignment node =FromNode - If
AvgFlow = 0: No dominant direction, default toToNode
- If
Key Logic:
-- Calculate average flow across all timestamps
WITH AverageHydraulicResults AS (
SELECT Id, AVG(Flow) as AvgFlow
FROM LinkHydrs
WHERE ScenarioId = 1
GROUP BY Id
)
-- Determine downstream/assignment node based on average flow direction
AssignmentNode = CASE
WHEN AvgFlow > 0 THEN ToNode
WHEN AvgFlow < 0 THEN FromNode
ELSE ToNode -- Zero average flow: default to ToNode
END
Rationale for Average Flow Approach:
- Problem: Flow direction can oscillate during simulation (especially in pipes with varying demand patterns)
- Issue with Last Timestamp: Using the final timestamp arbitrarily selects one direction, which may not represent the overall flow pattern
- Solution: Averaging flow values identifies the dominant direction over the entire simulation period
- Result: More stable and representative assignment of demands to downstream junctions
Step 7: Create Mains with Assignment Nodes Table
Objective: Create an intermediate table linking mains to their assignment (downstream) nodes.
Process:
Join Mains with Hydraulic Results:
- Join
MainsWithTotalBaseDemandwithLinkHydrson MainId = LinkId - Filter to
ScenarioId = 1 - Join with
Linksto getFromNodeandToNode
- Join
Calculate Assignment Node:
- Apply flow direction logic from Step 6
- Create
MainsWithAssignmentNodestable with columns:MainIdAssignmentNodeId(downstream node)TotalBaseDemandFlow(for verification)FromNode,ToNode(for reference)
SQL Script: sql/create_mains_with_assignment_nodes.sql (to be created)
Key Logic:
SELECT
m.MainId,
CASE
WHEN lh.Flow > 0 THEN l.ToNode
WHEN lh.Flow < 0 THEN l.FromNode
ELSE NULL
END as AssignmentNodeId,
m.TotalBaseDemand,
lh.Flow,
l.FromNode,
l.ToNode
FROM MainsWithTotalBaseDemand m
JOIN LinkHydrs lh ON m.MainId = lh.Id AND lh.ScenarioId = 1
JOIN Links l ON lh.Id = l.Id
Step 8: Aggregate Junction Demands
Objective: Sum base demand from all mains assigned to each junction.
Process:
Group by Assignment Node:
- Group
MainsWithAssignmentNodesbyAssignmentNodeId - Sum
TotalBaseDemandfor all mains assigned to each junction - Count number of mains per junction
- Group
Create Final Table:
- Create
JunctionsWithBaseDemandtable with columns:JunctionId(node ID)TotalBaseDemand(sum of all main demands)MainCount(number of mains assigned to this junction)
- Create
SQL Script: sql/create_junctions_with_base_demand.sql (to be created)
Key Logic:
SELECT
AssignmentNodeId as JunctionId,
SUM(TotalBaseDemand) as TotalBaseDemand,
COUNT(*) as MainCount
FROM MainsWithAssignmentNodes
WHERE AssignmentNodeId IS NOT NULL
GROUP BY AssignmentNodeId
Step 9: Update Junction Base Demand Values
Objective: Update the Junctions table with calculated base demand values.
Process:
Update Junction Records:
- Update
JunctionstableBaseDemandfield - Join with
JunctionsWithBaseDemandonId = JunctionId - Set
BaseDemand = TotalBaseDemand
- Update
Handle Missing Junctions:
- Junctions not in
JunctionsWithBaseDemandshould haveBaseDemand = 0or remain unchanged - Verify all expected junctions are accounted for
- Junctions not in
SQL Script: sql/update_junction_base_demands.sql (to be created)
Key Logic:
UPDATE Junctions j
SET BaseDemand = COALESCE(jbd.TotalBaseDemand, 0)
FROM JunctionsWithBaseDemand jbd
WHERE j.Id = jbd.JunctionId
Data Flow Summary
Source Shapefiles (shp_v2/)
↓ [Step 0: Spatial joins in QGIS - establish parcel relationships]
CSV Files with Parcel IDs:
- D_BuildingWithParcelId (Buildings → Parcels)
- B_SCwithParcelId (Service Connections → Parcels)
- A_SCwithMainId (Service Connections → Mains)
- C_WMwithParcelId_Closest_5m (Water Meters → Parcels)
↓ [Step 1: Calculate avg demand by type from meter data]
Building Type Average Demands
↓ [Step 2: Assign BaseDemand to each building]
Buildings (with BaseDemand)
↓ [Step 3: Sum by parcel]
ParcelsWithDemand (TotalBaseDemand per parcel)
↓ [Step 4: Divide equally among SCs]
SCwithBaseDemand (demand per service connection)
↓ [Step 5: Sum by main]
MainsWithTotalBaseDemand (demand per main)
↓ [Step 6-7: Determine flow direction, get assignment node]
MainsWithAssignmentNodes (main → downstream junction)
↓ [Step 8: Sum by junction]
JunctionsWithBaseDemand (demand per junction)
↓ [Step 9: Update Junctions table]
Junctions.BaseDemand (final values)
Database Schema Reference
Key Tables (from efcorelibraries)
- Links:
Id,FromNode,ToNode(base class for Pipes, Pumps, Valves) - Pipes: Extends
Link(these are the mains) - LinkHydrs:
Id(Link ID),Flow,ScenarioId,SimUnixTime - Junctions:
Id,BaseDemand(to be updated)
Key Tables (from spatial joins and CSV processing)
- D_BuildingWithParcelId: Buildings with
DBLINK(from spatial join to parcels),BLDGTYPE,BaseDemand(added in Step 2) - B_SCwithParcelId: Service connections with
ASSET_ID,DBLINK_1(from spatial join to parcels) - A_SCwithMainId: Service connections with
ASSET_ID,ASSET_ID_1(main ID, from spatial join to mains) - C_WMwithParcelId_Closest_5m: Water meters with parcel associations (from spatial join, closest 5m method)
- ParcelsWithDemand:
ParcelId,TotalBaseDemand(calculated in Step 3) - SCwithBaseDemand:
ServiceConnectionId,TotalBaseDemand,ParcelId(calculated in Step 4) - MainsWithTotalBaseDemand:
MainId,TotalBaseDemand,ServiceConnectionCount(calculated in Step 5)
Questions Requiring Clarification
Flow Direction Determination
Which SimUnixTime should be used?
- ✅ ANSWERED: Calculate average flow across all timestamps for each main
- Reasoning: Some pipes oscillate between positive/negative flow. Averaging identifies the dominant direction over the entire simulation period, avoiding arbitrary assignment based on final state.
Zero Flow Handling:
- ✅ ANSWERED: When average flow = 0, default to
ToNodeas the downstream node
- ✅ ANSWERED: When average flow = 0, default to
Flow Direction Consistency:
- Not a concern - using average flow across all timestamps identifies dominant direction
Junction Assignment
Multiple Mains to Same Junction:
- ✅ ANSWERED: Sum demands from all mains assigned to the same junction (handled in aggregation)
Junctions Without Assigned Mains:
- ✅ ANSWERED: Leave base demand unchanged (don't set to 0)
Main-to-Junction Mapping:
- ✅ ANSWERED: Only process mains with base demand > 0 that have hydraulic results
Data Validation
Verification Steps:
- ✅ ANSWERED: Not required for now
Missing Data Handling:
- ✅ ANSWERED: Not a concern - tables have default values, only update junctions that receive assignments
SQL Script Execution Order
Execute the following scripts in order:
sql/add_base_demand_column.sql- Add BaseDemand column to buildingssql/create_parcels_with_demand.sql- Create ParcelsWithDemand tablesql/create_sc_with_base_demand.sql- Create SCwithBaseDemand tablesql/create_mains_with_total_base_demand.sql- Create MainsWithTotalBaseDemand tablesql/create_mains_with_assignment_nodes.sql- Create MainsWithAssignmentNodes table ✅sql/create_junctions_with_base_demand.sql- Create JunctionsWithBaseDemand table ✅sql/update_junction_base_demands.sql- Update Junctions table ✅
Implementation Details
Step 6-7: Mains with Assignment Nodes
- Uses
AVG(Flow)aggregation to calculate average flow across all timestamps per main - Filters to
ScenarioId = 1andTotalBaseDemand > 0 - Flow direction logic based on average flow:
AvgFlow > 0: AssignmentNode = ToNode (dominant flow FromNode→ToNode)AvgFlow < 0: AssignmentNode = FromNode (dominant flow ToNode→FromNode)AvgFlow = 0: AssignmentNode = ToNode (default, no dominant direction)
- Rationale: Handles pipes where flow oscillates during simulation by identifying dominant direction
Step 8: Junction Aggregation
- Groups mains by AssignmentNodeId
- Sums TotalBaseDemand for all mains assigned to each junction
- Counts number of mains per junction
Step 9: Junction Updates
- Updates only junctions that have assigned mains
- Leaves other junctions unchanged (preserves default/existing values)
Monthly Processing Automation
Monthly Processing Overview
The workflow needs to be repeated for each month (12 months total) since building type average demands vary by month. A Python automation script has been created to streamline this process.
Automation Script: run_monthly_demand_assignment.py
Purpose: Automate the complete workflow for multiple months by:
- Loading monthly building type averages from CSV
- Updating building base demands for the specified month
- Re-running the complete aggregation chain (parcels → SCs → mains → junctions)
Key Features:
- Processes single month or all months
- Reads monthly averages from
Average of Demand by Month and BLDGTYPE.csv - Executes SQL scripts in correct sequence
- Provides progress feedback and error handling
- Can list available months without processing
Usage Examples:
# List available months
python run_monthly_demand_assignment.py --list-months
# Process a single month
python run_monthly_demand_assignment.py --month January --db-connection "postgresql://user:pass@host/db"
# Process all months
python run_monthly_demand_assignment.py --all-months --db-connection "postgresql://user:pass@host/db"
# Custom paths
python run_monthly_demand_assignment.py --month February \
--db-connection "postgresql://user:pass@host/db" \
--sql-dir sql \
--monthly-data "Average of Demand by Month and BLDGTYPE.csv"
What Changes Per Month:
- Building type average demands (from monthly CSV)
- Building
BaseDemandvalues (updated inD_BuildingWithParcelId) - All downstream aggregations (parcels, SCs, mains, junctions)
What Stays Constant:
- Building-to-parcel relationships
- Service connection-to-parcel relationships
- Service connection-to-main relationships
- Main topology and flow directions (from hydraulic simulation)
Note: The script overwrites intermediate tables each month. If you need to preserve monthly results, you may want to modify the script to create month-specific table names (e.g., JunctionsWithBaseDemand_January).
QA and Intermediate Data Export
QA Overview
For quality assurance and data verification, additional scripts have been created to:
- Split monthly averages into individual CSV files
- Generate monthly mains demand CSVs for comparison
- Insert junction demands into NodeProperties table with scenario IDs
Script 1: Split Monthly Averages (split_monthly_avg_demand.py)
Purpose: Split the combined monthly averages CSV into individual month files for easier review and QA.
Output: Creates files in ./monthly_avg_demand/ directory:
january_2023_avg_demand_by_bldg_type.csvfebruary_2023_avg_demand_by_bldg_type.csv- ... (one per month)
Format: Matches july_2023_avg_demand_by_bldg_type.csv format:
Average of Demand,BLDGTYPE
0.024974261,CHURCH
0.04553726,COMMERCIAL BUILDING
...
Usage:
python split_monthly_avg_demand.py
python split_monthly_avg_demand.py --input "Average of Demand by Month and BLDGTYPE.csv" --output-dir "monthly_avg_demand"
Script 2: Generate Monthly Mains CSVs (generate_monthly_mains_demand.py)
Purpose: Generate CSV files for mains with total base demand for each month, similar to july_2023_mains_with_total_base_demand.csv. This is a pure CSV-to-CSV transformation tool for QA purposes with NO database dependencies.
Output: Creates files in ./mains_with_monthly_base_demand/ directory:
january_2023_mains_with_total_base_demand.csvfebruary_2023_mains_with_total_base_demand.csv- ... (one per month)
Format: Matches july_2023_mains_with_total_base_demand.csv format:
MainId,TotalBaseDemand,ServiceConnectionCount
WTRMN00012,0,1
WTRMN00102,0.0954332411616717,5
...
Process (Pure CSV operations, no database):
- Reads monthly average demand files from
monthly_avg_demand/directory - Reads buildings from
D_BuildingWithParcelId.csvand assigns base demand based on building type - Aggregates by parcel (in-memory)
- Reads service connections from
B_SCwithParcelId.csvand divides parcel demands equally - Reads main IDs from
A_SCwithMainId.csvand sums SC demands by main - Exports to CSV in
mains_with_monthly_base_demand/directory
Usage:
# List available months
python generate_monthly_mains_demand.py --list-months
# Generate for a single month
python generate_monthly_mains_demand.py --month January
# Generate for all months
python generate_monthly_mains_demand.py --all-months
Note: This script does NOT require a database connection. It's designed for QA and verification by processing CSVs directly.
Script 3: Insert Junction Demands from Mains CSVs (insert_junction_demands_from_mains.py)
Purpose: Read monthly mains CSV files and assign their demands to junctions based on hydraulic simulation flow direction, then insert into the NodeProperties table with scenario-specific IDs.
⚠️ Note: This is the CORRECT script to use. Do NOT use insert_junction_demands_to_node_properties.py (which incorrectly re-runs the full aggregation workflow).
Key Details:
- Uses
EN_BASEDEMAND = 1(from EN_NodeProperty enum) - Each month gets its own
ScenarioId(e.g., January=2, February=3, etc.) - Uses
INSERT ... ON CONFLICTto handle updates - CRITICAL: Does NOT re-run the full aggregation workflow; instead reads pre-calculated mains demands from CSV
Process:
- Reads mains demands from monthly CSV files (e.g.,
january_2023_mains_with_total_base_demand.csv) - Queries
Linkstable for topology (FromNode,ToNode) where Link ID matches Main ID - Queries
LinkHydrstable (ScenarioId=1) and calculates average flow for each main - Determines downstream (assignment) node based on flow direction:
- If
AvgFlow > 0: Assign toToNode(flow is FromNode→ToNode) - If
AvgFlow < 0: Assign toFromNode(flow is ToNode→FromNode) - If
AvgFlow = 0: Default toToNode
- If
- Sums demands for all mains assigned to each junction
- Inserts into
NodePropertiestable:NodeId: Junction IDPropertyId: 1 (EN_BASEDEMAND)PropertyValue: Calculated base demand (sum of assigned mains)ScenarioId: Month-specific scenario ID
Usage:
# Process all months starting with scenario ID 2
python insert_junction_demands_from_mains.py --all-months --scenario-start 2 --db-connection "postgresql://user:pass@host/db"
# Process a single month with specific scenario ID
python insert_junction_demands_from_mains.py --month January --scenario-id 2 --db-connection "postgresql://user:pass@host/db"
# List available months
python insert_junction_demands_from_mains.py --list-months
Scenario ID Mapping:
- ScenarioId 1: Hydraulic simulation results (existing, used for flow direction)
- ScenarioId 2+: Monthly base demand scenarios (one per month)
- Default starting scenario ID: 2 (can be changed with
--scenario-start)
Database Schema:
NodeProperties (
Id INT PRIMARY KEY (auto-generated),
NodeId VARCHAR(31) NOT NULL,
PropertyId INT NOT NULL, -- 1 = EN_BASEDEMAND
PropertyValue DOUBLE NOT NULL,
CreatedAt DATETIME NOT NULL, -- Set to CURRENT_TIMESTAMP on insert
ModifiedAt DATETIME, -- Set to CURRENT_TIMESTAMP on update
ScenarioId INT -- NULL for defaults, non-null for scenarios
)
Important Notes:
- This script reads mains demands from CSV files generated by
generate_monthly_mains_demand.py. Make sure those CSV files exist in the./mains_with_monthly_base_demand/directory before running this script. - The script properly handles
CreatedAtandModifiedAttimestamps to avoid NULL constraint violations. - Skipped mains (due to missing topology or flow data) are written to a report file for post-insert analysis.
Complete Monthly Processing Workflow
Recommended execution order:
Step 1: Prepare Monthly Data Files
Split monthly averages (one-time setup):
python split_monthly_avg_demand.pyOutput:
./monthly_avg_demand/*_avg_demand_by_bldg_type.csvGenerate monthly mains CSVs (CSV-only QA tool, no database required):
python generate_monthly_mains_demand.py --all-monthsOutput:
./mains_with_monthly_base_demand/*_mains_with_total_base_demand.csv
Step 2: Create Monthly Scenarios in Database
- Create scenario records (one-time setup):This creates 12 scenario records (IDs 2-13) for each month.
psql -U postgres dnv_10_23_2025 -f sql/create_monthly_scenarios.sql
Step 3: Insert Junction Demands to Database
Insert junction demands to NodeProperties:
python insert_junction_demands_from_mains.py --all-months --scenario-start 2 --db-connection "postgresql://postgres:gq010102@localhost/dnv_10_23_2025"This reads mains CSVs, queries Links/LinkHydrs for flow direction, assigns to junctions, and inserts into NodeProperties.
Output: Skipped mains reports are written to
./skipped_mains_reports/for post-insert analysis.
Final Output Files
CSV Files (for QA/verification):
./monthly_avg_demand/*_avg_demand_by_bldg_type.csv- Building type averages per month (11 building types)./mains_with_monthly_base_demand/*_mains_with_total_base_demand.csv- Mains demands per month (~2800 mains)./skipped_mains_reports/*_skipped_mains.csv- Mains that were skipped during junction assignment (with reasons: no_topology or no_flow_data)
Database Tables (production data):
Scenariostable: 12 new scenario records (IDs 2-13) for monthly demand patternsNodePropertiestable: Junction base demands for each scenario (one set per month), with CreatedAt and ModifiedAt timestamps properly set