1+ import click
12import pandas as pd
23import pyhecdss as dss
34
@@ -6,6 +7,25 @@ def get_bpart_pattern():
67 return '[0-9]+|BBID'
78
89
10+ def bparts_to_pattern (bparts ):
11+ """
12+ Convert a list of B-part strings to a regex alternation pattern for DSS path matching.
13+ Each B-part is wrapped in ^ and $ anchors so that e.g. '1' does not match '11' or '100'.
14+ (pyhecdss uses pandas str.match which anchors the start but not the end.)
15+
16+ Parameters
17+ ----------
18+ bparts : list of str
19+ List of B-part identifiers, e.g. ['BBID', '12345', '67890']
20+
21+ Returns
22+ -------
23+ str
24+ Regex alternation string with full anchors, e.g. '^1$|^100$|^200$'
25+ """
26+ return '|' .join ('^' + b + '$' for b in bparts )
27+
28+
929def get_dss_data (file , path_pattern ):
1030 """
1131 returns a data frame of all the time series that match the path_pattern
@@ -71,3 +91,79 @@ def calculate_netcd(div_flows, drain_flows, seep_flows=None):
7191 return sum (div_flows ) - sum (drain_flows )
7292 else :
7393 return sum (div_flows ) + sum (seep_flows ) - sum (drain_flows )
94+
95+
96+ @click .command (name = "calc-netcd" )
97+ @click .argument ("dssfile" , type = click .Path (exists = True ))
98+ @click .option (
99+ "--bpart" ,
100+ multiple = True ,
101+ help = "B-part to include (repeatable, e.g. --bpart BBID --bpart 12345)" ,
102+ )
103+ @click .option (
104+ "--bpart-file" ,
105+ type = click .Path (exists = True ),
106+ default = None ,
107+ help = "Text file with one B-part per line" ,
108+ )
109+ @click .option (
110+ "--no-seepage" ,
111+ is_flag = True ,
112+ default = False ,
113+ help = "Exclude seepage flows (NetCD = DIV - DRAIN only)" ,
114+ )
115+ @click .option (
116+ "--epart" ,
117+ default = "1DAY" ,
118+ show_default = True ,
119+ help = "Time interval E-part (e.g. 1DAY, 1MON)" ,
120+ )
121+ @click .option (
122+ "-o" ,
123+ "--output" ,
124+ default = "netcd.csv" ,
125+ show_default = True ,
126+ help = "Output CSV file path" ,
127+ )
128+ def calc_netcd_cmd (dssfile , bpart , bpart_file , no_seepage , epart , output ):
129+ """Calculate aggregated Net Channel Depletion (NetCD) from a DSS file.
130+
131+ NetCD = DIV-FLOW - DRAIN-FLOW + SEEP-FLOW, summed over all matching B-parts.
132+
133+ B-parts can be supplied via --bpart (repeatable) and/or --bpart-file (one per line).
134+ If neither is given, the default pattern matches all numeric node IDs and BBID.
135+ """
136+ bparts = list (bpart )
137+ if bpart_file :
138+ with open (bpart_file ) as f :
139+ bparts .extend (line .strip () for line in f if line .strip ())
140+
141+ if bparts :
142+ bpart_pattern = bparts_to_pattern (bparts )
143+ else :
144+ bpart_pattern = get_bpart_pattern ()
145+
146+ try :
147+ div_flows = get_dss_data (dssfile , f"//({ bpart_pattern } )/DIV-FLOW//{ epart } //" )
148+ except Exception as e :
149+ raise click .ClickException (f"No DIV-FLOW data found for the specified B-parts: { e } " )
150+
151+ try :
152+ drain_flows = get_dss_data (dssfile , f"//({ bpart_pattern } )/DRAIN-FLOW//{ epart } //" )
153+ except Exception as e :
154+ raise click .ClickException (f"No DRAIN-FLOW data found for the specified B-parts: { e } " )
155+
156+ seep_flows = None
157+ if not no_seepage :
158+ try :
159+ seep_flows = get_dss_data (dssfile , f"//({ bpart_pattern } )/SEEP-FLOW//{ epart } //" )
160+ except Exception :
161+ click .echo (
162+ "Warning: No SEEP-FLOW data found for the specified B-parts. "
163+ "Proceeding without seepage (NetCD = DIV - DRAIN)." ,
164+ err = True ,
165+ )
166+
167+ netcd = calculate_netcd (div_flows , drain_flows , seep_flows )
168+ netcd .to_csv (output , header = ["NETCD" ])
169+ click .echo (f"NetCD written to { output } " )
0 commit comments