@@ -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
1405914298class epanetapi:
1406014299 """
0 commit comments