History matching of simulation models
We demonstrate history matching of a Direct Column Breakthrough (DCB) simulation model in Mocca. We leverage the powerful and flexible optimization functionality of Jutul to set up and perform the history matching. For more details about the DCB modelling, see the breakthrough curve example.
Import necessary modules
import Jutul
import Mocca
using Mocca.UnitsThe breakthrough
CO₂/N₂ fed at a constant interstitial velocity into an adiabatic column of zeolite 13X full of nitrogen, as in the breakthrough example but on a coarser grid.
gas = Mocca.haghpanah_2013_gas()
feed = Mocca.Feed(gas; composition = (CO2 = 0.15, N2 = 0.85), temperature = 298.15, pressure = 1bar)
fs = Mocca.Flowsheet(gas, feed)
Mocca.add_bed!(fs, :Bed, Mocca.load_adsorbent("zeolite_13x_haghpanah_2013"), Mocca.haghpanah_2013_column();
ncells = 50, thermal = Mocca.Adiabatic())
Mocca.add_device!(fs, :V_feed, :feed; at = (:Bed, :bottom))
Mocca.add_device!(fs, :V_product, :product; at = (:Bed, :top))
v_ref = 1.0 # interstitial velocity [m/s]
duration = 1000.0
process = Mocca.Process(fs, Mocca.Cycle([Mocca.Adsorb(duration; velocity = v_ref)]))Process: CO2/N2 feed at 1.0 bar, 298.15 K, capturing CO2
Bed: fixed bed of zeolite 13X, 50 cells
bottom V_feed (feed), from boundary at 1.0 bar
top V_product (product), to boundary at 1.0 bar
cycle of 1 stages, 1000.0 s:
adsorption 1000.0 s V_feed, V_productWe create a function for setting up new simulation cases from the value of the parameter we wish to tune, the feed velocity. The feed is the flow controller on V_feed, so a case for velocity v keeps the model, initial state and parameters of the process, and gives the controller the flow rate of v. The rate is the only thing that depends on v, and it accepts the automatic differentiation numbers the optimizer passes in.
bed = process.flowsheet[:Bed]
stage = only(Mocca.stages(process))
state0 = Mocca.initial_state(process)
function setup_case(prm, step_info = missing)
settings = copy(stage.settings)
rate = Mocca.interstitial_flow_rate(bed, prm["velocity"])
settings[:V_feed] = merge(settings[:V_feed], (law = Mocca.VolumetricFlow(rate),))
forces, dt = Mocca.setup_schedule(process.flowsheet, process.model, [Mocca.Stage(stage.name, stage.duration, settings)];
max_dt = 20.0)
return Mocca.MoccaCase(process.model, dt, forces; state0 = state0, parameters = process.parameters)
end;Create synthetic reference data
case_ref = setup_case(Dict("velocity" => v_ref));Configure simulator which will be used in the history matching. The time step selector limits the change of the bed's variables per step.
sim, cfg = Mocca.setup_process_simulator(case_ref.model, case_ref.state0, case_ref.parameters;
timestep_selector_cfg = (Bed = (y = 0.01, Temperature = 10.0, Pressure = 10.0),),
initial_dt = 1.0,
output_substates = true,
info_level = -1
);Run reference simulation to generate "ground truth" data from the result
states, timesteps_out = Mocca.simulate_process(case_ref;
simulator = sim,
config = cfg
);
times_ref = cumsum(timesteps_out)
total_time = times_ref[end]
last_cell_idx = Jutul.number_of_cells(bed.domain)
qCO2_ref = map(s -> s[:Bed][:AdsorbedConcentration][1, last_cell_idx], states)
qCO2_ref_by_time = Jutul.get_1d_interpolator(times_ref, qCO2_ref);Setting up and solving the optimization problem
We define a suitable objective function to quantify the match between our simulations and the reference solution. Here we choose deviation of adsorbed CO2 in the last grid cell.
function objective_function(model, state, dt, step_info, forces)
current_time = step_info[:time]
q_co2 = state[:Bed][:AdsorbedConcentration][1, last_cell_idx]
q_co2_ref = qCO2_ref_by_time(current_time)
v = dt/total_time*(q_co2 - q_co2_ref)^2
return v
end;Perturb the known velocity to form our initial guess for the optimization
prm_guess = Dict("velocity" => 1.5 * v_ref);Activate the velocity as a free parameter
dprm = Jutul.DictOptimization.DictParameters(prm_guess)
Jutul.DictOptimization.free_optimization_parameter!(dprm, "velocity"; rel_min = 0.001, rel_max = 100.0)DictParameters with 1 parameters (1 active), and 0 multipliers:
Active optimization parameters
┌──────────┬───────────────┬───────┬────────┬───────┐
│ Name │ Initial value │ Count │ Min │ Max │
├──────────┼───────────────┼───────┼────────┼───────┤
│ velocity │ 1.5 │ 1 │ 0.0015 │ 150.0 │
└──────────┴───────────────┴───────┴────────┴───────┘
No inactive optimization parameters.
No multipliers set.Run the optimization
prm_opt = Jutul.DictOptimization.optimize(dprm, objective_function, setup_case;
config = cfg,
max_it = 10,
obj_change_tol = 1e-6,
solution_history = true
);Optimization: Starting calibration of 1 parameters.
Optimization: Setting up adjoint storage.
Optimization: Finished setup in 23.838673119 seconds.
Optimization: Adjoint solve took 25.400090685 seconds.
Optimization: Objective #1: 3.41251e+05, gradient 2-norm: 5.60913e+05
It. | Objective | Proj. grad | Linesearch-its
-----------------------------------------------
0 | 6.0839e-01 | 1.5000e+02 | -
Optimization: Adjoint solve took 0.283890538 seconds.
Optimization: Objective #2: 1.15577e+06 (f/f0=3.387e+00), gradient 2-norm: 1.57824e-06
Optimization: Adjoint solve took 1.686406295 seconds.
Optimization: Objective #3: 2.33199e+05 (f/f0=6.834e-01), gradient 2-norm: 7.11058e+05
Optimization: Adjoint solve took 0.281744032 seconds.
Optimization: Objective #4: 1.15577e+06 (f/f0=3.387e+00), gradient 2-norm: 1.57824e-06
Optimization: Adjoint solve took 1.573315477 seconds.
Optimization: Objective #5: 1.19065e+05 (f/f0=3.489e-01), gradient 2-norm: 8.88513e+05
Optimization: Adjoint solve took 0.316145633 seconds.
Optimization: Objective #6: 1.15577e+06 (f/f0=3.387e+00), gradient 2-norm: 1.57824e-06
LBFGS: Line search unable to succeed in 5 iterations ...
Optimization: Adjoint solve took 1.615250145 seconds.
Optimization: Objective #7: 1.19065e+05 (f/f0=3.489e-01), gradient 2-norm: 8.88513e+05
1 | 2.1227e-01 | 1.5000e+02 | 5
Optimization: Adjoint solve took 0.285128613 seconds.
Optimization: Objective #8: 1.15577e+06 (f/f0=3.387e+00), gradient 2-norm: 1.57824e-06
Optimization: Adjoint solve took 1.567689016 seconds.
Optimization: Objective #9: 1.60637e+04 (f/f0=4.707e-02), gradient 2-norm: 6.24670e+05
2 | 2.8638e-02 | 2.3760e+02 | 2
Optimization: Adjoint solve took 1.665152495 seconds.
Optimization: Objective #10: 3.84195e+05 (f/f0=1.126e+00), gradient 2-norm: 2.31961e+06
Optimization: Adjoint solve took 1.559754176 seconds.
Optimization: Objective #11: 3.43361e+02 (f/f0=1.006e-03), gradient 2-norm: 1.05343e+05
3 | 6.1215e-04 | 1.6705e+02 | 2
Optimization: Adjoint solve took 1.61470303 seconds.
Optimization: Objective #12: 3.35912e+01 (f/f0=9.844e-05), gradient 2-norm: 3.17097e+04
4 | 5.9887e-05 | 2.8171e+01 | 1
Optimization: Adjoint solve took 1.596517821 seconds.
Optimization: Objective #13: 1.21890e-02 (f/f0=3.572e-08), gradient 2-norm: 6.35802e+02
5 | 2.1731e-08 | 8.4798e+00 | 1
Optimization: Adjoint solve took 1.628947701 seconds.
Optimization: Objective #14: 1.91768e-05 (f/f0=5.620e-11), gradient 2-norm: 2.52183e+01
6 | 3.4189e-11 | 1.7003e-01 | 1
Optimization: Finished in 82.921433371 seconds.We can see a clear reduction of the objective function value throughout the optimization iterations, indicating a close match between the reference solution and our simulation.
f = Mocca.plot_optimization_history(dprm)
We can look at the optimization result:
dprmDictParameters with 1 parameters (1 active), and 0 multipliers:
Active optimization parameters
┌──────────┬───────────────┬───────┬────────┬───────┬─────────────────┬────────┐
│ Name │ Initial value │ Count │ Min │ Max │ Optimized value │ Change │
├──────────┼───────────────┼───────┼────────┼───────┼─────────────────┼────────┤
│ velocity │ 1.5 │ 1 │ 0.0015 │ 150.0 │ 1.0 │ -33.0% │
└──────────┴───────────────┴───────┴────────┴───────┴─────────────────┴────────┘
No inactive optimization parameters.
No multipliers set.and see that the value matches the reference velocity
v_ref1.0Check size of objective mismatch
dprm.history.objectives14-element Vector{Float64}:
341251.459936567
1.1557668218285488e6
233198.9439900214
1.1557668218285488e6
119064.67405367695
1.1557668218285488e6
119064.67405367695
1.1557668218285488e6
16063.689469353185
384194.65311626997
343.36124993472805
33.59118728313624
0.012189042128415083
1.917683358537295e-5Example on GitHub
If you would like to run this example yourself, it can be downloaded from the Mocca.jl GitHub repository.
This page was generated using Literate.jl.