Skip to content

Commit 832866e

Browse files
committed
2.52.8 release; improvements to phenomena-based sim
1 parent 2bac34d commit 832866e

File tree

11 files changed

+880
-819
lines changed

11 files changed

+880
-819
lines changed

biosteam/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
1414
"""
1515
from __future__ import annotations
16-
__version__ = '2.52.7'
16+
__version__ = '2.52.8'
1717

1818
#: Chemical engineering plant cost index (defaults to 567.5 at 2017).
1919
CE: float = 567.5

biosteam/_system.py

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,6 @@ class Configuration:
9494
'composition_sensitive_nodes',
9595
'_has_dynamic_coefficients')
9696

97-
energy_variable_relaxation_factor = 0.5
98-
9997
def __init__(self, path, stages, nodes, streams, stream_ref, connections, aggregated):
10098
self.path = path
10199
self.stages = stages
@@ -139,24 +137,22 @@ def solve_nonlinearities(self):
139137
i._update_nonlinearities()
140138
else:
141139
for i in self.path:
142-
if getattr(i, 'decoupled', False):
143-
assert False
144-
i.run()
145140
i._update_nonlinearities()
146141

147142
if self.composition_sensitive_path:
148143
# for i in self.composition_sensitive_path: i._run()
144+
# breakpoint()
149145
containers, flows = self.solve_material_flows(composition_sensitive=True, full_output=True)
150146
def update_inner_material_balance_parameters(flows):
151-
for i in self.composition_sensitive_path: i._update_composition_parameters()
147+
for i in self.composition_sensitive_nodes: i._update_composition_parameters()
152148
return self.solve_material_flows(composition_sensitive=True)
153149

154150
flx.fixed_point(
155151
update_inner_material_balance_parameters,
156152
flows,
157153
xtol=tmo.LLE.pseudo_equilibrium_inner_loop_options['xtol'],
158154
rtol=tmo.LLE.pseudo_equilibrium_inner_loop_options['rtol'],
159-
maxiter=10, checkiter=False,
155+
maxiter=100, checkiter=False,
160156
checkconvergence=False,
161157
)
162158
for i in self.composition_sensitive_path: i._update_net_flow_parameters()
@@ -243,17 +239,6 @@ def solve_material_flows(self, composition_sensitive=False, full_output=False):
243239
values[values < 0] = 0
244240
for mol, value in zip(containers, values): # update material flows
245241
mol[:] = value
246-
# for obj, mask, value in zip(objs, masks, values): # update material flows
247-
# indexer, phase = obj
248-
# index = indexer._phase_indexer(phase)
249-
# mol = indexer.data.rows[index]
250-
# if mask.any():
251-
# new_mol = value[mask]
252-
# relaxation_factor = (-new_mol / (mol[mask] - new_mol)).max()
253-
# print(relaxation_factor)
254-
# mol[:] = relaxation_factor * mol + (1 - relaxation_factor) * value
255-
# else:
256-
# mol[:] = value
257242
else:
258243
if full_output:
259244
containers = []
@@ -276,37 +261,52 @@ def solve_material_flows(self, composition_sensitive=False, full_output=False):
276261
else:
277262
return values
278263

279-
def solve_energy_departures(self):
264+
def solve_energy_flows(self):
280265
nodes = self.nodes
281266
A = []
282267
b = []
283268
for node in nodes:
284-
for coefficients, value in node._create_energy_departure_equations():
269+
for coefficients, value in node._create_energy_balance_equations():
270+
A.append(coefficients)
271+
b.append(value)
272+
if not A: return
273+
for node in nodes:
274+
for coefficients, value in node._create_bulk_balance_equations():
285275
A.append(coefficients)
286276
b.append(value)
277+
A = [{(getattr(i, 'material_reference', i), v): j for (i, v), j in x.items()} for x in A]
287278
A, objs = dictionaries2array(A)
288-
departures = solve(A, np.array(b).T).T
289-
# Prevent negative numbers
290-
f = 1
291-
for obj, delta in zip(objs, departures):
292-
if delta >= 0: continue
293-
var = obj._energy_variable
294-
value = getattr(obj, var, None)
295-
if value is None:
296-
if var == 'T':
297-
value = obj.outs[0].T
279+
values = solve(A, np.array(b).T).T
280+
# try: values = solve(A, np.array(b).T).T
281+
# except:
282+
# A = []
283+
# b = []
284+
# breakpoint()
285+
# for node in nodes:
286+
# for coefficients, value in node._create_energy_balance_equations():
287+
# breakpoint()
288+
# A.append(coefficients)
289+
# b.append(value)
290+
# if not A: return
291+
# for node in nodes:
292+
# for coefficients, value in node._create_bulk_balance_equations():
293+
# breakpoint()
294+
# A.append(coefficients)
295+
# b.append(value)
296+
for (obj, var), value in zip(objs, values):
297+
if var == 'F_mol':
298+
indexer, phase = obj
299+
index = indexer._phase_indexer(phase)
300+
mol = indexer.data.rows[index]
301+
if value == 0:
302+
mol[:] = 0
298303
else:
299-
raise RuntimeError(f'energy variable {var!r} value is None')
300-
f = min(- value / delta, f)
301-
departures *= (1 - self.energy_variable_relaxation_factor) * f
302-
try:
303-
for obj, departure in zip(objs, departures):
304-
obj._update_energy_variable(departure)
305-
except AttributeError as e:
306-
if obj._update_energy_variable:
307-
raise e
304+
total = mol.sum()
305+
if total != 0: mol[:] *= value / total
308306
else:
309-
raise NotImplementedError(f'{obj!r} has no method `_update_energy_variable`')
307+
obj._update_variable(var, value)
308+
for node in nodes:
309+
if hasattr(node, '_reset_bulk_variable'): node._reset_bulk_variable()
310310

311311
def __enter__(self):
312312
units = self.stages
@@ -2526,13 +2526,14 @@ def run_phenomena(self):
25262526
try:
25272527
conf.solve_nonlinearities()
25282528
if flag: self._collect_variables('phenomenode')
2529-
conf.solve_energy_departures()
2529+
conf.solve_energy_flows()
25302530
if flag: self._collect_variables('energy')
25312531
conf.solve_material_flows()
25322532
if flag: self._collect_variables('material')
25332533
except (NotImplementedError, UnboundLocalError, KeyError) as error:
25342534
raise error
25352535
except Exception as e:
2536+
raise e
25362537
print('FAILED!')
25372538
print(e)
25382539
print('-------')
@@ -2553,7 +2554,7 @@ def run_phenomena_modular(self):
25532554
if flag: self._collect_variables((i.ID, 'phenomenode'))
25542555
conf.solve_material_flows()
25552556
if flag: self._collect_variables('material')
2556-
conf.solve_energy_departures()
2557+
conf.solve_energy_flows()
25572558
if flag: self._collect_variables('energy')
25582559
except (NotImplementedError, UnboundLocalError, KeyError) as error:
25592560
raise error

biosteam/_unit.py

Lines changed: 118 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,11 @@ def __init_subclass__(cls,
157157
if default_phenomena is None:
158158
methods = (
159159
'_update_nonlinearities',
160-
'_get_energy_departure_coefficient',
160+
'_update_energy_coefficient',
161+
'_update_variable',
161162
'_create_material_balance_equations',
162-
'_create_energy_departure_equations',
163-
'_update_energy_variable',
163+
'_create_energy_balance_equations',
164+
'_create_bulk_balance_equations',
164165
'_energy_variable',
165166
)
166167
if all([getattr(cls, i) is getattr(Unit, i) for i in methods]):
@@ -377,11 +378,11 @@ def _update_nonlinearities(self):
377378
self._duty = self.Hnet
378379
Ts_new = [i.T for i in outs]
379380
if all([i == j for i, j in zip(Ts_new, Ts)]): # T constant
380-
self._energy_variable = None
381+
self.specified_variable = 'T'
381382
elif self.Hnet == Q: # Q constant
382-
self._energy_variable = 'T'
383+
self.specified_variable = 'Q'
383384
else:
384-
self._energy_variable = None
385+
self.specified_variable = 'B'
385386
for i, j in zip(outs, data): i.set_data(j)
386387

387388
def _simulation_error(self):
@@ -401,20 +402,35 @@ def _simulation_error(self):
401402
self._outs = outs
402403
return np.abs(new_flows - flows).sum(), np.abs(Ts_new - Ts).sum()
403404

404-
def _get_energy_departure_coefficient(self, stream):
405+
def _update_energy_coefficient(self, stream, coefficients):
405406
"""
406-
tuple[object, float] Return energy departure coefficient of a stream
407-
for phenomena-based simulation.
407+
Update coefficient of a stream
408+
for phenomena-based simulation and
409+
return the reference value.
408410
"""
409-
if self._energy_variable == 'T': return (self, stream.C)
411+
if self.specified_variable == 'Q':
412+
C = stream.C
413+
coefficients[self, 'T'] = -C
414+
return -C * stream.T
415+
return 0
416+
417+
def _update_variable(self, variable, value):
418+
"""
419+
Update variable being solved in equations for
420+
phenomena-based simulation.
421+
"""
422+
if variable == 'T':
423+
for i in self.outs: i.T = value
424+
else:
425+
raise ValueError('invalid variable')
410426

411427
def _create_material_balance_equations(self, composition_sensitive):
412428
"""
413429
list[tuple[dict, array]] Create material balance equations for
414430
phenomena-based simulation.
415431
"""
416432
if self._link_streams: return []
417-
fresh_inlets, process_inlets, equations = self._begin_equations(composition_sensitive)
433+
fresh_inlets, process_inlets, equations = self._begin_material_equations(composition_sensitive)
418434
outs = self.flat_outs
419435
N = self.chemicals.size
420436
ones = np.ones(N)
@@ -425,7 +441,7 @@ def _create_material_balance_equations(self, composition_sensitive):
425441
except:
426442
rhs = predetermined_flow
427443
mol_total = sum([i.mol for i in outs])
428-
for i, s in enumerate(outs):
444+
for s in outs:
429445
split = s.mol / mol_total
430446
minus_split = -split
431447
eq_outs = {}
@@ -436,31 +452,58 @@ def _create_material_balance_equations(self, composition_sensitive):
436452
)
437453
return equations
438454

439-
def _create_energy_departure_equations(self):
455+
def _create_energy_balance_equations(self):
440456
"""
441457
list[tuple[dict, float]] Create energy departure equations for
442458
phenomena-based simulation.
443459
"""
460+
fresh_inlets, process_inlets, equations = self._begin_energy_equations()
444461
if self._energy_variable == 'T':
445-
coeff = {self: sum([i.C for i in self.outs])}
446-
for i in self.ins: i._update_energy_departure_coefficient(coeff)
447-
if self._dmol.any():
448-
dH = self._duty - self.Hnet
462+
dmol = self._dmol
463+
if dmol.any():
464+
dH = self._duty + sum([i.Hf for i in self.outs]) - sum([i.Hf for i in self.ins])
449465
else:
450-
dH = self._duty + self.H_in - self.H_out
451-
return [(coeff, dH)]
466+
dH = self._duty
467+
coeff = {(self, 'T'): sum([i.C for i in self.outs])}
468+
for i, s in enumerate(self.outs): coeff[self, i] = s.h
469+
for i in process_inlets:
470+
dH += i._update_energy_coefficient(coeff)
471+
for i in fresh_inlets:
472+
dH += i.H
473+
equations.append(
474+
(coeff, dH)
475+
)
476+
return equations
452477
else:
453-
return []
478+
return equations
454479

455-
def _update_energy_variable(self, departure):
480+
def _create_bulk_balance_equations(self):
456481
"""
457-
Update energy variable being solved in energy departure equations for
482+
list[tuple[dict, array]] Create bulk balance equations for
458483
phenomena-based simulation.
459484
"""
460-
if self._energy_variable == 'T':
461-
for i in self.outs: i.T += departure
485+
if self._link_streams: return []
486+
fresh_inlets, process_inlets, equations = self._begin_bulk_equations()
487+
outs = self.flat_outs
488+
predetermined_flow = [i.F_mol for i in fresh_inlets]
489+
try:
490+
dF_mol = self._dmol.sum()
491+
rhs = predetermined_flow + dF_mol
492+
except:
493+
rhs = predetermined_flow
494+
F_mol_total = sum([i.F_mol for i in outs])
495+
for i, s in enumerate(outs):
496+
split = s.F_mol / F_mol_total
497+
minus_split = -split
498+
eq_outs = {}
499+
for i in process_inlets: eq_outs[i, 'F_mol'] = minus_split
500+
eq_outs[s, 'F_mol'] = 1
501+
equations.append(
502+
(eq_outs, split * rhs)
503+
)
504+
return equations
462505

463-
def _begin_equations(self, composition_sensitive):
506+
def _begin_material_equations(self, composition_sensitive):
464507
inlets = self.ins
465508
fresh_inlets = []
466509
process_inlets = []
@@ -519,6 +562,56 @@ def _begin_equations(self, composition_sensitive):
519562
equations = material_equations
520563
return fresh_inlets, process_inlets, equations
521564

565+
def _begin_bulk_equations(self):
566+
inlets = self.ins
567+
fresh_inlets = []
568+
process_inlets = []
569+
equations = [f() for i in inlets for f in i.bulk_equations]
570+
isfeed = lambda s: s.isfeed() or s.source._recycle_system is not self._recycle_system
571+
if equations:
572+
for i in inlets:
573+
if (isfeed(i) and
574+
not i.bulk_equations):
575+
fresh_inlets.append(i)
576+
elif len(i) > 1:
577+
process_inlets.extend(i)
578+
else:
579+
process_inlets.append(i)
580+
else:
581+
for i in inlets:
582+
if isfeed(i):
583+
fresh_inlets.append(i)
584+
elif len(i) > 1:
585+
process_inlets.extend(i)
586+
else:
587+
process_inlets.append(i)
588+
return fresh_inlets, process_inlets, equations
589+
590+
def _begin_energy_equations(self):
591+
inlets = self.ins
592+
fresh_inlets = []
593+
process_inlets = []
594+
equations = [f() for i in inlets for f in i.energy_equations]
595+
isfeed = lambda s: s.isfeed() or s.source._recycle_system is not self._recycle_system
596+
if equations:
597+
for i in inlets:
598+
if (isfeed(i) and
599+
not i.energy_equations):
600+
fresh_inlets.append(i)
601+
elif len(i) > 1:
602+
process_inlets.extend(i)
603+
else:
604+
process_inlets.append(i)
605+
else:
606+
for i in inlets:
607+
if isfeed(i):
608+
fresh_inlets.append(i)
609+
elif len(i) > 1:
610+
process_inlets.extend(i)
611+
else:
612+
process_inlets.append(i)
613+
return fresh_inlets, process_inlets, equations
614+
522615
Inlets = piping.Inlets
523616
Outlets = piping.Outlets
524617

0 commit comments

Comments
 (0)