Skip to content

Commit 9bdbdf0

Browse files
committed
add new features for reporting and diagrams
1 parent 53532f2 commit 9bdbdf0

File tree

11 files changed

+252
-77
lines changed

11 files changed

+252
-77
lines changed

biosteam/_system.py

Lines changed: 103 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2149,6 +2149,10 @@ def _thorough_digraph(self, graph_attrs):
21492149
def _cluster_digraph(self, graph_attrs):
21502150
return digraph_from_system(self, **graph_attrs)
21512151

2152+
def _stage_digraph(self, graph_attrs):
2153+
with self.stage_configuration(aggregated=False) as conf:
2154+
return digraph_from_units(conf.stages, conf.streams, **graph_attrs)
2155+
21522156
def diagram(self, kind: Optional[int|str]=None, file: Optional[str]=None,
21532157
format: Optional[str]=None, display: Optional[bool]=True,
21542158
number: Optional[bool]=None, profile: Optional[bool]=None,
@@ -2163,9 +2167,10 @@ def diagram(self, kind: Optional[int|str]=None, file: Optional[str]=None,
21632167
----------
21642168
kind :
21652169
* 0 or 'cluster': Display all units clustered by system.
2166-
* 1 or 'thorough': Display every unit within the path.
2170+
* 1 or 'thorough': Display every unit within the system.
21672171
* 2 or 'surface': Display only elements listed in the path.
2168-
* 3 or 'minimal': Display a single box representing all units.
2172+
* 3 or 'minimal': Display a single box representing the system.
2173+
* 4 or 'stage': Display every stage within the system.
21692174
file :
21702175
File name to save diagram.
21712176
format:
@@ -2203,6 +2208,8 @@ def diagram(self, kind: Optional[int|str]=None, file: Optional[str]=None,
22032208
f = self._surface_digraph(graph_attrs)
22042209
elif kind == 3 or kind == 'minimal':
22052210
f = self._minimal_digraph(graph_attrs)
2211+
elif kind == 4 or kind == 'stage':
2212+
f = self._stage_digraph(graph_attrs)
22062213
else:
22072214
raise ValueError("kind must be one of the following: "
22082215
"0 or 'cluster', 1 or 'thorough', 2 or 'surface', "
@@ -3486,7 +3493,14 @@ def results(self, with_units=True):
34863493
return series
34873494

34883495
# Report summary
3489-
def save_report(self, file: Optional[str]='report.xlsx', dpi: Optional[str]='900', **stream_properties):
3496+
def save_report(
3497+
self,
3498+
file: Optional[str]='report.xlsx',
3499+
dpi: Optional[str]='900',
3500+
sheets=None,
3501+
stage=False,
3502+
**stream_properties
3503+
):
34903504
"""
34913505
Save a system report as an xlsx file.
34923506
@@ -3500,83 +3514,108 @@ def save_report(self, file: Optional[str]='report.xlsx', dpi: Optional[str]='900
35003514
Additional stream properties and units as key-value pairs (e.g. T='degC', flow='gpm', H='kW', etc..)
35013515
35023516
"""
3517+
if sheets is None:
3518+
sheets = {
3519+
'Flowsheet',
3520+
'Itemized costs',
3521+
'Stream table',
3522+
'Utilities',
3523+
'Design requirements',
3524+
'Reactions',
3525+
# 'Specifications'
3526+
}
35033527
writer = pd.ExcelWriter(file)
35043528
units = sorted(self.units, key=lambda x: x.line)
35053529
cost_units = [i for i in units if i._design or i._cost]
3506-
try:
3507-
with bst.preferences.temporary() as p:
3508-
p.reset()
3509-
p.light_mode()
3510-
self.diagram('thorough', file='flowsheet', dpi=str(dpi), format='png')
3511-
except:
3512-
diagram_completed = False
3513-
warn(RuntimeWarning('failed to generate diagram through graphviz'), stacklevel=2)
3514-
else:
3515-
import PIL.Image
3530+
if 'Flowsheet' in sheets:
35163531
try:
3517-
# Assume openpyxl is used
3518-
worksheet = writer.book.create_sheet('Flowsheet')
3519-
flowsheet = openpyxl.drawing.image.Image('flowsheet.png')
3520-
worksheet.add_image(flowsheet, anchor='A1')
3521-
except PIL.Image.DecompressionBombError:
3522-
PIL.Image.MAX_IMAGE_PIXELS = int(1e9)
3523-
flowsheet = openpyxl.drawing.image.Image('flowsheet.png')
3524-
worksheet.add_image(flowsheet, anchor='A1')
3532+
with bst.preferences.temporary() as p:
3533+
p.reset()
3534+
p.light_mode()
3535+
kind = 'stage' if stage else 'thorough'
3536+
self.diagram(kind, file='flowsheet', dpi=str(dpi), format='png')
35253537
except:
3526-
# Assume xlsx writer is used
3538+
diagram_completed = False
3539+
warn(RuntimeWarning('failed to generate diagram through graphviz'), stacklevel=2)
3540+
else:
3541+
import PIL.Image
35273542
try:
3528-
worksheet = writer.book.add_worksheet('Flowsheet')
3543+
# Assume openpyxl is used
3544+
worksheet = writer.book.create_sheet('Flowsheet')
3545+
flowsheet = openpyxl.drawing.image.Image('flowsheet.png')
3546+
worksheet.add_image(flowsheet, anchor='A1')
3547+
except PIL.Image.DecompressionBombError:
3548+
PIL.Image.MAX_IMAGE_PIXELS = int(1e9)
3549+
flowsheet = openpyxl.drawing.image.Image('flowsheet.png')
3550+
worksheet.add_image(flowsheet, anchor='A1')
35293551
except:
3530-
warn("problem in saving flowsheet; please submit issue to BioSTEAM with"
3531-
"your current version of openpyxl and xlsx writer", RuntimeWarning)
3532-
worksheet.insert_image('A1', 'flowsheet.png')
3533-
diagram_completed = True
3552+
# Assume xlsx writer is used
3553+
try:
3554+
worksheet = writer.book.add_worksheet('Flowsheet')
3555+
except:
3556+
warn("problem in saving flowsheet; please submit issue to BioSTEAM with"
3557+
"your current version of openpyxl and xlsx writer", RuntimeWarning)
3558+
worksheet.insert_image('A1', 'flowsheet.png')
3559+
diagram_completed = True
35343560

3535-
tea = self.TEA
3536-
if tea:
3561+
if 'Itemized costs' in sheets:
35373562
tea = self.TEA
3538-
cost = report.cost_table(tea)
3539-
cost.to_excel(writer, 'Itemized costs')
3540-
tea.get_cashflow_table().to_excel(writer, 'Cash flow')
3541-
else:
3542-
warn(f'Cannot find TEA object in {repr(self)}. Ignoring TEA sheets.',
3543-
RuntimeWarning, stacklevel=2)
3544-
3545-
# Stream tables
3546-
# Organize streams by chemicals first
3547-
streams_by_chemicals = {}
3548-
for i in self.streams:
3549-
if not i: continue
3550-
chemicals = i.chemicals
3551-
if chemicals in streams_by_chemicals:
3552-
streams_by_chemicals[chemicals].append(i)
3563+
if tea:
3564+
tea = self.TEA
3565+
cost = report.cost_table(tea)
3566+
cost.to_excel(writer, 'Itemized costs')
3567+
tea.get_cashflow_table().to_excel(writer, 'Cash flow')
35533568
else:
3554-
streams_by_chemicals[chemicals] = [i]
3555-
stream_tables = []
3556-
for chemicals, streams in streams_by_chemicals.items():
3557-
stream_tables.append(report.stream_table(streams, chemicals=chemicals, T='K', **stream_properties))
3558-
report.tables_to_excel(stream_tables, writer, 'Stream table')
3569+
warn(f'Cannot find TEA object in {repr(self)}. Ignoring TEA sheets.',
3570+
RuntimeWarning, stacklevel=2)
35593571

3560-
# Heat utility tables
3561-
heat_utilities = report.heat_utility_tables(cost_units)
3562-
n_row = report.tables_to_excel(heat_utilities, writer, 'Utilities')
3572+
if 'Stream table' in sheets:
3573+
if stage:
3574+
with self.stage_configuration() as conf:
3575+
stream_tables = report.stream_tables(conf.streams, **stream_properties)
3576+
else:
3577+
stream_tables = report.stream_tables(self.streams, **stream_properties)
3578+
report.tables_to_excel(stream_tables, writer, 'Stream table')
35633579

3564-
# Power utility table
3565-
power_utility = report.power_utility_table(cost_units)
3566-
n_row = report.tables_to_excel([power_utility], writer, 'Utilities', n_row=n_row)
3580+
if 'Utilities' in sheets:
3581+
# Heat utility tables
3582+
heat_utilities = report.heat_utility_tables(cost_units)
3583+
n_row = report.tables_to_excel(heat_utilities, writer, 'Utilities')
35673584

3568-
# Fees table
3569-
other_utilities = report.other_utilities_table(cost_units)
3570-
n_row = report.tables_to_excel(other_utilities, writer, 'Utilities', n_row=n_row)
3585+
# Power utility table
3586+
power_utility = report.power_utility_table(cost_units)
3587+
n_row = report.tables_to_excel([power_utility], writer, 'Utilities', n_row=n_row)
3588+
3589+
# Fees table
3590+
other_utilities = report.other_utilities_table(cost_units)
3591+
n_row = report.tables_to_excel(other_utilities, writer, 'Utilities', n_row=n_row)
35713592

3572-
# General desing requirements
3573-
results = report.unit_result_tables(cost_units)
3574-
report.tables_to_excel(results, writer, 'Design requirements')
3593+
if 'Design requirements' in sheets:
3594+
# General desing requirements
3595+
results = report.unit_result_tables(cost_units)
3596+
report.tables_to_excel(results, writer, 'Design requirements')
35753597

3576-
# Reaction tables
3577-
reactions = report.unit_reaction_tables(units)
3578-
report.tables_to_excel(reactions, writer, 'Reactions')
3598+
if 'Reactions' in sheets:
3599+
# Reaction tables
3600+
reactions = report.unit_reaction_tables(units)
3601+
report.tables_to_excel(reactions, writer, 'Reactions')
35793602

3603+
if 'Specifications' in sheets:
3604+
if stage:
3605+
with self.stage_configuration() as conf:
3606+
specifications = [
3607+
u._mass_and_energy_balance_specifications_table()
3608+
for u in conf.stages
3609+
]
3610+
else:
3611+
specifications = [
3612+
u._mass_and_energy_balance_specifications_table()
3613+
for u in units
3614+
]
3615+
report.tables_to_excel(
3616+
specifications, writer,
3617+
'Specifications'
3618+
)
35803619
writer.close()
35813620
if diagram_completed: os.remove("flowsheet.png")
35823621

biosteam/_unit.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,22 @@ def simulate(self,
13271327
self.run()
13281328
self._summary(design_kwargs, cost_kwargs)
13291329

1330+
def _mass_and_energy_balance_specifications(self):
1331+
return (self.line, ())
1332+
1333+
def _mass_and_energy_balance_specifications_table(self):
1334+
title, specs = self._mass_and_energy_balance_specifications()
1335+
index = []
1336+
values = []
1337+
for name, value, units in specs:
1338+
index.append(name)
1339+
values.append([value, units])
1340+
df = pd.DataFrame(
1341+
values, index, ('Values', 'Units')
1342+
)
1343+
df.columns.name = f"{title} - {self.ID}"
1344+
return df
1345+
13301346
def results(self, with_units=True, include_utilities=True,
13311347
include_total_cost=True, include_installed_cost=False,
13321348
include_zeros=True, external_utilities=None, key_hook=None,

biosteam/report/system_stages.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"acetic_acid_simple": 13, "acetic_acid_complex_decoupled": 21, "acetic_acid_complex": 44, "butanol_purification": 4, "ethanol_purification": 2, "haber_bosch_process": 4}

biosteam/report/table.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
DataFrame = pd.DataFrame
1717
ExcelWriter = pd.ExcelWriter
1818

19-
__all__ = ('stream_table', 'cost_table', 'unit_reaction_tables',
19+
__all__ = ('stream_table', 'stream_tables',
20+
'cost_table', 'unit_reaction_tables',
2021
'unit_result_tables', 'heat_utility_tables',
2122
'power_utility_table', 'tables_to_excel', 'voc_table',
2223
'other_utilities_table',
@@ -700,6 +701,20 @@ def other_utilities_table(units):
700701

701702
# %% Streams
702703

704+
def stream_tables(streams, **stream_properties):
705+
streams_by_chemicals = {}
706+
stream_tables = []
707+
for i in streams:
708+
if not i: continue
709+
chemicals = i.chemicals
710+
if chemicals in streams_by_chemicals:
711+
streams_by_chemicals[chemicals].append(i)
712+
else:
713+
streams_by_chemicals[chemicals] = [i]
714+
for chemicals, streams in streams_by_chemicals.items():
715+
stream_tables.append(stream_table(streams, chemicals=chemicals, T='K', **stream_properties))
716+
return stream_tables
717+
703718
def stream_table(streams, flow='kg/hr', percent=True, chemicals=None, **props):
704719
"""
705720
Return a stream table as a pandas DataFrame object.

biosteam/system_stages.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"acetic_acid_simple": 13, "acetic_acid_complex_decoupled": 21, "acetic_acid_complex": 44, "butanol_purification": 4, "ethanol_purification": 2, "haber_bosch_process": 4}

biosteam/units/_flash.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ def _cost_vacuum(self):
331331
def _design_parameters(self):
332332
# Retrieve run_args and properties
333333
vap, liq, *_ = self._outs
334+
if self.has_vapor_condenser: vap = self.vapor_condenser.outs[0]
334335
rhov = vap.get_property('rho', 'lb/ft3')
335336
rhol = liq.get_property('rho', 'lb/ft3')
336337
P = liq.get_property('P', 'psi') # Pressure (psi)

biosteam/units/compressor.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,12 @@ def _run(self):
567567
if self.system and self.system.algorithm == 'Phenomena oriented':
568568
self._coeffs = {self: feed.T, feed.source: out.T} # dT_out = (T_out/T_in) * dT_in
569569

570+
def _mass_and_energy_balance_specifications(self):
571+
return 'Compressor', [
572+
('Isentropic efficiency', 100 * self.eta, '%'),
573+
('P', self.P, 'Pa'),
574+
]
575+
570576
def _design(self):
571577
super()._design()
572578
self._set_power(self.design_results['Ideal power'] / self.eta)
@@ -585,24 +591,27 @@ def _update_nonlinearities(self):
585591
source = feed.source
586592
if source is None or source._energy_variable != 'T': return []
587593
data = product.get_data()
594+
T_product = product.T
588595
self._run()
589-
self._coeffs = {self: feed.T, source: product.T} # dT_out = (T_out/T_in) * dT_in
596+
# self._coeffs = {self: feed.T, source: T_product} # dT_out = (T_out/T_in) * dT_in
590597
product.set_data(data)
598+
product.T = T_product
591599

592600
def _create_energy_departure_equations(self):
601+
return []
593602
# Special case where T_out = f(T_in)
594603
# 0 = Cp * log(T/T0) - R * log(P/P0)
595604
# log(T/T0) = R / Cp * log(P/P0)
596605
# T = T0 * exp(R / Cp * log(P/P0))
597606
# T = T0 * P/P0 * exp(R/Cp)
598-
feed = self.ins[0]
599-
product = self.outs[0]
600-
if len(product.phases) > 1:
601-
raise NotImplementedError('energy departure equation with multiple phase not yet implemented for isentropic compressors')
602-
source = feed.source
603-
# TODO: add method <Stream>.energy_variable -> 'T'|'B'
604-
if source is None or source._energy_variable != 'T': return []
605-
return [(self._coeffs, 0)]
607+
# feed = self.ins[0]
608+
# product = self.outs[0]
609+
# if len(product.phases) > 1:
610+
# raise NotImplementedError('energy departure equation with multiple phase not yet implemented for isentropic compressors')
611+
# source = feed.source
612+
# # TODO: add method <Stream>.energy_variable -> 'T'|'B'
613+
# if source is None or source._energy_variable != 'T': return []
614+
# return [(self._coeffs, 0)]
606615

607616
def _create_material_balance_equations(self, composition_sensitive):
608617
fresh_inlets, process_inlets, equations = self._begin_equations(composition_sensitive)
@@ -621,8 +630,8 @@ def _create_material_balance_equations(self, composition_sensitive):
621630
)
622631
return equations
623632

624-
def _update_energy_variable(self, departure):
625-
self.outs[0].T += departure
633+
# def _update_energy_variable(self, departure):
634+
# self.outs[0].T += departure
626635

627636

628637
class PolytropicCompressor(Compressor, new_graphics=False):

biosteam/units/distillation.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,36 @@ def _init(self,
346346
self.vacuum_system_preference = vacuum_system_preference
347347
self._load_components(partial_condenser, condenser_thermo, reboiler_thermo)
348348
self.LHK = LHK
349-
349+
350+
def _mass_and_energy_balance_specifications(self):
351+
spec = self.product_specification_format
352+
specs = []
353+
if spec == 'Composition':
354+
self._Lr = self._Hr = None
355+
elif spec == 'Recovery':
356+
self._y_top = self._x_bot = None
357+
specs.append(
358+
('Partial condenser', self._partial_condenser, '-'),
359+
)
360+
if spec == 'Composition':
361+
specs.extend([
362+
('Distillate light key fraction', 100 * self._y_top, '%'),
363+
('Bottoms product heavy key fraction', 100 * self._x_bot, '%'),
364+
])
365+
elif spec == 'Recovery':
366+
specs.extend([
367+
('Light key recovery', 100 * self._Lr, '%'),
368+
('Heavy key recovery', 100 * self._Hr, '%'),
369+
])
370+
else:
371+
raise RuntimeError('invalid product specification format')
372+
if isinstance(self, ShortcutColumn):
373+
return 'Shortcut column', specs
374+
elif isinstance(self, BinaryDistillation):
375+
return 'Binary distillation', specs
376+
else:
377+
raise NotImplementedError('unknown name for distillation class')
378+
350379
def _reset_thermo(self, thermo):
351380
super()._reset_thermo(thermo)
352381
self.LHK = self._LHK
@@ -1947,6 +1976,8 @@ def _estimate_mean_volatilities_relative_to_heavy_key(self):
19471976
alpha_mean = compute_mean_volatilities_relative_to_heavy_key(
19481977
K_distillate, K_bottoms, HK_index
19491978
)
1979+
distillate.T = dp.T
1980+
bottoms.T = bp.T
19501981
return alpha_mean
19511982

19521983
def _estimate_distillate_recoveries(self):

0 commit comments

Comments
 (0)