Skip to content

Commit 05495d4

Browse files
authored
Merge pull request #78 from ichrys03/msxstatistics
#77 + example
2 parents 84e939d + c07e56c commit 05495d4

File tree

3 files changed

+350
-0
lines changed

3 files changed

+350
-0
lines changed

README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,92 @@ If you want to contribute, please check out our [Code of Conduct](https://github
532532
| useHydraulicFile |Uses the contents of the specified file as the current binary hydraulics file
533533
| writeLineInReportFile |Writes a line of text to the EPANET report file
534534
| writeReport |Writes a formatted text report on simulation results to the Report file
535+
| <b> MSX Functions </b>
536+
| loadMSXFile |Opens the EPANET-MSX toolkit system|
537+
| unloadMSX |Closes the EPANET-MSX toolkit system|
538+
| addMSXPattern |Adds a new, empty MSX source time pattern to the project|
539+
| writeMSXFile |Write a new MSX file|
540+
| initializeMSXQualityAnalysis |Initializes the MSX system before solving for water quality results in step-wise fashion|
541+
| getMethods |Returns all methods of epanet|
542+
| getMSXComputedQualitySpecie |Retrieves the quality values for specific specie (e.g getMSXComputedQualitySpecie('CL2'))|
543+
| getMSXComputedLinkQualitySpecie |Returns the link quality for specific specie|
544+
| getMSXComputedNodeQualitySpecie |Returns the node quality for specific specie|
545+
| getMSXComputedQualityNode |Retrieves the concentration of a chemical species at a specific node of the network at the current simulation time step.|
546+
| stepMSXQualityAnalysisTimeLeft |Advances the water quality solution through a single water quality time step when performing a step-wise simulation|
547+
| saveMSXFile |Saves the data associated with the current MSX project into a new MSX input file|
548+
| saveMSXQualityFile |Saves water quality results computed for each node, link and reporting time period to a named binary file|
549+
| getMSXSourcePatternIndex |Retrieves the value of all node source pattern index|
550+
| getMSXLinkInitqualValue |Retrieves the initial concentration of chemical species assigned to links of the pipe network|
551+
| getMSXNodeInitqualValue |Retrieves the initial concentration of chemical species assigned to nodes|
552+
| getMSXSourceLevel |Retrieves the value of all nodes source level|
553+
| getMSXSourceType |Retrieves the value of all node source type|
554+
| getMSXSourceNodeNameID |Retrieves the ID label of all nodes|
555+
| getMSXSpeciesCount |Retrieves the number of species|
556+
| getMSXSpeciesNameID |Retrieves the species IDs|
557+
| getMSXSpeciesIndex |Retrieves the indices of species|
558+
| getMSXSpeciesType |Retrieves the type of all species (BULK/WALL)|
559+
| getMSXSpeciesUnits |Retrieves the species mass units|
560+
| getMSXSpeciesATOL |Retrieves the atol|
561+
| getMSXSpeciesRTOL |Retrieves the rtol|
562+
| getMSXSpeciesConcentration |Retrieves the concentration of chemical species for nodes and links|
563+
| getMSXConstantsCount |Retrieves the number of constants|
564+
| getMSXConstantsNameID |Retrieves the ID name of constants (given its internal index number)|
565+
| getMSXConstantsIndex |Retrieves the internal index number of constants (given its ID name)|
566+
| getMSXPattern |Retrieves the multiplier factor for all patterns and all times|
567+
| getMSXPatternsCount |Retrieves the number of patterns|
568+
| getMSXPatternValue |Retrieves the multiplier at a specific time period for a given source time pattern|
569+
| getMSXPatternsNameID |Retrieves the patterns IDs|
570+
| getMSXPatternsIndex |Retrieves the indices of patterns|
571+
| getMSXPatternsLengths |Retrieves the number of time periods in all or some patterns|
572+
| getMSXParametersCount |Retrieves the number of parameters|
573+
| getMSXParametersNameID |Retrieves the ID name of parameters|
574+
| getMSXParametersIndex |Retrieves the indices of parameters|
575+
| getMSXParametersPipesValue |Retrieves the value of reaction parameters for pipes|
576+
| getMSXParametersTanksValue |Retrieves the value of reaction parameters for tanks|
577+
| solveMSXCompleteHydraulics |Solves for system hydraulics over the entire simulation period saving results to an internal scratch file|
578+
| solveMSXCompleteQuality |Solves for water quality over the entire simulation period and saves the results to an internal scratch file|
579+
| getMSXError |Returns the text for an error message given its error code|
580+
| getMSXOptions |Retrieves all the msx option parameters|
581+
| getMSXTimeStep |Retrieves the time step|
582+
| getMSXRateUnits |Retrieves the rate/time units (SEC/MIN/HR/DAY)|
583+
| getMSXAreaUnits |Retrieves the area units (FT2/M2/CM2)|
584+
| getMSXCompiler |Retrieves the compiler (NONE/VC/GC)|
585+
| getMSXCoupling |Retrieves the coupling (FULL/NONE)|
586+
| getMSXAtol |Retrieves the absolute concentration tolerance|
587+
| getMSXRtol |Retrieves the relative concentration tolerance|
588+
| getMSXComputedQualitySpecie |Retrieves the quality values for specific specie (e.g getMSXComputedQualitySpecie(['CL2']))|
589+
| getMSXEquationsPipes |Retrieves the species dynamics in pipes|
590+
| getMSXEquationsTanks |Retrieves the species dynamics in tanks|
591+
| getMSXEquationsTerms |Retrieves the species dynamics in terms|
592+
| setMSXAreaUnitsCM2 |Sets area units to CM2|
593+
| setMSXAreaUnitsFT2 |Sets area units to FT2|
594+
| setMSXAreaUnitsM2 |Sets area units to M2|
595+
| setMSXAtol |Sets the value of Atol|
596+
| setMSXRtol |Sets the value of Rtol|
597+
| setMSXCompilerGC |Sets compilet to GC|
598+
| setMSXCompilerNONE |Sets compiler to None|
599+
| setMSXCompilerVC |Sets compiler to VC|
600+
| setMSXCouplingFULL |Sets coupling option to FULL|
601+
| setMSXCouplingNONE |Sets coupling option to NONE|
602+
| setMSXRateUnitsDAY |Sets rate units to DAY|
603+
| setMSXRateUnitsHR |Sets rate units to HR|
604+
| setMSXRateUnitsMIN |Sets rate units to MIN|
605+
| setMSXRateUnitsSEC |Sets rate units to SEC|
606+
| setMSXSolverEUL |Sets solver to EUL (standard Euler integrator)|
607+
| setMSXSolverRK5 |Sets solver to RK5 (Runge-Kutta 5th order integrator)|
608+
| setMSXSolverROS2 |Sets solver to ROS2 (2nd order Rosenbrock integrator)|
609+
| setMSXTimeStep |Sets time step|
610+
| setMSXPatternValue |Assigns a new value to the multiplier for a specific time period in a given MSX source time pattern|
611+
| setMSXPattern |Sets all of the multiplier factors for a specific time pattern|
612+
| setMSXParametersPipesValue |Assigns a value to a particular reaction parameter for given pipes|
613+
| setMSXParametersTanksValue |Assigns a value to a particular reaction parameter for given tanks|
614+
| setMSXConstantsValue |Assigns a new value to a specific reaction constant|
615+
| setMSXLinkInitqualValue |Assigns an initial concentration of chemical species to links|
616+
| setMSXNodeInitqualValue |Assigns an initial concentration of chemical species to nodes|
617+
| setMSXSources |Sets the attributes of an external source of a particular chemical species to a specific node of the pipe network|
618+
| useMSXHydraulicFile |Uses a previously saved EPANET hydraulics file as the source of hydraulic information|
619+
| exportMSXts |Exports multi-species water-quality time-series results to an Excel workbook—one sheet per species|
620+
| exportMSXstatistics |Summarizes min, max, and average values for each node in an Excel file|
535621

536622
## List of MSX Functions
537623
|Function|Description|

epyt/epanet.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14055,6 +14055,245 @@ def plotMSXSpeciesLinkConcentration(self, *args):
1405514055
plt.legend()
1405614056
plt.show()
1405714057

14058+
def exportMSXts(self, results, output_file='computedtoexcel.xlsx', selected_nodes=None,
14059+
selected_species=None,
14060+
header=True):
14061+
"""
14062+
Exports multi-species water-quality time-series results (from an EPANET-MSX
14063+
simulation) to an Excel workbook—one sheet per species.
14064+
14065+
Parameters:
14066+
----------
14067+
results : obj
14068+
A results object returned by `getMSXComputedQualityNode`.
14069+
It must expose ``Time`` (1-D array-like) and ``Quality``
14070+
output_file : str, default ``"computedtoexcel.xlsx"``
14071+
Name (or path) for the Excel file to create. “.xlsx” is appended
14072+
automatically when omitted.
14073+
selected_nodes : list[str | int] | None, default ``None``
14074+
Node IDs or zero-based node indices to include.
14075+
• ``None`` → export **all** nodes.
14076+
• Strings → treated as node IDs.
14077+
• Integers → treated as node indices.
14078+
selected_species : list[str | int] | None, default ``None``
14079+
Species names or zero-based species indices to include.
14080+
Same ID / index rules as *selected_nodes*.
14081+
header : bool, default ``True``
14082+
Write column headers (“NODE INDEX”, “NODE ID”, time steps …).
14083+
If ``False``, headers are suppressed and the first data row is
14084+
removed—useful for appending to an existing sheet.
14085+
14086+
Simple Example with all nodes and species:
14087+
G = epanet("net2-cl2.inp")
14088+
G.loadMSXFile("net2-cl2.msx")
14089+
MSX_comp = G.getMSXComputedQualityNode()
14090+
G.exportMSXts(MSX_comp, "net2")
14091+
G.exportMSXstatistics("net2","summarynet2")
14092+
14093+
Advanced Examples:
14094+
G = epanet("net2-cl2.inp")
14095+
G.loadMSXFile("net2-cl2.msx")
14096+
14097+
# Run MSX simulation and grab node-quality results
14098+
msx_results = G.getMSXComputedQualityNode()
14099+
14100+
# 1) Export every species for every node (default behaviour)
14101+
G.exportMSXts(msx_results, "net2_full.xlsx")
14102+
14103+
# 2) Export only chlorine for two specific nodes, keep headers
14104+
G.exportMSXts(
14105+
MSX_comp,
14106+
output_file="chlorine_subset.xlsx",
14107+
selected_nodes=["10", "15"], #select nodes by their id
14108+
selected_species=["CL2"]
14109+
)
14110+
14111+
14112+
G.exportMSXts(
14113+
MSX_comp,
14114+
output_file="chlorine_subset1.xlsx",
14115+
selected_nodes=[9, 14], #select node by their index
14116+
selected_species=["CL2"]
14117+
)
14118+
14119+
# 3) Export species index 0 for nodes 0-4, omit headers
14120+
G.exportMSXts(
14121+
msx_results,
14122+
"first_species_nodes0to4.xlsx",
14123+
selected_nodes=list(range(5)),
14124+
selected_species=[0], #select specie by its index
14125+
header=False
14126+
14127+
"""
14128+
if not output_file.endswith('.xlsx'):
14129+
output_file += '.xlsx'
14130+
14131+
if not hasattr(results, 'Time') or not hasattr(results, 'Quality'):
14132+
raise ValueError("Simulation results are not properly initialized or run.")
14133+
14134+
time_data = results.Time
14135+
species_list = self.getMSXSpeciesNameID()
14136+
14137+
node_ids = self.getNodeNameID()
14138+
node_indices = list(range(len(node_ids)))
14139+
14140+
if selected_nodes:
14141+
selected_node_indices = []
14142+
for node in selected_nodes:
14143+
if isinstance(node, str): # Node ID
14144+
if node in node_ids:
14145+
selected_node_indices.append(node_ids.index(node))
14146+
else:
14147+
raise ValueError(f"Node ID '{node}' not found.")
14148+
elif isinstance(node, int): # Node index
14149+
if 0 <= node < len(node_ids):
14150+
selected_node_indices.append(node)
14151+
else:
14152+
raise ValueError(f"Node index '{node}' is out of range.")
14153+
else:
14154+
raise ValueError(f"Invalid node identifier: {node}")
14155+
else:
14156+
selected_node_indices = node_indices
14157+
14158+
if selected_species:
14159+
selected_species_indices = []
14160+
for species in selected_species:
14161+
if isinstance(species, str): # Species name
14162+
if species in species_list:
14163+
selected_species_indices.append(species_list.index(species))
14164+
else:
14165+
raise ValueError(f"Species name '{species}' not found.")
14166+
elif isinstance(species, int): # Species index
14167+
if 0 <= species < len(species_list):
14168+
selected_species_indices.append(species)
14169+
else:
14170+
raise ValueError(f"Species index '{species}' is out of range.")
14171+
else:
14172+
raise ValueError(f"Invalid species identifier: {species}")
14173+
else:
14174+
selected_species_indices = list(range(len(species_list)))
14175+
14176+
with pd.ExcelWriter(output_file, engine='xlsxwriter') as writer:
14177+
node_keys = list(results.Quality.keys())
14178+
14179+
for species_index in selected_species_indices:
14180+
species_name = species_list[species_index]
14181+
species_data = []
14182+
14183+
for node_index in selected_node_indices:
14184+
node_key = node_keys[node_index]
14185+
quality_data = np.array(results.Quality[node_key])
14186+
14187+
# If quality_data has an extra leading dimension
14188+
if quality_data.ndim == 3 and quality_data.shape[0] == 1:
14189+
quality_data = quality_data[0]
14190+
14191+
num_timesteps = len(time_data)
14192+
num_species = len(species_list)
14193+
expected_shape = (num_timesteps, num_species)
14194+
14195+
if quality_data.shape != expected_shape:
14196+
raise ValueError(
14197+
f"Node {node_key}: quality_data does not match expected shape {expected_shape}. "
14198+
f"Actual shape: {quality_data.shape}"
14199+
)
14200+
species_data.append(quality_data[:, species_index])
14201+
14202+
species_data_array = np.array(species_data)
14203+
14204+
df = pd.DataFrame(species_data_array, columns=time_data,
14205+
index=[node_ids[i] for i in selected_node_indices])
14206+
df.insert(0, 'NODE INDEX', [node_indices[i] for i in selected_node_indices])
14207+
df.insert(1, 'NODE ID', [node_ids[i] for i in selected_node_indices])
14208+
14209+
# If header is False, remove the first data row from df
14210+
if not header and len(df) > 0:
14211+
df = df.iloc[1:].copy()
14212+
14213+
sheet_name = f"{species_name}"
14214+
# If header=False, no column headers will be written to the Excel sheet.
14215+
df.to_excel(writer, index=False, sheet_name=sheet_name, header=header)
14216+
14217+
worksheet = writer.sheets[sheet_name]
14218+
worksheet.set_column('A:A', 13.0)
14219+
14220+
print(f"Data successfully written to {output_file}")
14221+
14222+
def exportMSXstatistics(self,input_path, output_path="summary_output.xlsx", nodeids=True, nodeindex=True):
14223+
"""
14224+
Summarizes min, max, and average values for each node in an Excel file with a specific structure.
14225+
14226+
Parameters:
14227+
input_path (str): Path to the input Excel file.
14228+
output_path (str): Path to save the output summary Excel file.
14229+
nodeids (bool): Include node IDs (from column 1) in the summary.
14230+
nodeindex (bool): Include node index (from column 0) in the summary.
14231+
14232+
Simple Example with all nodes and species:
14233+
G = epanet("net2-cl2.inp")
14234+
G.loadMSXFile("net2-cl2.msx")
14235+
MSX_comp = G.getMSXComputedQualityNode()
14236+
G.exportMSXts(MSX_comp, "net2")
14237+
G.exportMSXstatistics("net2","summarynet2")
14238+
14239+
# Example usage:
14240+
exportMSXstatistics("outexcel3.xlsx","summary_output1.xlsx", nodeids=True, nodeindex=False) # Only node IDs
14241+
exportMSXstatistics("outexcel3.xlsx", "summary_output2.xlsx",nodeids=False, nodeindex=True) # Only node indices
14242+
exportMSXstatistics("outexcel3.xlsx","summary_output3.xlsx", nodeids=True, nodeindex=True) # Both
14243+
"""
14244+
if not input_path.endswith('.xlsx'):
14245+
input_path += '.xlsx'
14246+
14247+
if not output_path.endswith('.xlsx'):
14248+
output_path += '.xlsx'
14249+
xls = pd.ExcelFile(input_path)
14250+
output_data = {}
14251+
14252+
for sheet in xls.sheet_names:
14253+
df = xls.parse(sheet, header=None)
14254+
14255+
data = df.iloc[1:].reset_index(drop=True)
14256+
14257+
summary_rows = []
14258+
14259+
for _, row in data.iterrows():
14260+
index = int(row[0])
14261+
node_id = str(row[1])
14262+
values = pd.to_numeric(row[2:], errors='coerce').dropna()
14263+
14264+
if values.empty:
14265+
continue
14266+
14267+
summary = {
14268+
'Min': values.min(),
14269+
'Max': values.max(),
14270+
'Mean': values.mean()
14271+
}
14272+
14273+
if nodeids:
14274+
summary['NodeID'] = node_id
14275+
if nodeindex:
14276+
summary['NodeIndex'] = index
14277+
14278+
ordered_summary = {}
14279+
if nodeids:
14280+
ordered_summary['NodeID'] = summary['NodeID']
14281+
if nodeindex:
14282+
ordered_summary['NodeIndex'] = summary['NodeIndex']
14283+
ordered_summary['Min'] = summary['Min']
14284+
ordered_summary['Max'] = summary['Max']
14285+
ordered_summary['Mean'] = summary['Mean']
14286+
14287+
summary_rows.append(ordered_summary)
14288+
14289+
output_data[sheet] = pd.DataFrame(summary_rows)
14290+
14291+
with pd.ExcelWriter(output_path) as writer:
14292+
for sheet_name, df in output_data.items():
14293+
df.to_excel(writer, sheet_name=sheet_name, index=False)
14294+
14295+
print(f"Summary saved to: {output_path}")
14296+
1405814297

1405914298
class epanetapi:
1406014299
"""
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from epyt import epanet
2+
3+
4+
5+
G = epanet("net2-cl2.inp")
6+
G.loadMSXFile("net2-cl2.msx")
7+
MSX_comp = G.getMSXComputedQualityNode()
8+
G.exportMSXts(MSX_comp, "net2")
9+
G.exportMSXstatistics("net2","summarynet2")
10+
11+
12+
G.exportMSXts(
13+
MSX_comp,
14+
output_file="chlorine_subset.xlsx",
15+
selected_nodes=["10", "15"],
16+
selected_species=["CL2"]
17+
)
18+
19+
20+
G.exportMSXts(
21+
MSX_comp,
22+
output_file="chlorine_subset1.xlsx",
23+
selected_nodes=[9, 14],
24+
selected_species=[0]
25+
)

0 commit comments

Comments
 (0)