2222import thermosteam as tmo
2323from scipy .integrate import romb
2424from scipy .optimize import minimize
25+ from warnings import warn
26+ from biosteam import report
27+ import openpyxl
28+ import json
2529
2630__all__ = (
2731 'BenchmarkModel' ,
3236 'register' ,
3337 'all_systems' ,
3438 'test_convergence' ,
39+ 'save_stream_tables_and_specifications' ,
40+ 'save_simulation_errors' ,
3541 'Tracker'
3642)
3743
@@ -229,10 +235,17 @@ def plot_monte_carlo(system='alcohol_wide_flash'):
229235system_tickmarks = {}
230236system_labels = {}
231237system_yticks = {}
238+ try :
239+ with open ('system_stages.json' ) as f : system_stages = json .load (f )
240+ except :
241+ system_stages = {}
232242
233243def register (name , title , time , tickmarks , label , yticks = None ):
234244 f = getattr (systems , 'create_system_' + name , None ) or getattr (systems , 'create_' + name + '_system' )
235245 if yticks is None : yticks = [(- 10 , - 5 , 0 , 5 ), (- 10 , - 5 , 0 , 5 )]
246+ if name not in system_stages :
247+ sys = f (None )
248+ system_stages [name ] = len (sys .stages )
236249 all_systems [name ] = f
237250 system_titles [name ] = title
238251 system_convergence_times [name ] = time
@@ -247,7 +260,8 @@ def register(name, title, time, tickmarks, label, yticks=None):
247260register (
248261 'acetic_acid_simple' , 'Subsystem' ,
249262 40 , [0 , 8 , 16 , 24 , 32 ], 'AcOH\n partial\n dewatering' ,
250- [(- 15 , - 10 , - 5 , 0 , 5 , 10 ), (- 15 , - 10 , - 5 , 0 , 5 , 10 )],
263+ [(- 15 , - 10 , - 5 , 0 , 5 ), (- 15 , - 10 , - 5 , 0 , 5 )],
264+ # [(-15, -10, -5, 0, 5, 10), (-15, -10, -5, 0, 5, 10)],
251265)
252266register (
253267 'acetic_acid_complex_decoupled' , 'Shortcut system' ,
@@ -256,8 +270,9 @@ def register(name, title, time, tickmarks, label, yticks=None):
256270)
257271register (
258272 'acetic_acid_complex' , 'Rigorous system' ,
259- 500 , [0 , 60 , 120 , 180 , 240 ], 'AcOH\n industrial\n dewatering' ,
260- [(- 5 , - 2.5 , 0 , 2.5 , 5 ), (- 8 , - 5 , - 2 , 1 , 4 )],
273+ 240 , [0 , 60 , 120 , 180 , 240 ], 'AcOH\n industrial\n dewatering' ,
274+ [(- 15 , - 10 , - 5 , 0 , 5 ), (- 15 , - 10 , - 5 , 0 , 5 )],
275+ # [(-5, -2.5, 0, 2.5, 5), (-8, -5, -2, 1, 4)],
261276)
262277# register(
263278# 'alcohol_narrow_flash', 'Alcohol flash narrow',
@@ -269,12 +284,12 @@ def register(name, title, time, tickmarks, label, yticks=None):
269284# )
270285register (
271286 'butanol_purification' , 'Butanol purification' ,
272- 1 , [0 , 0.2 , 0.4 , 0.6 , 0.8 , 1 ], 'BtOH\n separation' ,
287+ 1 , [0 , 0.1 , 0.2 , 0.3 , 0.4 , 0.5 ], 'BtOH\n separation' ,
273288 [(- 15 , - 10 , - 5 , 0 , 5 ), (- 15 , - 10 , - 5 , 0 , 5 )],
274289)
275290register (
276291 'ethanol_purification' , 'Ethanol purification' ,
277- 0.2 , [0 , 0.05 , 0.1 , 0.15 , 0.2 ], 'EtOH\n separation' ,
292+ 0.2 , [0 , 0.03 , 0.06 , 0.09 , 0.12 ], 'EtOH\n separation' ,
278293 [(- 15 , - 10 , - 5 , 0 , 5 ), (- 15 , - 10 , - 5 , 0 , 5 )],
279294)
280295# register(
@@ -291,10 +306,12 @@ def register(name, title, time, tickmarks, label, yticks=None):
291306# )
292307register (
293308 'haber_bosch_process' , 'Haber-Bosch' ,
294- 0.03 , [0 , 0.007 , 0.014 , 0.021 , 0.028 ], 'Haber-Bosch\n ammonia\n production' ,
309+ 0.03 , [0 , 0.004 , 0.010 , 0.015 , 0.020 ], 'Haber-Bosch\n ammonia\n production' ,
295310 [[- 10 , - 7.5 , - 5 , - 2.5 , 0 ], [- 10 , - 7.5 , - 5 , - 2.5 , 0 ]],
296311)
297312
313+ with open ('system_stages.json' , 'w' ) as file : json .dump (system_stages , file )
314+
298315# %% Testing
299316
300317def test_convergence (systems = None , alg = None , maxiter = None ):
@@ -339,26 +356,86 @@ def test_convergence(systems=None, alg=None, maxiter=None):
339356 print (f'- Multiple steady stages for { s_sm } , { s_sm .sink .ins .index (s_sm )} -{ s_sm .sink } : sm-{ actual } po-{ value } ' )
340357 return outs
341358
359+ # %% Stream tables and specifications
360+
361+ def save_stream_tables_and_specifications (systems = None ):
362+ if systems is None : systems = list (all_systems )
363+ for sys in systems :
364+ po = Tracker (sys , 'phenomena oriented' )
365+ for i in range (50 ): po .run ()
366+ with po .system .stage_configuration () as conf :
367+ for i in conf .streams :
368+ if i .ID == '' : i .ID = ''
369+ name = system_labels [sys ].replace ('\n ' , ' ' )
370+ file_name = f'{ name } .xlsx'
371+ file = os .path .join (simulations_folder , file_name )
372+ po .system .save_report (
373+ file = file ,
374+ sheets = {
375+ 'Flowsheet' ,
376+ 'Stream table' ,
377+ 'Specifications' ,
378+ 'Reactions' ,
379+ },
380+ stage = True ,
381+ )
382+
383+ # %% Simulation errors
384+
385+ def save_simulation_errors (systems = None ):
386+ if systems is None : systems = list (all_systems )
387+ values = []
388+ for sys in systems :
389+ file_name = f"{ sys } _steady_state"
390+ file = os .path .join (simulations_folder , file_name )
391+ values .append ([
392+ Convergence (
393+ all_systems [sys ](i ), file
394+ ).benchmark
395+ for i in ('phenomena-oriented' , 'sequential-modular' )
396+ ])
397+ df = pd .DataFrame (
398+ values ,
399+ [system_labels [i ].replace ('\n ' , ' ' ) for i in systems ],
400+ ('Phenomena-based' , 'Sequential modular' )
401+ )
402+ file_name = 'Simulation times.xlsx'
403+ file = os .path .join (simulations_folder , file_name )
404+ df .to_excel (file )
405+ return df
406+
407+
408+ # %% Convergence time
409+
410+ def convergence_time (sm , po ):
411+ ysm = np .array (sm )
412+ ypo = np .array (po )
413+ cutoff = ysm .min () + 1
414+ sm_index = sum (ysm > cutoff )
415+ po_index = sum (ypo > cutoff )
416+ return sm ['Time' ][sm_index ], po ['Time' ][po_index ]
417+
418+
342419# %% Profiling and benchmarking utilities
343420
344421class ErrorPoint (NamedTuple ):
345422 time : float ; error : float
346423
347- default_steady_state_cutoff = 2
424+ default_steady_state_cutoff = 1
348425
349426def steady_state_error (profile , steady_state_cutoff = None ):
350427 if steady_state_cutoff is None : steady_state_cutoff = default_steady_state_cutoff
351- minimum_error = np .log10 (10 ** profile ['Component flow rate error' ][- 1 ] + 10 ** profile ['Temperature error' ][- 1 ])
428+ minimum_error = np .log10 (10 ** profile ['Component flow rate error' ][- 1 ] + 10 ** profile ['Temperature error' ][- 1 ])
352429 return minimum_error + steady_state_cutoff
353430
354431def benchmark (profile , steady_state_error = None ):
355432 if steady_state_error is None : steady_state_error = steady_state_error (profile )
356433 time = profile ['Time' ]
357434 error = np .log10 (10 ** profile ['Component flow rate error' ] + 10 ** profile ['Temperature error' ])
358435 if np .isnan (error ).any (): breakpoint ()
359- error = interpolate .interp1d (error , time , bounds_error = False )(steady_state_error )
436+ time = interpolate .interp1d (error , time , bounds_error = False )(steady_state_error )
360437 if np .isnan (error ).any (): breakpoint ()
361- return error
438+ return time
362439
363440class Convergence :
364441
@@ -381,6 +458,8 @@ def __init__(self, system, file):
381458 flows = np .array (sum ([i ._imol ._parent .data .rows for i in streams ], []))
382459 Ts = np .array ([i .T for i in streams ])
383460 results = flows , Ts
461+ # cfe, te = zip(*[i._simulation_error() for i in all_stages])
462+ system .run ()
384463 cfe , te = zip (* [i ._simulation_error () for i in all_stages ])
385464 benchmark = sum (cfe ) + sum (te )
386465 with open (file , 'wb' ) as f : pickle .dump ((* results , benchmark ), f )
@@ -733,12 +812,15 @@ def division_mean_std(xdx, ydy):
733812
734813# %% Benchmark plot
735814
736- def plot_benchmark (systems = None , exclude = None , N = 5 , load = True , save = True ):
815+ def plot_benchmark (systems = None , exclude = None , N = 5 , load = True , save = True , sort_by_stage = True , label_stages = True ):
737816 if systems is None : systems = list (all_systems )
738817 if exclude is not None : systems = [i for i in systems if i not in exclude ]
818+ if sort_by_stage : systems = sorted (systems , key = lambda x : system_stages [x ])
739819 n_systems = len (systems )
740820 results = np .zeros ([n_systems , 3 ])
741821 Ns = N * np .ones (n_systems , int )
822+ index = []
823+ values = []
742824 for m , sys in enumerate (systems ):
743825 N = Ns [m ]
744826 time = system_convergence_times [sys ]
@@ -804,11 +886,18 @@ def plot_benchmark(systems=None, exclude=None, N=5, load=True, save=True):
804886 po_name = f'po_{ time } _{ sys } _profile.npy'
805887 file = os .path .join (simulations_folder , po_name )
806888 with open (file , 'wb' ) as f : pickle .dump (pos , f )
807- pos = pos [1 :]
808- sms = sms [1 :]
809889 cutoff = max ([steady_state_error (i ) for i in sms + pos ])
810890 sms = np .array ([benchmark (i , cutoff ) for i in sms ])
811891 pos = np .array ([benchmark (i , cutoff ) for i in pos ])
892+ values .append (pos )
893+ name = system_labels [sys ].replace ('\n ' , ' ' )
894+ index .append (
895+ (name , 'Phen.' )
896+ )
897+ values .append (sms )
898+ index .append (
899+ (name , 'Seq. mod.' )
900+ )
812901 pos_mean_std = [np .mean (pos ), np .std (pos )]
813902 sms_mean_std = [np .mean (sms ), np .std (sms )]
814903 mean , std = division_mean_std (pos_mean_std , sms_mean_std )
@@ -826,9 +915,18 @@ def plot_benchmark(systems=None, exclude=None, N=5, load=True, save=True):
826915 # Assume only time matters from here on
827916 # for i, (sm, po) in enumerate(system_results):
828917 # results[i] = uncertainty_percent(po['Time'], sm['Time'])
918+ df = pd .DataFrame (
919+ values ,
920+ index = pd .MultiIndex .from_tuples (index ),
921+ columns = [str (i ) for i in range (N )],
922+ )
923+ file_name = 'Simulation times.xlsx'
924+ file = os .path .join (simulations_folder , file_name )
925+ df .to_excel (file )
926+
829927 n_rows = 1
830928 n_cols = 1
831- fs = 10
929+ fs = 8
832930 bst .set_font (fs )
833931 bst .set_figure_size ()
834932 fig , ax = plt .subplots (n_rows , n_cols )
@@ -842,7 +940,10 @@ def plot_benchmark(systems=None, exclude=None, N=5, load=True, save=True):
842940 yticks = (0 , 25 , 50 , 75 , 100 , 125 )
843941 yticklabels = [f'{ i } %' for i in yticks ]
844942 xticks = list (range (n_systems ))
845- xticklabels = [system_labels [sys ] for sys in systems ]
943+ if label_stages :
944+ xticklabels = [system_labels [sys ] + f'\n [{ system_stages [sys ]} stages]' for sys in systems ]
945+ else :
946+ xticklabels = [system_labels [sys ] for sys in systems ]
846947 sm_index , = np .where (results [:, - 1 ])
847948 po_index , = np .where (~ results [:, - 1 ].astype (bool ))
848949 plt .errorbar ([xticks [i ] for i in sm_index ], results [sm_index , 0 ],
@@ -851,7 +952,7 @@ def plot_benchmark(systems=None, exclude=None, N=5, load=True, save=True):
851952 plt .errorbar ([xticks [i ] for i in po_index ], results [po_index , 0 ], yerr = results [po_index , 1 ], color = cpo ,
852953 marker = 's' , linestyle = '' , capsize = 5 , capthick = 1.5 , ecolor = black )
853954 plt .axhline (y = 100 , color = 'grey' , ls = '--' , zorder = - 1 )
854- plt .ylabel ('Simulation time [%]' )
955+ plt .ylabel ('Relative simulation time [%]' )
855956 bst .utils .style_axis (
856957 ax , xticks = xticks , yticks = yticks ,
857958 xticklabels = xticklabels ,
@@ -867,11 +968,12 @@ def plot_benchmark(systems=None, exclude=None, N=5, load=True, save=True):
867968
868969# %% Profile plot
869970
870- def dct_mean_profile (dcts : list [dict ], keys : list [str ], ub : float , n = 50 ):
871- mean = {i : np .zeros (n ) for i in keys }
872- tmin = np .min ([np .min (i ['Time' ]) for i in dcts ])
873- t = np .linspace (0 , ub , n )
874- goods = [np .zeros (n ) for i in range (len (keys ))]
971+ def dct_mean_profile (dcts : list [dict ], keys : list [str ], ub : float ):
972+ tmin = np .mean ([np .min (i ['Time' ]) for i in dcts ])
973+ size = np .min ([len (i ['Time' ]) for i in dcts ])
974+ t = np .array ([i ['Time' ][:size ] for i in dcts ]).mean (axis = 0 )
975+ mean = {i : np .zeros (size ) for i in keys }
976+ goods = [np .zeros (size ) for i in range (len (keys ))]
875977 for dct in dcts :
876978 for i , good in zip (keys , goods ):
877979 x = dct ['Time' ]
@@ -893,29 +995,30 @@ def dct_mean_std(dcts: list[dict], keys: list[str]):
893995 return {i : (values [i ].mean (), values [i ].std ()) for i in keys }
894996
895997def plot_profile (
896- systems = None , N = 1 , load = True , save = True
998+ systems = None , N = 1 , load = True , save = True ,
999+ T = False ,
8971000 ):
8981001 if systems is None : systems = list (all_systems )
8991002 fs = 9
9001003 bst .set_font (fs )
9011004 keys = (
902- 'Component flow rate error' ,
903- # 'Temperature error',
904- # 'Component flow rate',
905- # 'Stream temperature',
906- # 'Stripping factor',
907- # 'Material balance',
908- # 'Energy balance',
1005+ 'Temperature error' if T else 'Component flow rate error' ,
9091006 )
1007+ # 'Temperature error',
1008+ # 'Component flow rate',
1009+ # 'Stream temperature',
1010+ # 'Stripping factor',
1011+ # 'Material balance',
1012+ # 'Energy balance',
9101013 units = (
911- r'$[\mathrm{mol} \cdot \mathrm{hr}^{\mathrm{-1}}]$' ,
912- # r'$[\mathrm{K}]$',
913- # r'$[\mathrm{mol} \cdot \mathrm{hr}^{\mathrm{-1}}]$',
914- # r'$[\mathrm{K}]$',
915- # r'$[-]$',
916- # r'$[\mathrm{mol} \cdot \mathrm{hr}^{\mathrm{-1}}]$',
917- # r'$[\mathrm{K}]$',
1014+ r'$[\mathrm{K}]$' if T else r'$[\mathrm{mol} \cdot \mathrm{hr}^{\mathrm{-1}}]$' ,
9181015 )
1016+ # r'$[\mathrm{K}]$',
1017+ # r'$[\mathrm{mol} \cdot \mathrm{hr}^{\mathrm{-1}}]$',
1018+ # r'$[\mathrm{K}]$',
1019+ # r'$[-]$',
1020+ # r'$[\mathrm{mol} \cdot \mathrm{hr}^{\mathrm{-1}}]$',
1021+ # r'$[\mathrm{K}]$',
9191022 n_rows = len (units )
9201023 n_cols = len (systems )
9211024 if n_cols >= 2 :
@@ -985,8 +1088,16 @@ def plot_profile(
9851088 sms = sms [1 :]
9861089 tub = system_tickmarks [sys ][- 1 ]
9871090 tub = min (tub , min ([dct ['Time' ][- 1 ] for dct in sms ]), min ([dct ['Time' ][- 1 ] for dct in pos ]))
1091+ for dct in sms + pos :
1092+ for key in dct :
1093+ if key == 'Time' : continue
1094+ dct [key ] = 10 ** np .array (dct [key ])
9881095 sm = dct_mean_profile (sms , keys , tub )
9891096 po = dct_mean_profile (pos , keys , tub )
1097+ for dct in (sm , po ):
1098+ for key in dct :
1099+ if key == 'Time' : continue
1100+ dct [key ] = np .log10 (dct [key ])
9901101 csm = Color (fg = '#33BBEE' ).RGBn
9911102 cpo = Color (fg = '#EE7733' ).RGBn
9921103 labels = {
@@ -1014,6 +1125,9 @@ def plot_profile(
10141125 ypo = np .array (po [i ])
10151126 ysm = gaussian_filter (ysm , 0.2 )
10161127 ypo = gaussian_filter (ypo , 0.2 )
1128+ cutoff = ysm .min () + 1
1129+ sm_index = sum (ysm > cutoff )
1130+ po_index = sum (ypo > cutoff )
10171131 tsm = np .array (sm ['Time' ])
10181132 tpo = np .array (po ['Time' ])
10191133 if ms :
@@ -1024,6 +1138,11 @@ def plot_profile(
10241138 plt .plot (tsm , ysm , lw = 0 , marker = '.' , color = csm , markersize = 2.5 )
10251139 plt .plot (tpo , ypo , lw = 0 , marker = '.' , color = cpo , markersize = 2.5 )
10261140 plt .plot (tpo , ypo , '-' , color = cpo , lw = 1.5 , alpha = 0.5 )
1141+ plt .plot (tsm [sm_index ], ysm [sm_index ], lw = 0 , marker = 'x' , color = csm , markersize = 5 )
1142+ plt .plot (tpo [po_index ], ypo [po_index ], lw = 0 , marker = 'x' , color = cpo , markersize = 5 )
1143+ print (sm_index , po_index )
1144+ # plt.annotate(str(sm_index), (tsm[sm_index], ysm[sm_index]), (tsm[sm_index], ysm[sm_index] - 1.5), color=csm)
1145+ # plt.annotate(str(po_index), (tpo[po_index], ypo[po_index]), (tpo[po_index], ypo[po_index] + 0.5), color=cpo)
10271146 yticklabels = m == 0
10281147 yticks = yticks_list [n ]
10291148 if yticklabels :
@@ -1094,4 +1213,4 @@ def plot_profile(
10941213 name = f'PO_SM_{ system_names } _profile.{ i } '
10951214 file = os .path .join (images_folder , name )
10961215 plt .savefig (file , dpi = 900 , transparent = True )
1097- return fig , all_axes , sms , pos
1216+ # return fig, all_axes, sms, pos
0 commit comments