-
Notifications
You must be signed in to change notification settings - Fork 2
Fix reactor mode crash paths #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -292,9 +292,11 @@ def _eval_state_events(self, time, states, sw): | |
| return events | ||
|
|
||
| def heat_transfer(self, temp, temp_ht, vol): | ||
| """Return reactor heat transfer duty for supported heat-transfer modes.""" | ||
| # Heat transfer area | ||
| if self.ht_mode == 'coil': # Half pipe heat transfer | ||
| pass | ||
| raise NotImplementedError( | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking — this refusal never reaches a CSTR/Semibatch user.
I reproduced this on this head for both Fix: mirror the if self.ht_mode == 'coil' and not self.isothermal:
raise NotImplementedError(
"CSTR heat transfer with ht_mode='coil' is not supported")Scoping on
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we even allow that ht_mode to be called? Is it implemented elsewhere? Should this become an issue to be implemented? |
||
| "heat_transfer with ht_mode='coil' is not supported") | ||
| else: | ||
| area_ht = 4 / self.diam * vol + self.area_base # m**2 | ||
| heat_transf = self.u_ht * area_ht * (temp - temp_ht) | ||
|
|
@@ -947,7 +949,8 @@ class CSTR(_BaseReactor): | |
| whether or not the paramest_wrapper method should return | ||
| the sensitivity system along with the concentratio profiles. | ||
| Use False if you want the parameter estimation platform to | ||
| estimate the sensitivity system using finite differences | ||
| estimate the sensitivity system using finite differences. | ||
| Direct sensitivity evaluation is not implemented for CSTR. | ||
| """ | ||
|
|
||
| def __init__(self, mask_params=None, | ||
|
|
@@ -1095,9 +1098,19 @@ def solve_unit(self, runtime=None, time_grid=None, eval_sens=False, | |
|
|
||
| check_modeling_objects(self) | ||
|
|
||
| if eval_sens: | ||
| raise NotImplementedError( | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nonblocking — this improves the error but still leaves no working path.
Meanwhile Flipping the If you would rather not change a public default in this PR, please at least name the escape hatch in the message and fix the docstring at raise NotImplementedError(
"CSTR sensitivity evaluation is not supported; construct with "
"return_sens=False to use finite-difference sensitivities")
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed the lower-risk option in |
||
| "CSTR sensitivity evaluation is not supported; construct " | ||
| "with return_sens=False to use finite-difference " | ||
| "sensitivities") | ||
|
|
||
| self.params_control = params_control | ||
| self.set_names() | ||
|
|
||
| if self.ht_mode == 'coil' and not self.isothermal: | ||
| raise NotImplementedError( | ||
| "CSTR heat transfer with ht_mode='coil' is not supported") | ||
|
|
||
| self.num_concentr = len(self.Liquid_1.mole_conc) | ||
| self.args_inputs = (self, self.num_concentr, 0) | ||
|
|
||
|
|
@@ -1131,14 +1144,11 @@ def solve_unit(self, runtime=None, time_grid=None, eval_sens=False, | |
|
|
||
| # Create problem | ||
| merged_params = self.Kinetics.concat_params() | ||
| if eval_sens: | ||
| pass | ||
| else: | ||
| def fobj(time, states): return self.unit_model( | ||
| time, states, merged_params) | ||
| def fobj(time, states): return self.unit_model( | ||
| time, states, merged_params) | ||
|
|
||
| problem = Explicit_Problem(fobj, states_init, | ||
| t0=self.elapsed_time) | ||
| problem = Explicit_Problem(fobj, states_init, | ||
| t0=self.elapsed_time) | ||
|
|
||
| # Set solver | ||
| solver = CVode(problem) | ||
|
|
@@ -1250,7 +1260,8 @@ class SemibatchReactor(CSTR): | |
| whether or not the paramest_wrapper method should return | ||
| the sensitivity system along with the concentratio profiles. | ||
| Use False if you want the parameter estimation platform to | ||
| estimate the sensitivity system using finite differences | ||
| estimate the sensitivity system using finite differences. | ||
| Direct sensitivity evaluation is not implemented for SemibatchReactor. | ||
| """ | ||
|
|
||
| def __init__(self, vol_tank, | ||
|
|
@@ -1299,9 +1310,20 @@ def solve_unit(self, runtime=None, time_grid=None, eval_sens=False, | |
|
|
||
| check_modeling_objects(self) | ||
|
|
||
| if eval_sens: | ||
| raise NotImplementedError( | ||
| "SemibatchReactor sensitivity evaluation is not supported; " | ||
| "construct with return_sens=False to use finite-difference " | ||
| "sensitivities") | ||
|
|
||
| self.params_control = params_control | ||
| self.set_names() | ||
|
|
||
| if self.ht_mode == 'coil' and not self.isothermal: | ||
| raise NotImplementedError( | ||
| "SemibatchReactor heat transfer with ht_mode='coil' is " | ||
| "not supported") | ||
|
|
||
| if runtime is not None: | ||
| final_time = runtime + self.elapsed_time | ||
|
|
||
|
|
@@ -1323,14 +1345,11 @@ def solve_unit(self, runtime=None, time_grid=None, eval_sens=False, | |
| states_init = np.append(states_init, tht_init) | ||
|
|
||
| merged_params = self.Kinetics.concat_params() | ||
| if eval_sens: | ||
| pass | ||
| else: | ||
| def fobj(time, states): return self.unit_model( | ||
| time, states, merged_params) | ||
| def fobj(time, states): return self.unit_model( | ||
| time, states, merged_params) | ||
|
|
||
| problem = Explicit_Problem(fobj, states_init, | ||
| t0=self.elapsed_time) | ||
| problem = Explicit_Problem(fobj, states_init, | ||
| t0=self.elapsed_time) | ||
|
|
||
| # Set solver | ||
| solver = CVode(problem) | ||
|
|
@@ -1556,16 +1575,15 @@ def energy_steady(self, conc, temp): | |
| delta_hrxn=deltah_rxn) | ||
|
|
||
| # ---------- Balance terms (W) | ||
| # source_term = -inner1d(deltah_rxn, rates) * 1000 # W/m**3 | ||
| # TODO: Check if this is correct | ||
| # source_term = -np.dot(deltah_rxn, rates) * 1000 # W / m**3 | ||
| source_term = -(deltah_rxn * rates).sum(axis=1) * 1000 # W / m**3 | ||
| source_term = -np.dot(deltah_rxn, rates) * 1000 # W / m**3 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Units formatting and why do we need the 1000 factor? |
||
|
|
||
| if self.adiabatic: | ||
| heat_transfer = 0 | ||
| else: # W/m**3 | ||
| # The steady-PFR area formula is a pre-existing issue tracked in #33. | ||
| a_prime = self.diam / 4 # m**2 / m**3 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is 4 here? |
||
| heat_transfer = self.u_ht * a_prime * (temp - self.Utility.temp) | ||
| heat_transfer = self.u_ht * a_prime * ( | ||
| temp - self.temp_ht_steady) | ||
|
|
||
| flow_term = self.Inlet.vol_flow * cp_vol | ||
|
|
||
|
|
@@ -1601,7 +1619,7 @@ def solve_steady(self, vol_rxn, adiabatic=False): | |
| self.isothermal = False | ||
| self.states_uo.append('temp') | ||
|
|
||
| c_inlet = self.Inlet.concentr | ||
| c_inlet = self.Inlet.mole_conc | ||
|
|
||
| self.c_inert = c_inlet[~self.mask_species] | ||
| c_partic = c_inlet[self.mask_species] | ||
|
|
@@ -1613,6 +1631,11 @@ def solve_steady(self, vol_rxn, adiabatic=False): | |
| if 'temp' in self.states_uo: | ||
| states_init = np.append(states_init, self.Inlet.temp) | ||
|
|
||
| if 'temp' in self.states_uo and not self.adiabatic: | ||
| # The steady solve integrates over volume, not time, so use the | ||
| # inlet utility condition at the start of the volume profile. | ||
| self.temp_ht_steady = self.Utility.evaluate_inputs(0)['temp_in'] | ||
|
|
||
| problem = Explicit_Problem(self.unit_steady, states_init, t0=0) | ||
| solver = CVode(problem) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| import copy | ||
| import importlib.util | ||
| import json | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
|
|
||
|
|
||
| HAS_ASSIMULO = importlib.util.find_spec("assimulo") is not None | ||
|
|
||
| pytestmark = [ | ||
| pytest.mark.assimulo, | ||
| pytest.mark.integration, | ||
| pytest.mark.skipif( | ||
| not HAS_ASSIMULO, | ||
| reason="assimulo is not installed; solver-backed integration tests skipped", | ||
| ), | ||
| ] | ||
|
|
||
| if HAS_ASSIMULO: | ||
| from PharmaPy.Kinetics import RxnKinetics | ||
| from PharmaPy.Phases import LiquidPhase | ||
| from PharmaPy.Reactors import CSTR, BatchReactor, PlugFlowReactor | ||
| from PharmaPy.Reactors import SemibatchReactor | ||
| from PharmaPy.Streams import LiquidStream | ||
| from PharmaPy.Utilities import CoolingWater | ||
|
|
||
| SENSITIVITY_REACTORS = [CSTR, SemibatchReactor] | ||
| else: | ||
| SENSITIVITY_REACTORS = [] | ||
|
|
||
|
|
||
| def _load_pfr_config(data_path): | ||
| with open(data_path["integration"] / "pfr_test_constructor_kwargs.json") as f: | ||
| config = json.load(f) | ||
|
|
||
| config = copy.deepcopy(config) | ||
| tau = config["inlet"].pop("tau") | ||
| config["inlet"]["vol_flow"] = config["phase"]["vol"] / tau | ||
|
|
||
| datapath = str(data_path["integration"] / "pfr_test_pure_comp.json") | ||
| config["kinetics"].update({ | ||
| "stoich_matrix": [[-1, -1, 1], [0, -1, 1]], | ||
| "k_params": [40 / 60, 10 / 60], | ||
| "ea_params": [2e3, 1e3], | ||
| "delta_hrxn": [-5e3, -2.5e3], | ||
| }) | ||
| config["kinetics"]["path"] = datapath | ||
|
|
||
| return config, datapath | ||
|
|
||
|
|
||
| def _reactor_objects(data_path, reactor): | ||
| config, datapath = _load_pfr_config(data_path) | ||
|
|
||
| inlet = LiquidStream(datapath, **config["inlet"]) | ||
| phase = LiquidPhase(datapath, **config["phase"]) | ||
| kinetics = RxnKinetics(**config["kinetics"]) | ||
| utility = CoolingWater(**config["utility"]) | ||
|
|
||
| reactor.Inlet = inlet | ||
| reactor.Phases = phase | ||
| reactor.Kinetics = kinetics | ||
| reactor.Utility = utility | ||
|
|
||
| return reactor | ||
|
|
||
|
|
||
| def test_pfr_solve_steady_reads_inlet_mole_conc(data_path): | ||
| config, _ = _load_pfr_config(data_path) | ||
| reactor = _reactor_objects( | ||
| data_path, PlugFlowReactor(**config["reactor"]) | ||
| ) | ||
|
|
||
| vol_position, states = reactor.solve_steady(reactor.Liquid_1.vol) | ||
|
|
||
| vol_position = np.asarray(vol_position) | ||
|
|
||
| assert vol_position.size > 1 | ||
| assert states.shape[0] == vol_position.size | ||
| assert reactor.concProfSteady.shape[0] == vol_position.size | ||
|
bernalde marked this conversation as resolved.
|
||
| assert reactor.Kinetics.num_rxns == 2 | ||
| assert reactor.tempProfSteady[-1] > reactor.Inlet.temp | ||
| assert reactor.tempProfSteady[-1] < reactor.temp_ht_steady | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("reactor_cls", SENSITIVITY_REACTORS) | ||
| def test_sensitivity_mode_refuses_unsupported_reactors(data_path, reactor_cls): | ||
| if reactor_cls is CSTR: | ||
| reactor = reactor_cls() | ||
| else: | ||
| reactor = reactor_cls(vol_tank=0.002) | ||
|
|
||
| reactor = _reactor_objects(data_path, reactor) | ||
|
|
||
| with pytest.raises(NotImplementedError, match="sensitivity.*not supported"): | ||
| reactor.solve_unit(runtime=1, eval_sens=True, verbose=False) | ||
|
|
||
|
|
||
| def test_coil_ht_mode_refuses_unsupported_heat_transfer(): | ||
| reactor = BatchReactor(isothermal=False, ht_mode="coil") | ||
|
|
||
| with pytest.raises(NotImplementedError, match="coil.*not supported"): | ||
| reactor.heat_transfer(np.array([300.0]), np.array([290.0]), 0.002) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking (pairs with my comment on Every user reaches Please add a @pytest.mark.parametrize("reactor_cls", SENSITIVITY_REACTORS)
def test_coil_ht_mode_refuses_through_solve_unit(data_path, reactor_cls):
kwargs = {} if reactor_cls is CSTR else {"vol_tank": 0.002}
reactor = _reactor_objects(
data_path, reactor_cls(isothermal=False, ht_mode="coil", **kwargs))
with pytest.raises(NotImplementedError, match="coil.*not supported"):
reactor.solve_unit(runtime=1, verbose=False)That test is red on this head (it gets
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in |
||
|
|
||
|
|
||
| @pytest.mark.parametrize("reactor_cls", SENSITIVITY_REACTORS) | ||
| def test_coil_ht_mode_refuses_through_solve_unit(data_path, reactor_cls): | ||
| if reactor_cls is CSTR: | ||
| reactor = reactor_cls(isothermal=False, ht_mode="coil") | ||
| else: | ||
| reactor = reactor_cls( | ||
| vol_tank=0.002, isothermal=False, ht_mode="coil") | ||
|
|
||
| reactor = _reactor_objects(data_path, reactor) | ||
|
|
||
| with pytest.raises(NotImplementedError, match="coil.*not supported"): | ||
| reactor.solve_unit(runtime=1, verbose=False) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this, and any upcoming PR, we need to have a docs template that also discusses the arguments, their type, units (in [unit] format), and possible default value. I propose to use the Numpy format. This would also help us with the future type hinting