@@ -406,8 +406,51 @@ def __init__(self, name: str = ""):
406406 self ._name = name
407407
408408 def _get_outlet (self , ref : Union [str , Any ]) -> Any :
409- """Get outlet stream from equipment reference (name or object)."""
409+ """
410+ Get outlet stream from equipment reference (name or object).
411+
412+ Supports dot notation for selecting specific outlets from separators:
413+ - 'separator.gas' or 'separator.vapor' - gas/vapor outlet
414+ - 'separator.liquid' - liquid outlet (2-phase separator)
415+ - 'separator.oil' - oil outlet (3-phase separator)
416+ - 'separator.water' or 'separator.aqueous' - water outlet (3-phase separator)
417+
418+ Examples:
419+ >>> builder.add_compressor('comp', 'sep.gas', pressure=100)
420+ >>> builder.add_pump('pump', 'sep.oil', pressure=50)
421+ """
410422 if isinstance (ref , str ):
423+ # Check for dot notation (e.g., 'separator.gas')
424+ if '.' in ref :
425+ parts = ref .split ('.' , 1 )
426+ equip_name = parts [0 ]
427+ outlet_type = parts [1 ].lower ()
428+
429+ equip = self .equipment .get (equip_name )
430+ if equip is None :
431+ raise ValueError (f"Equipment '{ equip_name } ' not found" )
432+
433+ # Map outlet type to method
434+ outlet_methods = {
435+ 'gas' : ['getGasOutStream' , 'getOutletStream' ],
436+ 'vapor' : ['getGasOutStream' , 'getOutletStream' ],
437+ 'liquid' : ['getLiquidOutStream' , 'getOilOutStream' ],
438+ 'oil' : ['getOilOutStream' , 'getLiquidOutStream' ],
439+ 'water' : ['getWaterOutStream' , 'getAqueousOutStream' ],
440+ 'aqueous' : ['getWaterOutStream' , 'getAqueousOutStream' ],
441+ }
442+
443+ if outlet_type not in outlet_methods :
444+ raise ValueError (f"Unknown outlet type '{ outlet_type } '. "
445+ f"Valid types: { list (outlet_methods .keys ())} " )
446+
447+ for method_name in outlet_methods [outlet_type ]:
448+ if hasattr (equip , method_name ):
449+ return getattr (equip , method_name )()
450+
451+ raise ValueError (f"Equipment '{ equip_name } ' does not have a '{ outlet_type } ' outlet" )
452+
453+ # Standard lookup without dot notation
411454 equip = self .equipment .get (ref )
412455 if equip is None :
413456 raise ValueError (f"Equipment '{ ref } ' not found" )
@@ -773,6 +816,164 @@ def get(self, name: str) -> Any:
773816 def get_process (self ) -> Any :
774817 """Get the underlying ProcessSystem object."""
775818 return self .process
819+
820+ def results_json (self ) -> Dict [str , Any ]:
821+ """
822+ Get simulation results as a JSON-compatible dictionary.
823+
824+ Returns:
825+ Dictionary with all process results.
826+
827+ Example:
828+ >>> process = ProcessBuilder("Test").add_stream(...).run()
829+ >>> results = process.results_json()
830+ >>> print(json.dumps(results, indent=2))
831+ """
832+ json_report = str (self .process .getReport_json ())
833+ return json .loads (json_report )
834+
835+ def results_dataframe (self ) -> pd .DataFrame :
836+ """
837+ Get simulation results as a pandas DataFrame.
838+
839+ Returns:
840+ DataFrame with equipment results including temperatures,
841+ pressures, flow rates, power, and duties.
842+
843+ Example:
844+ >>> process = ProcessBuilder("Test").add_stream(...).run()
845+ >>> df = process.results_dataframe()
846+ >>> print(df)
847+ """
848+ rows = []
849+ for name , eq in self .equipment .items ():
850+ row = {'Equipment' : name }
851+
852+ # Get outlet stream properties
853+ out_stream = None
854+ if hasattr (eq , 'getOutletStream' ):
855+ out_stream = eq .getOutletStream ()
856+ elif hasattr (eq , 'getOutStream' ):
857+ out_stream = eq .getOutStream ()
858+ elif hasattr (eq , 'getGasOutStream' ):
859+ out_stream = eq .getGasOutStream ()
860+
861+ if out_stream :
862+ try :
863+ row ['T_out (°C)' ] = round (out_stream .getTemperature () - 273.15 , 2 )
864+ except :
865+ pass
866+ try :
867+ row ['P_out (bara)' ] = round (out_stream .getPressure (), 2 )
868+ except :
869+ pass
870+ try :
871+ row ['Flow (kg/hr)' ] = round (out_stream .getFlowRate ('kg/hr' ), 1 )
872+ except :
873+ pass
874+
875+ # Get power/duty
876+ if hasattr (eq , 'getPower' ):
877+ try :
878+ row ['Power (kW)' ] = round (eq .getPower () / 1e3 , 2 )
879+ except :
880+ pass
881+ if hasattr (eq , 'getDuty' ):
882+ try :
883+ row ['Duty (kW)' ] = round (eq .getDuty () / 1e3 , 2 )
884+ except :
885+ pass
886+
887+ rows .append (row )
888+
889+ return pd .DataFrame (rows )
890+
891+ def print_results (self ) -> 'ProcessBuilder' :
892+ """
893+ Print a formatted summary of simulation results.
894+
895+ Returns:
896+ Self for method chaining.
897+
898+ Example:
899+ >>> (ProcessBuilder("Test")
900+ ... .add_stream('inlet', feed)
901+ ... .add_compressor('comp1', 'inlet', pressure=100)
902+ ... .run()
903+ ... .print_results())
904+ """
905+ print (f"\n { '=' * 60 } " )
906+ print (f"Process Results: { self ._name } " )
907+ print (f"{ '=' * 60 } \n " )
908+
909+ for name , eq in self .equipment .items ():
910+ print (f"📦 { name } " )
911+
912+ # Get outlet stream properties
913+ out_stream = None
914+ if hasattr (eq , 'getOutletStream' ):
915+ out_stream = eq .getOutletStream ()
916+ elif hasattr (eq , 'getOutStream' ):
917+ out_stream = eq .getOutStream ()
918+ elif hasattr (eq , 'getGasOutStream' ):
919+ out_stream = eq .getGasOutStream ()
920+
921+ if out_stream :
922+ try :
923+ print (f" Temperature: { out_stream .getTemperature () - 273.15 :.1f} °C" )
924+ except :
925+ pass
926+ try :
927+ print (f" Pressure: { out_stream .getPressure ():.1f} bara" )
928+ except :
929+ pass
930+ try :
931+ print (f" Flow rate: { out_stream .getFlowRate ('kg/hr' ):.0f} kg/hr" )
932+ except :
933+ pass
934+
935+ if hasattr (eq , 'getPower' ):
936+ try :
937+ print (f" Power: { eq .getPower ()/ 1e3 :.2f} kW" )
938+ except :
939+ pass
940+ if hasattr (eq , 'getDuty' ):
941+ try :
942+ print (f" Duty: { eq .getDuty ()/ 1e3 :.2f} kW" )
943+ except :
944+ pass
945+ print ()
946+
947+ return self
948+
949+ def save_results (self , filename : str , format : str = 'json' ) -> 'ProcessBuilder' :
950+ """
951+ Save simulation results to a file.
952+
953+ Args:
954+ filename: Output file path.
955+ format: Output format - 'json', 'csv', or 'excel'.
956+
957+ Returns:
958+ Self for method chaining.
959+
960+ Example:
961+ >>> process.run().save_results('results.json')
962+ >>> process.save_results('results.csv', format='csv')
963+ >>> process.save_results('results.xlsx', format='excel')
964+ """
965+ if format == 'json' :
966+ with open (filename , 'w' ) as f :
967+ json .dump (self .results_json (), f , indent = 2 )
968+ elif format == 'csv' :
969+ self .results_dataframe ().to_csv (filename , index = False )
970+ elif format == 'excel' :
971+ self .results_dataframe ().to_excel (filename , index = False )
972+ else :
973+ raise ValueError (f"Unknown format: { format } . Use 'json', 'csv', or 'excel'." )
974+
975+ print (f"Results saved to { filename } " )
976+ return self
776977
777978
778979def _add_to_process (equipment : Any , process : Any = None ) -> None :
0 commit comments