From 8611b60c50dc3389d824192a7ed435a1a23f4817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Brostr=C3=B8m?= Date: Mon, 29 Apr 2024 12:47:24 +0200 Subject: [PATCH 1/9] Add dummy implementation of ML-based relperm evaluation --- examples/hybrid_simulation_relperm.jl | 102 ++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 examples/hybrid_simulation_relperm.jl diff --git a/examples/hybrid_simulation_relperm.jl b/examples/hybrid_simulation_relperm.jl new file mode 100644 index 00000000..f8502d06 --- /dev/null +++ b/examples/hybrid_simulation_relperm.jl @@ -0,0 +1,102 @@ + + +using JutulDarcy, Jutul +Darcy, bar, kg, meter, day = si_units(:darcy, :bar, :kilogram, :meter, :day) +nx = ny = 25 +nz = 10 +cart_dims = (nx, ny, nz) +physical_dims = (1000.0, 1000.0, 100.0).*meter +g = CartesianMesh(cart_dims, physical_dims) +domain = reservoir_domain(g, permeability = 0.3Darcy, porosity = 0.2) +Injector = setup_vertical_well(domain, 1, 1, name = :Injector); +Producer = setup_well(domain, (nx, ny, 1), name = :Producer); + + +phases = (LiquidPhase(), VaporPhase()) +rhoLS = 1000.0kg/meter^3 +rhoGS = 100.0kg/meter^3 +reference_densities = [rhoLS, rhoGS] +sys = ImmiscibleSystem(phases, reference_densities = reference_densities) + +model, parameters = setup_reservoir_model(domain, sys, wells = [Injector, Producer]) + +""" +Machine Learning-based method for computing relative permeabilites +""" +struct MLModelRelativePermeabilities{T} <: JutulDarcy.AbstractRelativePermeabilities + test_value::T + function MLModelRelativePermeabilities(input_test_value) + new{typeof(input_test_value)}(input_test_value) + end +end + +Jutul.@jutul_secondary function update_kr!(kr, kr_def::MLModelRelativePermeabilities, model, Saturations, ix) + test_value = kr_def.test_value + for i in ix + for ph in axes(kr, 1) + S = Saturations[ph, i] + kr[ph, i] = S*test_value + end + end + return kr +end + +c = [1e-6, 1e-4]/bar +density = ConstantCompressibilityDensities( + p_ref = 100*bar, + density_ref = reference_densities, + compressibility = c +) +#kr = BrooksCoreyRelativePermeabilities(sys, [2.0, 3.0]) +#replace_variables!(model, PhaseMassDensities = density, BrooksCoreyRelativePermeabilities = kr); +kr = MLModelRelativePermeabilities(1.0) +rmodel = reservoir_model(model) +replace_variables!(rmodel, RelativePermeabilities = kr, throw = true) + +state0 = setup_reservoir_state(model, + Pressure = 120bar, + Saturations = [1.0, 0.0] +) +# ### Set up schedule with driving forces + +nstep = 25 +dt = fill(365.0day, nstep) +pv = pore_volume(model, parameters) +inj_rate = 1.5*sum(pv)/sum(dt) +rate_target = TotalRateTarget(inj_rate) +I_ctrl = InjectorControl(rate_target, [0.0, 1.0], density = rhoGS) +bhp_target = BottomHolePressureTarget(100bar) +P_ctrl = ProducerControl(bhp_target) +controls = Dict(:Injector => I_ctrl, :Producer => P_ctrl) +forces = setup_reservoir_forces(model, control = controls) + +wd, states, t = simulate_reservoir(state0, model, dt, parameters = parameters, forces = forces) +## +wd(:Producer) +## +wd(:Injector, :bhp) +# ### Plot the well rates +using GLMakie + +grat = wd[:Producer, :grat] +lrat = wd[:Producer, :lrat] +bhp = wd[:Injector, :bhp] + +fig = Figure(size = (1200, 400)) + +ax = Axis(fig[1, 1], + title = "Injector", + xlabel = "Time / days", + ylabel = "Bottom hole pressure / bar") +lines!(ax, t/day, bhp./bar) + +ax = Axis(fig[1, 2], + title = "Producer", + xlabel = "Time / days", + ylabel = "Production rate / m³/day") +lines!(ax, t/day, abs.(grat).*day) +lines!(ax, t/day, abs.(lrat).*day) + +fig +# ### Launch interactive plotting of reservoir values +plot_reservoir(model, states, key = :Saturations, step = 3) From d417f3101dc560348f5258a335b130438124b3af Mon Sep 17 00:00:00 2001 From: jakobtorben Date: Fri, 3 May 2024 10:49:41 +0200 Subject: [PATCH 2/9] Add notebook to train ML based Brooks Corey method --- examples/BrooksCoreyMLModel.bson | Bin 0 -> 22512 bytes examples/train_BrooksCorey.jl | 112 +++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 examples/BrooksCoreyMLModel.bson create mode 100644 examples/train_BrooksCorey.jl diff --git a/examples/BrooksCoreyMLModel.bson b/examples/BrooksCoreyMLModel.bson new file mode 100644 index 0000000000000000000000000000000000000000..d3beaef8caa1f1e5bb2a62482d9c265f90e3d467 GIT binary patch literal 22512 zcmeHOOK%)S5U%Vv;DA8L*B}V7!s|eG9)3k2Vy_i25M#x*KwPY`cO0+Shc!E6IUper zKL8{^xd0aqTq1Ge!V&%l@&$20f`0&Cbx)hw-FYN-?49xU9CmuAYr4Ct>+yA0&p+=G z8Ldv;Ym)T>Q6+G1)1ySx4g7E`AS1Z5>(NJJQOyO^)ZM@pPses$-)(hi2DL_&u4WSb zWeB1JG$8s&Z0S4HqNSxwqVuSwv9{at=vgML(j&MnFE<;TR58V$N#ARRcxae3$t29w zI0~EY)`z~gO%wd{5*~=lurjD#QB+Zv&)`40#Yem&0vK1Tg^Gq0xIFkO8cr>}O?R(zF zX0Ob}dxQwRrkWqH?ezJu@Q%3T{5^&8M3osiyvD_pO_+y_f@gIZHcg={xb1g17g2$x z>Jcp(h_{OA--85`<&-|$L#QE#YoQ5fg0{g?w82?zgKS%{!IyDMw86-nl?@7Vi#m#Y zz)vixnmh-Ux2lllDLoU0Xp+Vq!c%G70Sy~i9mgV{#vKA{rQr@T41_#^JFb7rg%Q9V ziqRwEjvn`?aYyo|i?+dj#GPh?*EH^6zf%U>VQJi<_d5hK&I7a>cVyy@?=|j-eBJ}> zcWB%pa$tJTQSR&?chB)vjXTPMJ8b$YHP13)Su?p4^tpy18ittSfWdLnF27$t-2TvM zm*g$qDD9$hEl9g)7@}ba0_Vd@eIER8gJT?zah>_pr((2s3ol@LDvM&9IMK-dv|0+2ouE~F4VI4oYf1}xP1Di9|@PIfKAude9 z$t^~rLj;%5Q(x+yfqyNW_kA}yxODmwP^wrA30Nbq1NIOkP%IsAh9H4r`GKh+NT66c zFg*kb6iWwYh9H4r>A>s|Bv33Jm>YrwilqZ)mH4!H`VPq^Y5@vQ9K2nT;Em6kzp=6| z5L=gzzA}Pa#3}1r_$C4)F-`i3DO0~-IIZ!%$fKx=v++ISeH_m*)>bqc2R@uS}o#xy;w9CUFt4idww> zb|z75T+cCK?|E5nx3<0&>~C~=SP?pZIz|{BxaYOqzyaS+|MbuM8tp9quE|jWot!COeDz<`tA)=#nuXY0XfWuNdn09cFx&hT+( zX~LOR%~7%9g)_?>d0cJ*G%_FbVHN2U>(rT5t^ zz0qa?G?pox*?FL;c;U=8bvP?iI75a!dndDaxZKvcQyZYMRE{hUG!@X1O>0M1Dn~X` zK1ZeznXPl5jy@3Q05p~eY+ NS*aY^T-h8M(SJ@g{ssU5 literal 0 HcmV?d00001 diff --git a/examples/train_BrooksCorey.jl b/examples/train_BrooksCorey.jl new file mode 100644 index 00000000..30405a66 --- /dev/null +++ b/examples/train_BrooksCorey.jl @@ -0,0 +1,112 @@ +using Flux, CUDA, Statistics, ProgressMeter, Plots, BSON + +# Brooks Corey relperm formula that we are trying to learn + +# s = saturation (variable range 0.0, 1.0) +# n = Exponents for each phase (range from 1.0 to 6.0) set to constant 2 +# sr = Residual saturations for each phase (set to 0.2) +# kwm = The maximum relative permeabilities (range from 0.0 to 1.0) set to constant 1.0 +# sr_tot = Total residual saturation over all phases i.e. S_or + S_gr + S_wr. (range 0.0 to 1.0) should sr*number of phases + +# the output should be between 0 and 1 + +function brooks_corey_relperm(s::T, n::Real, sr::Real, kwm::Real, sr_tot::Real) where T + den = 1 - sr_tot + sat = (s - sr) / den + sat = clamp(sat, zero(T), one(T)) + return kwm*sat^n +end + +# Generate some data for training a model to represent BrooksCorey function +#training_sat = rand(Float32, 1000); # 1×1000 Matrix{Float32} [0.0,1.0] +training_sat = collect(range(Float32(0), stop=Float32(1), length=10000)) +rel_perm_analytical = Array{Float32, 1}(undef, 10000); # Creates a one-dimensional array of Float32 with 1000 elements + +training_sat = reshape(training_sat, 1, :) +rel_perm_analytical = reshape(rel_perm_analytical, 1, :) + +println("Size of training_sat: ", size(training_sat)) +println("Size of rel_perm_analytical: ", size(rel_perm_analytical)) +println("Shape of input data: ", size(training_sat)) +println("Shape of output data: ", size(rel_perm_analytical)) +println("Number of training samples: ", length(training_sat)) + +for i in eachindex(training_sat) + rel_perm_analytical[i] = brooks_corey_relperm(training_sat[i], 2.0, 0.2, 1.0, 0.4) +end + +plot(vec(training_sat), vec(rel_perm_analytical), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") + +# Define our model, a multi-layer perceptron with two hidden layers of size 100 +model = Chain( + Dense(1 => 100, relu; init=Flux.glorot_normal), # activation function inside layer + Dense(100 => 100, relu; init=Flux.glorot_normal), + Dense(100 => 100, relu; init=Flux.glorot_normal), + Dense(100 => 1; init=Flux.glorot_normal), + relu) |> gpu # move model to GPU, if available + +# The model takes in the saturation with the shape (1xN) +rel_perm_predicted = model(training_sat |> gpu) |> cpu # 1×1000 Matrix{Float32} +# The output of hte model is the relative Permeability with shape (1xN) + +# To train the model, we use batches of 64 samples +loader = Flux.DataLoader((training_sat, rel_perm_analytical) |> gpu, batchsize=256, shuffle=true); + +optim = Flux.setup(Flux.Adam(0.0001), model) # will store optimiser momentum, etc. + +# Training loop, using the whole data set 1000 times: +losses = [] +@showprogress for epoch in 1:1000 + for (x, y) in loader + loss, grads = Flux.withgradient(model) do m + # Evaluate model and loss inside gradient context: + y_hat = m(x) + Flux.mse(y_hat, y) + end + Flux.update!(optim, model, grads[1]) + push!(losses, loss) # logging, outside gradient context + end +end + +# plot loss function + +plot(losses; xaxis=(:log10, "iteration"), + yaxis="loss", label="per batch") +n = length(loader) +plot!(n:n:length(losses), mean.(Iterators.partition(losses, n)), + label="epoch mean", dpi=200) + +# Predict on the trained model +rel_perm_pred = model(training_sat |> gpu) |> cpu + +plot(vec(training_sat), vec(rel_perm_analytical), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") +plot!(vec(training_sat), vec(rel_perm_pred), label="ML model RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") + +using BSON: @save + +@save "BrooksCoreyMLModel.bson" model + +using Flux, BSON + +BSON.@load "BrooksCoreyMLModel.bson" model + +# test on random inputs, different to the training set + +# Generate 1000 random numbers between 0 and 1 +testing_sat = rand(Float32, 1000) +# sort for easier plotting +testing_sat = sort(testing_sat) +testing_sat = reshape(testing_sat, 1, :) + +# Calculate analytical solution using Brooks Corey relperm +test_y = Array{Float32, 1}(undef, 1000) +test_y = reshape(test_y, 1, :) +for i in eachindex(testing_sat) + test_y[i] = brooks_corey_relperm(testing_sat[i], 2.0, 0.2, 1.0, 0.4) +end + +# Predict on the trained model +pred_y = model(testing_sat |> gpu) |> cpu + +plot(vec(testing_sat), vec(test_y), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") +plot!(vec(testing_sat), vec(pred_y), label="ML model RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") \ No newline at end of file From e0e411ad40d86214ac067a4b36c467c87a346fe0 Mon Sep 17 00:00:00 2001 From: jakobtorben Date: Wed, 14 Aug 2024 20:28:59 +0200 Subject: [PATCH 3/9] Improve rel perm model and add validation --- examples/BrooksCoreyMLModel.bson | Bin 22512 -> 70181 bytes examples/train_BrooksCorey.jl | 195 ++++++++++++++++++++++++++----- 2 files changed, 168 insertions(+), 27 deletions(-) diff --git a/examples/BrooksCoreyMLModel.bson b/examples/BrooksCoreyMLModel.bson index d3beaef8caa1f1e5bb2a62482d9c265f90e3d467..6e1eb33d4acaa7b30acab95aef2750f685c924f4 100644 GIT binary patch literal 70181 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 NS*aY^T-h8M(SJ@g{ssU5 diff --git a/examples/train_BrooksCorey.jl b/examples/train_BrooksCorey.jl index 30405a66..f8384851 100644 --- a/examples/train_BrooksCorey.jl +++ b/examples/train_BrooksCorey.jl @@ -18,9 +18,10 @@ function brooks_corey_relperm(s::T, n::Real, sr::Real, kwm::Real, sr_tot::Real) end # Generate some data for training a model to represent BrooksCorey function -#training_sat = rand(Float32, 1000); # 1×1000 Matrix{Float32} [0.0,1.0] -training_sat = collect(range(Float32(0), stop=Float32(1), length=10000)) -rel_perm_analytical = Array{Float32, 1}(undef, 10000); # Creates a one-dimensional array of Float32 with 1000 elements +#training_sat = rand(Float64, 1000); # 1×1000 Matrix{Float64} [0.0,1.0] +training_sat = collect(range(Float64(0), stop=Float64(1), length=500000)) +#training_sat = vcat(zeros(1000), training_sat, ones(1000)) # important to be well behaved at 0.0 and 1.0. so adding training data in this range +rel_perm_analytical = Array{Float64, 1}(undef, 500000); training_sat = reshape(training_sat, 1, :) rel_perm_analytical = reshape(rel_perm_analytical, 1, :) @@ -37,76 +38,216 @@ end plot(vec(training_sat), vec(rel_perm_analytical), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") -# Define our model, a multi-layer perceptron with two hidden layers of size 100 -model = Chain( - Dense(1 => 100, relu; init=Flux.glorot_normal), # activation function inside layer - Dense(100 => 100, relu; init=Flux.glorot_normal), - Dense(100 => 100, relu; init=Flux.glorot_normal), - Dense(100 => 1; init=Flux.glorot_normal), - relu) |> gpu # move model to GPU, if available +struct CustomModel{T <: Chain} # Parameter to avoid type instability + chain::T + end + +function (m::CustomModel)(x) + return (1.0.-x).*0.5.*(tanh.(m.chain(x)) .+ 1.0).*x .+ x + #min.(max.(0.0, m.chain(x)), 1.0) # clamping between 0 and 1 + end + +# Call @layer to allow for training. Described below in more detail. +Flux.@layer CustomModel + # The model takes in the saturation with the shape (1xN) -rel_perm_predicted = model(training_sat |> gpu) |> cpu # 1×1000 Matrix{Float32} -# The output of hte model is the relative Permeability with shape (1xN) +# The output of the model is the relative Permeability with shape (1xN) + + +# Define our model, a multi-layer perceptron with three hidden layers of size 50 +# use tanh as we want a smooth first derivative +MLP = f64(Chain( # use float64 to match Jutul + Dense(1 => 50, tanh; init=Flux.glorot_normal), # activation function inside layer + Dense(50 => 50, tanh; init=Flux.glorot_normal), + Dense(50 => 50, tanh; init=Flux.glorot_normal), + Dense(50 => 50, tanh; init=Flux.glorot_normal), + # use sigmoid in final activation to ensure output is between 0 and 1 + Dense(50 => 1, sigmoid; init=Flux.glorot_normal))) -# To train the model, we use batches of 64 samples -loader = Flux.DataLoader((training_sat, rel_perm_analytical) |> gpu, batchsize=256, shuffle=true); + BrooksCoreyMLModel = f64(MLP |> gpu) # move model to GPU, if available) -optim = Flux.setup(Flux.Adam(0.0001), model) # will store optimiser momentum, etc. + # alternatively use custom final activation function + #BrooksCoreyMLModel = CustomModel(MLP) + + +# To train the model, we use batches of 10000 +loader = Flux.DataLoader((training_sat, rel_perm_analytical) |> gpu, batchsize=10000, shuffle=true); + +optim = Flux.setup(Flux.Adam(0.0001), BrooksCoreyMLModel) # will store optimiser momentum, etc. # Training loop, using the whole data set 1000 times: losses = [] @showprogress for epoch in 1:1000 for (x, y) in loader - loss, grads = Flux.withgradient(model) do m + loss, grads = Flux.withgradient(BrooksCoreyMLModel) do m # Evaluate model and loss inside gradient context: y_hat = m(x) Flux.mse(y_hat, y) end - Flux.update!(optim, model, grads[1]) + Flux.update!(optim, BrooksCoreyMLModel, grads[1]) push!(losses, loss) # logging, outside gradient context end end + # plot loss function plot(losses; xaxis=(:log10, "iteration"), - yaxis="loss", label="per batch") + yaxis=(:log10, "loss"), label="per batch") n = length(loader) plot!(n:n:length(losses), mean.(Iterators.partition(losses, n)), label="epoch mean", dpi=200) # Predict on the trained model -rel_perm_pred = model(training_sat |> gpu) |> cpu +rel_perm_pred = BrooksCoreyMLModel(training_sat |> gpu) |> cpu plot(vec(training_sat), vec(rel_perm_analytical), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") plot!(vec(training_sat), vec(rel_perm_pred), label="ML model RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") -using BSON: @save +plot(vec(training_sat), vec(rel_perm_pred - rel_perm_analytical), label="ML model RelPerm - analytical", xlabel="Saturation", ylabel="Relative Permeability", title="Relative Permeability Error") + +println("Norm: ", norm(rel_perm_pred - rel_perm_analytical)) -@save "BrooksCoreyMLModel.bson" model +BrooksCoreyMLModel = f64(BrooksCoreyMLModel |> cpu) +@save "BrooksCoreyMLModel.bson" BrooksCoreyMLModel -using Flux, BSON -BSON.@load "BrooksCoreyMLModel.bson" model +BSON.@load "BrooksCoreyMLModel.bson" BrooksCoreyMLModel +BrooksCoreyMLModel = f64(BrooksCoreyMLModel |> gpu) # test on random inputs, different to the training set # Generate 1000 random numbers between 0 and 1 -testing_sat = rand(Float32, 1000) +testing_sat = rand(Float64, 1000) +# add 0 and 1 to testing_sat +testing_sat[1] = 0.0 +testing_sat[end] = 1.0 # sort for easier plotting testing_sat = sort(testing_sat) testing_sat = reshape(testing_sat, 1, :) # Calculate analytical solution using Brooks Corey relperm -test_y = Array{Float32, 1}(undef, 1000) +test_y = Array{Float64, 1}(undef, 1000) test_y = reshape(test_y, 1, :) for i in eachindex(testing_sat) test_y[i] = brooks_corey_relperm(testing_sat[i], 2.0, 0.2, 1.0, 0.4) end # Predict on the trained model -pred_y = model(testing_sat |> gpu) |> cpu +pred_y = BrooksCoreyMLModel(testing_sat |> gpu) |> cpu plot(vec(testing_sat), vec(test_y), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") -plot!(vec(testing_sat), vec(pred_y), label="ML model RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") \ No newline at end of file +plot!(vec(testing_sat), vec(pred_y), label="ML model RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") + +function f_brooks_corey_relperm(s::T, n::Real=2, sr::Real=0.2, kwm::Real=1.0, sr_tot::Real=0.4) where T + den = 1 - sr_tot + sat = (s - sr) / den + sat = clamp(sat, zero(T), one(T)) + return kwm*sat^n +end + +#= +Evaluate the gradient of the Brooks Corey relperm function and ML model + + +For the parameters we have used, we can easily calculate the analytical expression of the gradient for our function (ignoring the clamp function for now): + +function f_brooks_corey_relperm(s) +den = 0.6 +sat = kwm*(s - sr) / den = (1*(s - 0.2) / 0.6) +return 1*sat^2 + +-> + +f(s) = 1*(s/0.6 - 0.2/0.6)^2 + +f'(s) = 2*((s - 0.2)/0.6)*1/0.6 = 50/9 * (s - 0.2) +=# + +using ForwardDiff + +BrooksCoreyMLModel = BrooksCoreyMLModel |> cpu + +println("f_brooks_corey_relperm(0.5): ", f_brooks_corey_relperm(0.5)) + +s = 0.5 + +ForwardDiff.derivative(f_brooks_corey_relperm, s) + +f_gradients = Array{Float64, 1}(undef, 1000); + +for i in eachindex(testing_sat) + f_gradients[i] = ForwardDiff.derivative(f_brooks_corey_relperm, testing_sat[i]) +end + +f_model_gradients_zygote = gradient(testing_sat -> sum(BrooksCoreyMLModel(testing_sat)), testing_sat) + +f_model_gradients_forwardDiff = ForwardDiff.gradient(testing_sat -> sum(BrooksCoreyMLModel(testing_sat)), testing_sat) +#f_model_gradients_forwardDiff = diag(ForwardDiff.jacobian(testing_sat -> BrooksCoreyMLModel(testing_sat), testing_sat)) + +f_gradients_analytic = Array{Float64, 1}(undef, 1000); +for i in eachindex(testing_sat) + f_gradients_analytic[i] = 50/9*(testing_sat[i]-0.2) + # manually adding the effect of the clamp function + if (testing_sat[i] < 0.2) || (testing_sat[i] > 0.8) + f_gradients_analytic[i] = 0 + end +end + +println("Every 100th element of f_gradients_analytic:") +println(f_gradients_analytic[1:100:end]) + +plot(vec(testing_sat[1:1000]), vec(f_gradients[1:1000]), marker=(:circle,2), label="Brooks Coorey function derivative", xlabel="Saturation", ylabel="Relative Permeability derivative", title="Derivative comparison") +plot!(vec(testing_sat[1:1000]), vec(f_gradients_analytic[1:1000]), marker=(:circle,2), label="Brooks Coorey analytical derivative", xlabel="Saturation", ylabel="Relative Permeability", title="Derivative comparison") +plot!(vec(testing_sat[1:1000]), vec(f_model_gradients_zygote[1][1:1000]), marker=(:circle,2), label="Brooks Coorey ML model derivative (Zygote)", xlabel="Saturation", ylabel="Relative Permeability", title="Derivative comparison") +plot!(vec(testing_sat[1:1000]), vec(f_model_gradients_forwardDiff[1:1000]), marker=(:circle,2), label="Brooks Coorey ML model derivative (ForwardDiff)", xlabel="Saturation", ylabel="Relative Permeability", title="Derivative comparison") + +plot(vec(testing_sat), vec(vec(f_model_gradients_forwardDiff) - f_gradients), label="ML model derivative - analytical", xlabel="Saturation", ylabel="Relative Permeability derivative error", title="Derivative error") + +plot(vec(testing_sat), vec(vec(f_model_gradients_zygote[1]) - vec(f_model_gradients_forwardDiff)), label="Zygote - ForwardDiff", xlabel="Saturation", ylabel="Relative Permeability derivative", title="Zygote - ForwardDiff") + +println("f_brooks_corey_relperm(0.0):") +print(f_brooks_corey_relperm(0.0)) +s = 0.0 +println("\nDerivative at s = 0.0:") +println(ForwardDiff.derivative(f_brooks_corey_relperm, s)) + +println("\nf_brooks_corey_relperm(1.0):") +print(f_brooks_corey_relperm(1.0)) +s = 1.0 +println("\nDerivative at s = 1.0:") +println(ForwardDiff.derivative(f_brooks_corey_relperm, s)) + +println("\nf_brooks_corey_relperm(0.5):") +print(f_brooks_corey_relperm(0.5)) +s = 0.5 +println("\nDerivative at s = 0.5:") +println(ForwardDiff.derivative(f_brooks_corey_relperm, s)) + +s = [0.5] +pred_y_0 = BrooksCoreyMLModel(s) +println("\nBrooksCoreyMLModel([0.5]):") +println(pred_y_0) + +s = [0.5] +println("\nGradient of BrooksCoreyMLModel at s = [0.5]:") +println(ForwardDiff.gradient(s -> BrooksCoreyMLModel(s)[1], s)) + +s = [0.0] +pred_y_0 = BrooksCoreyMLModel(s) +println("\nBrooksCoreyMLModel([0.0]):") +println(pred_y_0) + +s = [0.0] +println("\nGradient of BrooksCoreyMLModel at s = [0.0]:") +println(ForwardDiff.gradient(s -> BrooksCoreyMLModel(s)[1], s)) + +s = [1.0] +pred_y_0 = BrooksCoreyMLModel(s) +println("\nBrooksCoreyMLModel([1.0]):") +println(pred_y_0) + +s = [1.0] +println("\nGradient of BrooksCoreyMLModel at s = [1.0]:") +println(ForwardDiff.gradient(s -> BrooksCoreyMLModel(s)[1], s)) \ No newline at end of file From 93fce938bdf218795bc2d2ab186ecde51c3ce8fd Mon Sep 17 00:00:00 2001 From: jakobtorben Date: Wed, 14 Aug 2024 20:29:33 +0200 Subject: [PATCH 4/9] Add example with DNN based rel perm --- examples/hybrid_simulation_relperm.jl | 44 ++++++++++++++++++--------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/examples/hybrid_simulation_relperm.jl b/examples/hybrid_simulation_relperm.jl index f8502d06..bb9629f8 100644 --- a/examples/hybrid_simulation_relperm.jl +++ b/examples/hybrid_simulation_relperm.jl @@ -1,6 +1,6 @@ -using JutulDarcy, Jutul +using JutulDarcy, Jutul, Flux, BSON, CUDA Darcy, bar, kg, meter, day = si_units(:darcy, :bar, :kilogram, :meter, :day) nx = ny = 25 nz = 10 @@ -23,21 +23,29 @@ model, parameters = setup_reservoir_model(domain, sys, wells = [Injector, Produc """ Machine Learning-based method for computing relative permeabilites """ -struct MLModelRelativePermeabilities{T} <: JutulDarcy.AbstractRelativePermeabilities - test_value::T - function MLModelRelativePermeabilities(input_test_value) - new{typeof(input_test_value)}(input_test_value) + +struct MLModelRelativePermeabilities{M} <: JutulDarcy.AbstractRelativePermeabilities + ML_model::M + function MLModelRelativePermeabilities(input_ML_model) + new{typeof(input_ML_model)}(input_ML_model) end end Jutul.@jutul_secondary function update_kr!(kr, kr_def::MLModelRelativePermeabilities, model, Saturations, ix) - test_value = kr_def.test_value - for i in ix - for ph in axes(kr, 1) - S = Saturations[ph, i] - kr[ph, i] = S*test_value - end + ML_model = kr_def.ML_model + for ph in axes(kr, 1) + # processing all the cells in one batch on the gpu + sat_batch = reshape(Saturations[ph, :], 1, :) |> gpu # Reshape to 1 x n matrix + kr[ph, :] = vec(ML_model(sat_batch)) |> cpu end + # use analytical function + #for i in ix + # for ph in axes(kr, 1) + # S = [Saturations[ph, i]] + # result = JutulDarcy.brooks_corey_relperm(S[1], 2.0, 0.2, 1.0, 0.4) + # kr[ph, i] = result[1] + # end + #end return kr end @@ -47,9 +55,15 @@ density = ConstantCompressibilityDensities( density_ref = reference_densities, compressibility = c ) -#kr = BrooksCoreyRelativePermeabilities(sys, [2.0, 3.0]) -#replace_variables!(model, PhaseMassDensities = density, BrooksCoreyRelativePermeabilities = kr); -kr = MLModelRelativePermeabilities(1.0) + +jutul_dir = realpath(joinpath(@__DIR__, "..")) +model_path = joinpath(jutul_dir, "examples", "BrooksCoreyMLModel.bson") +BSON.@load model_path BrooksCoreyMLModel + +BrooksCoreyMLModel = BrooksCoreyMLModel |> gpu # move model to gpu + + +kr = MLModelRelativePermeabilities(BrooksCoreyMLModel) rmodel = reservoir_model(model) replace_variables!(rmodel, RelativePermeabilities = kr, throw = true) @@ -99,4 +113,4 @@ lines!(ax, t/day, abs.(lrat).*day) fig # ### Launch interactive plotting of reservoir values -plot_reservoir(model, states, key = :Saturations, step = 3) +plot_reservoir(model, states, key = :Saturations, step = 3) \ No newline at end of file From c1e203fff60be06790fc859bb6bd7f6cb626c02e Mon Sep 17 00:00:00 2001 From: jakobtorben Date: Mon, 21 Oct 2024 16:49:35 +0200 Subject: [PATCH 5/9] Update hybrid simulation example to Literate style --- docs/Project.toml | 3 + docs/make.jl | 1 + examples/BrooksCoreyMLModel.bson | Bin 70181 -> 0 bytes examples/hybrid_simulation_relperm.jl | 354 ++++++++++++++++++++------ examples/train_BrooksCorey.jl | 253 ------------------ 5 files changed, 277 insertions(+), 334 deletions(-) delete mode 100644 examples/BrooksCoreyMLModel.bson delete mode 100644 examples/train_BrooksCorey.jl diff --git a/docs/Project.toml b/docs/Project.toml index 4ea34f58..93bc15e5 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -3,6 +3,7 @@ DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" +Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" GeoEnergyIO = "3b1dd628-313a-45bb-9d8d-8f3c48dcb5d4" GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" @@ -16,3 +17,5 @@ Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" MultiComponentFlash = "35e5bd01-9722-4017-9deb-64a5d32478ff" NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a" Optim = "429524aa-4258-5aef-a3af-852621145aeb" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" diff --git a/docs/make.jl b/docs/make.jl index 6606d926..a024419d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -39,6 +39,7 @@ function build_jutul_darcy_docs(build_format = nothing; "CO2 injection in saline aquifer" => "co2_sloped", "Compositional with five components" => "compositional_5components", "Parameter matching of Buckley-Leverett" => "optimize_simple_bl", + "Hybrid simulation with relative permeability" => "hybrid_simulation_relperm", "Validation: SPE1" => "validation_spe1", "Validation: SPE9" => "validation_spe9", "Validation: Compositional" => "validation_compositional", diff --git a/examples/BrooksCoreyMLModel.bson b/examples/BrooksCoreyMLModel.bson deleted file mode 100644 index 6e1eb33d4acaa7b30acab95aef2750f685c924f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70181 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 I_ctrl, :Producer => P_ctrl) + + model, parameters = setup_reservoir_model(domain, sys, wells = [Injector, Producer]) + forces = setup_reservoir_forces(model, control = controls); + + return model, parameters, forces, sys, dt +end + +ref_model, ref_parameters, ref_forces, ref_sys, ref_dt = setup_simulation_case(); + +# The simulation model has a set of default secondary variables (properties) that are used to compute the flow equations. We can have a look at the reservoir model to see what the defaults are for the Darcy flow part of the domain: + +reservoir_model(ref_model) + +# The secondary variables can be swapped out, replaced and new variables can be added with arbitrary functional dependencies thanks to Jutul's flexible setup for automatic differentiation. Let us adjust the defaults by replacing the relative permeabilities with Brooks-Corey functions: + +# ### Brooks-Corey +# The Brooks-Corey model is a simple model that can be used to generate relative +# permeabilities. The model is defined in the mobile region as: +# +# ``k_{rw} = k_{max,w} \bar{S}_w`` +# +# ``k_{rg} = k_{max,g} \bar{S}_g`` +# +# where $k_{max,w}$ is the maximum relative permeability, $\bar{S}_w$ +# is the normalized saturation for the water phase, +# +# `` \bar{S}_w = \frac{S_w - S_{wi}}{1 - S_{wi} - S_{rg}}`` +# +# and, similarly, for the vapor phase: +# +# ``\bar{S}_g = \frac{S_g - S_{rg}}{1 - S_{wi} - S_{rg}}`` +# +# We use the Brooks Corey function available in JutulDarcy to evaluate the values for a given saturation range. +# For simplicity, we use the same exponent and residual saturation for both the liquid and vapour phase, such that we only need to train a single model +exponent = 2.0 +sr_g = sr_w = 0.2 +r_tot = sr_g + sr_w; + +kr = BrooksCoreyRelativePermeabilities(ref_sys, [exponent, exponent], [sr_w, sr_g]) +replace_variables!(ref_model, RelativePermeabilities = kr); + + +# ### Run the reference simulation + +# We then set up the initial state with constant pressure and liquid-filled reservoir. +# The inputs (pressure and saturations) must match the model's primary variables. +# We can now run the simulation. + +ref_state0 = setup_reservoir_state(ref_model, + Pressure = 120bar, + Saturations = [1.0, 0.0] +) + +ref_wd, ref_states, ref_t = simulate_reservoir(ref_state0, ref_model, ref_dt, parameters = ref_parameters, forces = ref_forces); + + +# ## Training a neural network to compute relative permeability +# The next step is to train a neural network to learn the Brooks-Corey relative permeability curve. +# While using a neural network to learn a simple analytical function is not typically practical, it serves as a +# good example for integrating machine learning with reservoir simulation. + + +# First we must generate some data for training a model to represent the Brooks Corey function. +train_samples = 1000 +test_samples = 1000 + +training_sat = collect(range(Float64(0), stop=Float64(1), length=train_samples)) +training_sat = reshape(training_sat, 1, :) + +rel_perm_analytical = JutulDarcy.brooks_corey_relperm.(training_sat, n = exponent, residual = sr_g, residual_total = r_tot) +plot(vec(training_sat), vec(rel_perm_analytical), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") -phases = (LiquidPhase(), VaporPhase()) -rhoLS = 1000.0kg/meter^3 -rhoGS = 100.0kg/meter^3 -reference_densities = [rhoLS, rhoGS] -sys = ImmiscibleSystem(phases, reference_densities = reference_densities) +# ### Define the neural network architecture +# Next we define the neural network architecture. The model takes in a saturation value and outputs a relative permeability value. +# For a batched input, such as the number of cells in the model, the input and output +# shapes are (1xN_cells). -model, parameters = setup_reservoir_model(domain, sys, wells = [Injector, Producer]) -""" -Machine Learning-based method for computing relative permeabilites -""" +# We define our neural network architecture for relative permeability prediction +# using a multi-layer perceptron (MLP) with the following characteristics: +# - Input layer: 1 neuron (saturation value) +# - Three hidden layers: 16 neurons each, with tanh activation +# - Output layer: 1 neuron with sigmoid activation (relative permeability) +# +# Key design choices: +# - Use of tanh activation in hidden layers for smooth first derivatives +# - Sigmoid in the final layer to constrain output to [0, 1] range +# - Float64 precision to match JutulDarcy's numerical precision +# - Glorot normal initialization for weights +# - Use GPU for faster training (if available) + +MLP = f64(Chain( + Dense(1 => 16, tanh; init=Flux.glorot_normal), + Dense(16 => 16, tanh; init=Flux.glorot_normal), + Dense(16 => 16, tanh; init=Flux.glorot_normal), + Dense(16 => 1, sigmoid; init=Flux.glorot_normal))) + +BrooksCoreyMLModel = f64(MLP) + +# Define training parameters +# We train the model using the Adam optimizer with a learning rate of 0.0005. For a total of 10 epochs. +# The `optim` object will store the optimiser momentum, etc. + +epochs = 20000 +batchsize = 1000 +lr = 0.0005 +loader = Flux.DataLoader((training_sat, rel_perm_analytical), batchsize=batchsize, shuffle=true); +optim = Flux.setup(Flux.Adam(lr), BrooksCoreyMLModel); + +# Training loop, using the whole data set epochs number of times: +# Evaluate model and loss inside gradient context +# logging, outside gradient context. +losses = [] +@showprogress for epoch in 1:epochs + for (x, y) in loader + loss, grads = Flux.withgradient(BrooksCoreyMLModel) do m + y_hat = m(x) + Flux.mse(y_hat, y) + end + Flux.update!(optim, BrooksCoreyMLModel, grads[1]) + push!(losses, loss) + end +end + +# The loss function is plotted to show that the model is learning. + +plot(losses; xaxis=(:log10, "iteration"), + yaxis=(:log10, "loss"), label="per batch") +n = length(loader) +plot!(n:n:length(losses), mean.(Iterators.partition(losses, n)), + label="epoch mean", dpi=200) + + +# To test the trained model , we generate some test data, different to the training set + +testing_sat = sort([0.0; rand(Float64, test_samples-2); 1.0]) +testing_sat = reshape(testing_sat, 1, :) + +# Next, we calculate the analytical solution and predicted values with the trained model. +test_y = JutulDarcy.brooks_corey_relperm.(testing_sat, n = exponent, residual = sr_g, residual_total = r_tot) +pred_y = BrooksCoreyMLModel(testing_sat) + +plot(vec(testing_sat), vec(test_y), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") +plot!(vec(testing_sat), vec(pred_y), label="ML model RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") + +# The plot demonstrates that our neural network has successfully learned to approximate the Brooks-Corey relative permeability curve. +# This close match between the analytical solution and the ML model's predictions indicates that we can use this trained neural network in our simulation model. + + +# ## Replacing the relative permeability function with our neural network + +# Now we can replace the relative permeability function with our neural network. +# We define a new type `MLModelRelativePermeabilities` that wraps our neural network model and implements the `update_kr!` function. +# This function is called by the simulator to update the relative permeability values for the liquid and vapour phase. +# A potential benefit of using a neural network, is that we can compute all the cells in parallel, and access to highly optimised GPU acceleration is trivial. struct MLModelRelativePermeabilities{M} <: JutulDarcy.AbstractRelativePermeabilities ML_model::M @@ -34,83 +231,78 @@ end Jutul.@jutul_secondary function update_kr!(kr, kr_def::MLModelRelativePermeabilities, model, Saturations, ix) ML_model = kr_def.ML_model for ph in axes(kr, 1) - # processing all the cells in one batch on the gpu - sat_batch = reshape(Saturations[ph, :], 1, :) |> gpu # Reshape to 1 x n matrix - kr[ph, :] = vec(ML_model(sat_batch)) |> cpu + sat_batch = reshape(Saturations[ph, :], 1, :) + kr[ph, :] .= vec(ML_model(sat_batch)) end - # use analytical function - #for i in ix - # for ph in axes(kr, 1) - # S = [Saturations[ph, i]] - # result = JutulDarcy.brooks_corey_relperm(S[1], 2.0, 0.2, 1.0, 0.4) - # kr[ph, i] = result[1] - # end - #end - return kr end -c = [1e-6, 1e-4]/bar -density = ConstantCompressibilityDensities( - p_ref = 100*bar, - density_ref = reference_densities, - compressibility = c -) +# Since JutulDarcy uses automatic differentiation, our new realtive permeability model needs to be differentiable. +# This is not a problem for our neural network model, since differentiatiability is a necessary condition for machine learning models. +# One thing to note, is that the machine learning library Flux uses Zygote.jl for automatic differentiation, while Jutul uses ForwardDiff.jl. +# This is not a problem, as the gradient of our simple neural network is fully compatible with ForwardDiff.jl, so no middlelayer is needed. +# We can now replace the default relative permeability model with our new model. + + +ml_model, ml_parameters, ml_forces, ml_sys, ml_dt = setup_simulation_case() -jutul_dir = realpath(joinpath(@__DIR__, "..")) -model_path = joinpath(jutul_dir, "examples", "BrooksCoreyMLModel.bson") -BSON.@load model_path BrooksCoreyMLModel +ml_kr = MLModelRelativePermeabilities(BrooksCoreyMLModel) +replace_variables!(ml_model, RelativePermeabilities = ml_kr); -BrooksCoreyMLModel = BrooksCoreyMLModel |> gpu # move model to gpu +# We can now inspect the model to see that the relative permeability model has been replaced. +reservoir_model(ml_model) -kr = MLModelRelativePermeabilities(BrooksCoreyMLModel) -rmodel = reservoir_model(model) -replace_variables!(rmodel, RelativePermeabilities = kr, throw = true) -state0 = setup_reservoir_state(model, +# ### Run the simulation + +ml_state0 = setup_reservoir_state(ml_model, Pressure = 120bar, Saturations = [1.0, 0.0] ) -# ### Set up schedule with driving forces - -nstep = 25 -dt = fill(365.0day, nstep) -pv = pore_volume(model, parameters) -inj_rate = 1.5*sum(pv)/sum(dt) -rate_target = TotalRateTarget(inj_rate) -I_ctrl = InjectorControl(rate_target, [0.0, 1.0], density = rhoGS) -bhp_target = BottomHolePressureTarget(100bar) -P_ctrl = ProducerControl(bhp_target) -controls = Dict(:Injector => I_ctrl, :Producer => P_ctrl) -forces = setup_reservoir_forces(model, control = controls) - -wd, states, t = simulate_reservoir(state0, model, dt, parameters = parameters, forces = forces) -## -wd(:Producer) -## -wd(:Injector, :bhp) -# ### Plot the well rates + +ml_wd, ml_states, ml_t = simulate_reservoir(ml_state0, ml_model, ml_dt, parameters = ml_parameters, forces = ml_forces) + + +# ### Compare results + + +# We can now compare the results of the reference simulation and the simulation with the neural network-based relative permeability model. using GLMakie +function plot_comparison(ref_wd, ml_wd, ref_t, ml_t) + fig = Figure(size = (1200, 800)) + + ax1 = Axis(fig[1, 1], title = "Injector BHP", xlabel = "Time (days)", ylabel = "Pressure (bar)") + lines!(ax1, ref_t/day, ref_wd[:Injector, :bhp]./bar, label = "Brooks-Corey") + lines!(ax1, ml_t/day, ml_wd[:Injector, :bhp]./bar, label = "ML Model") + axislegend(ax1) + + ax2 = Axis(fig[1, 2], title = "Producer BHP", xlabel = "Time (days)", ylabel = "Pressure (bar)") + lines!(ax2, ref_t/day, ref_wd[:Producer, :bhp]./bar, label = "Brooks-Corey") + lines!(ax2, ml_t/day, ml_wd[:Producer, :bhp]./bar, label = "ML Model") + axislegend(ax2) + + ax3 = Axis(fig[2, 1], title = "Producer Liquid Rate", xlabel = "Time (days)", ylabel = "Rate (m³/day)") + lines!(ax3, ref_t/day, abs.(ref_wd[:Producer, :lrat]).*day, label = "Brooks-Corey") + lines!(ax3, ml_t/day, abs.(ml_wd[:Producer, :lrat]).*day, label = "ML Model") + axislegend(ax3) + + ax4 = Axis(fig[2, 2], title = "Producer Gas Rate", xlabel = "Time (days)", ylabel = "Rate (m³/day)") + lines!(ax4, ref_t/day, abs.(ref_wd[:Producer, :grat]).*day, label = "Brooks-Corey") + lines!(ax4, ml_t/day, abs.(ml_wd[:Producer, :grat]).*day, label = "ML Model") + axislegend(ax4) + + return fig +end -grat = wd[:Producer, :grat] -lrat = wd[:Producer, :lrat] -bhp = wd[:Injector, :bhp] +plot_comparison(ref_wd, ml_wd, ref_t, ml_t) -fig = Figure(size = (1200, 400)) +# From the plot, we can see that the neural network-based relative permeability model is able to match the reference simulation to an acceptable level. -ax = Axis(fig[1, 1], - title = "Injector", - xlabel = "Time / days", - ylabel = "Bottom hole pressure / bar") -lines!(ax, t/day, bhp./bar) +# Interactive visualization of the 3D results is also possible if GLMakie is loaded: -ax = Axis(fig[1, 2], - title = "Producer", - xlabel = "Time / days", - ylabel = "Production rate / m³/day") -lines!(ax, t/day, abs.(grat).*day) -lines!(ax, t/day, abs.(lrat).*day) +plot_reservoir(ml_model, ml_states, key = :Saturations, step = 3) -fig -# ### Launch interactive plotting of reservoir values -plot_reservoir(model, states, key = :Saturations, step = 3) \ No newline at end of file +# This example demonstrates how to integrate a neural network model for relative +# permeability into a reservoir simulation using JutulDarcy.jl. While we used a +# simple Brooks-Corey model for demonstration, this approach can be extended to +# more complex scenarios where analytical models may not be sufficient. \ No newline at end of file diff --git a/examples/train_BrooksCorey.jl b/examples/train_BrooksCorey.jl deleted file mode 100644 index f8384851..00000000 --- a/examples/train_BrooksCorey.jl +++ /dev/null @@ -1,253 +0,0 @@ -using Flux, CUDA, Statistics, ProgressMeter, Plots, BSON - -# Brooks Corey relperm formula that we are trying to learn - -# s = saturation (variable range 0.0, 1.0) -# n = Exponents for each phase (range from 1.0 to 6.0) set to constant 2 -# sr = Residual saturations for each phase (set to 0.2) -# kwm = The maximum relative permeabilities (range from 0.0 to 1.0) set to constant 1.0 -# sr_tot = Total residual saturation over all phases i.e. S_or + S_gr + S_wr. (range 0.0 to 1.0) should sr*number of phases - -# the output should be between 0 and 1 - -function brooks_corey_relperm(s::T, n::Real, sr::Real, kwm::Real, sr_tot::Real) where T - den = 1 - sr_tot - sat = (s - sr) / den - sat = clamp(sat, zero(T), one(T)) - return kwm*sat^n -end - -# Generate some data for training a model to represent BrooksCorey function -#training_sat = rand(Float64, 1000); # 1×1000 Matrix{Float64} [0.0,1.0] -training_sat = collect(range(Float64(0), stop=Float64(1), length=500000)) -#training_sat = vcat(zeros(1000), training_sat, ones(1000)) # important to be well behaved at 0.0 and 1.0. so adding training data in this range -rel_perm_analytical = Array{Float64, 1}(undef, 500000); - -training_sat = reshape(training_sat, 1, :) -rel_perm_analytical = reshape(rel_perm_analytical, 1, :) - -println("Size of training_sat: ", size(training_sat)) -println("Size of rel_perm_analytical: ", size(rel_perm_analytical)) -println("Shape of input data: ", size(training_sat)) -println("Shape of output data: ", size(rel_perm_analytical)) -println("Number of training samples: ", length(training_sat)) - -for i in eachindex(training_sat) - rel_perm_analytical[i] = brooks_corey_relperm(training_sat[i], 2.0, 0.2, 1.0, 0.4) -end - -plot(vec(training_sat), vec(rel_perm_analytical), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") - -struct CustomModel{T <: Chain} # Parameter to avoid type instability - chain::T - end - -function (m::CustomModel)(x) - return (1.0.-x).*0.5.*(tanh.(m.chain(x)) .+ 1.0).*x .+ x - #min.(max.(0.0, m.chain(x)), 1.0) # clamping between 0 and 1 - end - -# Call @layer to allow for training. Described below in more detail. -Flux.@layer CustomModel - - -# The model takes in the saturation with the shape (1xN) -# The output of the model is the relative Permeability with shape (1xN) - - -# Define our model, a multi-layer perceptron with three hidden layers of size 50 -# use tanh as we want a smooth first derivative -MLP = f64(Chain( # use float64 to match Jutul - Dense(1 => 50, tanh; init=Flux.glorot_normal), # activation function inside layer - Dense(50 => 50, tanh; init=Flux.glorot_normal), - Dense(50 => 50, tanh; init=Flux.glorot_normal), - Dense(50 => 50, tanh; init=Flux.glorot_normal), - # use sigmoid in final activation to ensure output is between 0 and 1 - Dense(50 => 1, sigmoid; init=Flux.glorot_normal))) - - BrooksCoreyMLModel = f64(MLP |> gpu) # move model to GPU, if available) - - # alternatively use custom final activation function - #BrooksCoreyMLModel = CustomModel(MLP) - - -# To train the model, we use batches of 10000 -loader = Flux.DataLoader((training_sat, rel_perm_analytical) |> gpu, batchsize=10000, shuffle=true); - -optim = Flux.setup(Flux.Adam(0.0001), BrooksCoreyMLModel) # will store optimiser momentum, etc. - -# Training loop, using the whole data set 1000 times: -losses = [] -@showprogress for epoch in 1:1000 - for (x, y) in loader - loss, grads = Flux.withgradient(BrooksCoreyMLModel) do m - # Evaluate model and loss inside gradient context: - y_hat = m(x) - Flux.mse(y_hat, y) - end - Flux.update!(optim, BrooksCoreyMLModel, grads[1]) - push!(losses, loss) # logging, outside gradient context - end -end - - -# plot loss function - -plot(losses; xaxis=(:log10, "iteration"), - yaxis=(:log10, "loss"), label="per batch") -n = length(loader) -plot!(n:n:length(losses), mean.(Iterators.partition(losses, n)), - label="epoch mean", dpi=200) - -# Predict on the trained model -rel_perm_pred = BrooksCoreyMLModel(training_sat |> gpu) |> cpu - -plot(vec(training_sat), vec(rel_perm_analytical), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") -plot!(vec(training_sat), vec(rel_perm_pred), label="ML model RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") - -plot(vec(training_sat), vec(rel_perm_pred - rel_perm_analytical), label="ML model RelPerm - analytical", xlabel="Saturation", ylabel="Relative Permeability", title="Relative Permeability Error") - -println("Norm: ", norm(rel_perm_pred - rel_perm_analytical)) - -BrooksCoreyMLModel = f64(BrooksCoreyMLModel |> cpu) -@save "BrooksCoreyMLModel.bson" BrooksCoreyMLModel - - -BSON.@load "BrooksCoreyMLModel.bson" BrooksCoreyMLModel -BrooksCoreyMLModel = f64(BrooksCoreyMLModel |> gpu) - -# test on random inputs, different to the training set - -# Generate 1000 random numbers between 0 and 1 -testing_sat = rand(Float64, 1000) -# add 0 and 1 to testing_sat -testing_sat[1] = 0.0 -testing_sat[end] = 1.0 -# sort for easier plotting -testing_sat = sort(testing_sat) -testing_sat = reshape(testing_sat, 1, :) - -# Calculate analytical solution using Brooks Corey relperm -test_y = Array{Float64, 1}(undef, 1000) -test_y = reshape(test_y, 1, :) -for i in eachindex(testing_sat) - test_y[i] = brooks_corey_relperm(testing_sat[i], 2.0, 0.2, 1.0, 0.4) -end - -# Predict on the trained model -pred_y = BrooksCoreyMLModel(testing_sat |> gpu) |> cpu - -plot(vec(testing_sat), vec(test_y), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") -plot!(vec(testing_sat), vec(pred_y), label="ML model RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") - -function f_brooks_corey_relperm(s::T, n::Real=2, sr::Real=0.2, kwm::Real=1.0, sr_tot::Real=0.4) where T - den = 1 - sr_tot - sat = (s - sr) / den - sat = clamp(sat, zero(T), one(T)) - return kwm*sat^n -end - -#= -Evaluate the gradient of the Brooks Corey relperm function and ML model - - -For the parameters we have used, we can easily calculate the analytical expression of the gradient for our function (ignoring the clamp function for now): - -function f_brooks_corey_relperm(s) -den = 0.6 -sat = kwm*(s - sr) / den = (1*(s - 0.2) / 0.6) -return 1*sat^2 - --> - -f(s) = 1*(s/0.6 - 0.2/0.6)^2 - -f'(s) = 2*((s - 0.2)/0.6)*1/0.6 = 50/9 * (s - 0.2) -=# - -using ForwardDiff - -BrooksCoreyMLModel = BrooksCoreyMLModel |> cpu - -println("f_brooks_corey_relperm(0.5): ", f_brooks_corey_relperm(0.5)) - -s = 0.5 - -ForwardDiff.derivative(f_brooks_corey_relperm, s) - -f_gradients = Array{Float64, 1}(undef, 1000); - -for i in eachindex(testing_sat) - f_gradients[i] = ForwardDiff.derivative(f_brooks_corey_relperm, testing_sat[i]) -end - -f_model_gradients_zygote = gradient(testing_sat -> sum(BrooksCoreyMLModel(testing_sat)), testing_sat) - -f_model_gradients_forwardDiff = ForwardDiff.gradient(testing_sat -> sum(BrooksCoreyMLModel(testing_sat)), testing_sat) -#f_model_gradients_forwardDiff = diag(ForwardDiff.jacobian(testing_sat -> BrooksCoreyMLModel(testing_sat), testing_sat)) - -f_gradients_analytic = Array{Float64, 1}(undef, 1000); -for i in eachindex(testing_sat) - f_gradients_analytic[i] = 50/9*(testing_sat[i]-0.2) - # manually adding the effect of the clamp function - if (testing_sat[i] < 0.2) || (testing_sat[i] > 0.8) - f_gradients_analytic[i] = 0 - end -end - -println("Every 100th element of f_gradients_analytic:") -println(f_gradients_analytic[1:100:end]) - -plot(vec(testing_sat[1:1000]), vec(f_gradients[1:1000]), marker=(:circle,2), label="Brooks Coorey function derivative", xlabel="Saturation", ylabel="Relative Permeability derivative", title="Derivative comparison") -plot!(vec(testing_sat[1:1000]), vec(f_gradients_analytic[1:1000]), marker=(:circle,2), label="Brooks Coorey analytical derivative", xlabel="Saturation", ylabel="Relative Permeability", title="Derivative comparison") -plot!(vec(testing_sat[1:1000]), vec(f_model_gradients_zygote[1][1:1000]), marker=(:circle,2), label="Brooks Coorey ML model derivative (Zygote)", xlabel="Saturation", ylabel="Relative Permeability", title="Derivative comparison") -plot!(vec(testing_sat[1:1000]), vec(f_model_gradients_forwardDiff[1:1000]), marker=(:circle,2), label="Brooks Coorey ML model derivative (ForwardDiff)", xlabel="Saturation", ylabel="Relative Permeability", title="Derivative comparison") - -plot(vec(testing_sat), vec(vec(f_model_gradients_forwardDiff) - f_gradients), label="ML model derivative - analytical", xlabel="Saturation", ylabel="Relative Permeability derivative error", title="Derivative error") - -plot(vec(testing_sat), vec(vec(f_model_gradients_zygote[1]) - vec(f_model_gradients_forwardDiff)), label="Zygote - ForwardDiff", xlabel="Saturation", ylabel="Relative Permeability derivative", title="Zygote - ForwardDiff") - -println("f_brooks_corey_relperm(0.0):") -print(f_brooks_corey_relperm(0.0)) -s = 0.0 -println("\nDerivative at s = 0.0:") -println(ForwardDiff.derivative(f_brooks_corey_relperm, s)) - -println("\nf_brooks_corey_relperm(1.0):") -print(f_brooks_corey_relperm(1.0)) -s = 1.0 -println("\nDerivative at s = 1.0:") -println(ForwardDiff.derivative(f_brooks_corey_relperm, s)) - -println("\nf_brooks_corey_relperm(0.5):") -print(f_brooks_corey_relperm(0.5)) -s = 0.5 -println("\nDerivative at s = 0.5:") -println(ForwardDiff.derivative(f_brooks_corey_relperm, s)) - -s = [0.5] -pred_y_0 = BrooksCoreyMLModel(s) -println("\nBrooksCoreyMLModel([0.5]):") -println(pred_y_0) - -s = [0.5] -println("\nGradient of BrooksCoreyMLModel at s = [0.5]:") -println(ForwardDiff.gradient(s -> BrooksCoreyMLModel(s)[1], s)) - -s = [0.0] -pred_y_0 = BrooksCoreyMLModel(s) -println("\nBrooksCoreyMLModel([0.0]):") -println(pred_y_0) - -s = [0.0] -println("\nGradient of BrooksCoreyMLModel at s = [0.0]:") -println(ForwardDiff.gradient(s -> BrooksCoreyMLModel(s)[1], s)) - -s = [1.0] -pred_y_0 = BrooksCoreyMLModel(s) -println("\nBrooksCoreyMLModel([1.0]):") -println(pred_y_0) - -s = [1.0] -println("\nGradient of BrooksCoreyMLModel at s = [1.0]:") -println(ForwardDiff.gradient(s -> BrooksCoreyMLModel(s)[1], s)) \ No newline at end of file From 0d4efcd1e2880515e0f722baa51b2ce89a759c6a Mon Sep 17 00:00:00 2001 From: jakobtorben Date: Tue, 22 Oct 2024 11:00:12 +0200 Subject: [PATCH 6/9] Migrate hybrid example from Flux.jl to Lux.jl --- docs/Project.toml | 8 ++- examples/hybrid_simulation_relperm.jl | 99 +++++++++++++++++---------- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 93bc15e5..a337f2d8 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,12 +1,13 @@ [deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" -Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" GeoEnergyIO = "3b1dd628-313a-45bb-9d8d-8f3c48dcb5d4" GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" +Lux = "b2108857-7c20-44ae-9111-449ecde12c47" HYPRE = "b5ffcf37-a2bd-41ab-a3da-4bd9bc8ad771" Jutul = "2b460a1a-8a2b-45b2-b125-b5c536396eb9" JutulDarcy = "82210473-ab04-4dce-b31b-11573c4f8e0a" @@ -17,5 +18,8 @@ Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" MultiComponentFlash = "35e5bd01-9722-4017-9deb-64a5d32478ff" NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a" Optim = "429524aa-4258-5aef-a3af-852621145aeb" +Optimisers = "3bd65402-5787-11e9-1adc-39752487f4e2" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" -ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" \ No newline at end of file diff --git a/examples/hybrid_simulation_relperm.jl b/examples/hybrid_simulation_relperm.jl index 98fac967..9ec01004 100644 --- a/examples/hybrid_simulation_relperm.jl +++ b/examples/hybrid_simulation_relperm.jl @@ -14,9 +14,10 @@ # ## Preliminaries # First, let's import the necessary packages. -# We will use Flux for the neural network model. +# We will use Lux for the neural network model, due to its explicit representation of the model and the ability to use different optimisers, ideal for integration with Jutul. +# However, Flux.jl would work just as well for this simple example. -using JutulDarcy, Jutul, Flux, ProgressMeter, Plots +using JutulDarcy, Jutul, Lux, ADTypes, Zygote, Optimisers, Random, Plots, Statistics # ## Set up the simulation case # We set up a reference simulation case following the [Your first JutulDarcy.jl simulation](https://sintefmath.github.io/JutulDarcy.jl/dev/man/first_ex) example: @@ -156,46 +157,65 @@ plot(vec(training_sat), vec(rel_perm_analytical), label="Brooks-Corey RelPerm", # - Glorot normal initialization for weights # - Use GPU for faster training (if available) -MLP = f64(Chain( - Dense(1 => 16, tanh; init=Flux.glorot_normal), - Dense(16 => 16, tanh; init=Flux.glorot_normal), - Dense(16 => 16, tanh; init=Flux.glorot_normal), - Dense(16 => 1, sigmoid; init=Flux.glorot_normal))) - -BrooksCoreyMLModel = f64(MLP) +BrooksCoreyMLModel = Chain( + Dense(1 => 16, tanh), + Dense(16 => 16, tanh), + Dense(16 => 16, tanh), + Dense(16 => 1, sigmoid) +) # Define training parameters # We train the model using the Adam optimizer with a learning rate of 0.0005. For a total of 10 epochs. # The `optim` object will store the optimiser momentum, etc. -epochs = 20000 -batchsize = 1000 -lr = 0.0005 -loader = Flux.DataLoader((training_sat, rel_perm_analytical), batchsize=batchsize, shuffle=true); -optim = Flux.setup(Flux.Adam(lr), BrooksCoreyMLModel); +epochs = 20000; +lr = 0.0005; # Training loop, using the whole data set epochs number of times: -# Evaluate model and loss inside gradient context -# logging, outside gradient context. -losses = [] -@showprogress for epoch in 1:epochs - for (x, y) in loader - loss, grads = Flux.withgradient(BrooksCoreyMLModel) do m - y_hat = m(x) - Flux.mse(y_hat, y) +# We use Adam for the optimiser, set the random seed and initialise the parameters. +# Lux defaults to float32 precision, so we need to convert the parameters to float64. +# Lux uses a stateless, explicit representation of the model. It consists of four parts: +# - model - the model architecture +# - parameters - the learnable parameters of the model +# - states - the state of the model, e.g. the hidden states of the recurrent model +# - optimiser state - the state of the optimiser, e.g. the momentum +# In addition, we need to define a rule for automatic differentiation. Here we use Zygote. + +rng = MersenneTwister(42) +Random.seed!(rng, 42) + +function train_model(BrooksCoreyMLModel, training_sat, rel_perm_analytical, epochs, lr) + rng = MersenneTwister(42) + Random.seed!(rng, 42) + opt = Optimisers.Adam(lr) + ps, st = Lux.setup(rng, BrooksCoreyMLModel) + ps = ps |> f64 + tstate = Lux.Training.TrainState(BrooksCoreyMLModel, ps, st, opt) + vjp_rule = ADTypes.AutoZygote() + loss_function = Lux.MSELoss() + + losses = [] + for epoch in 1:epochs + epoch_losses = [] + _, loss, _, tstate = Lux.Training.single_train_step!(vjp_rule, loss_function, (training_sat, rel_perm_analytical), tstate) + push!(epoch_losses, loss) + append!(losses, epoch_losses) + if epoch % 1000 == 1 || epoch == epochs + println("Epoch: $(lpad(epoch, 3)) \t Loss: $(round(mean(epoch_losses), sigdigits=5))") end - Flux.update!(optim, BrooksCoreyMLModel, grads[1]) - push!(losses, loss) end + + return tstate, losses end +tstate, losses = train_model(BrooksCoreyMLModel, training_sat, rel_perm_analytical, epochs, lr); + # The loss function is plotted to show that the model is learning. -plot(losses; xaxis=(:log10, "iteration"), - yaxis=(:log10, "loss"), label="per batch") -n = length(loader) -plot!(n:n:length(losses), mean.(Iterators.partition(losses, n)), - label="epoch mean", dpi=200) +plot(losses, xlabel="Iteration", ylabel="Loss", label="per batch", yscale=:log10) +plot!(epochs:epochs:length(losses), mean.(Iterators.partition(losses, epochs)), +label="epoch mean", dpi=200) +title!("Training Loss") # To test the trained model , we generate some test data, different to the training set @@ -205,7 +225,7 @@ testing_sat = reshape(testing_sat, 1, :) # Next, we calculate the analytical solution and predicted values with the trained model. test_y = JutulDarcy.brooks_corey_relperm.(testing_sat, n = exponent, residual = sr_g, residual_total = r_tot) -pred_y = BrooksCoreyMLModel(testing_sat) +pred_y = Lux.apply(BrooksCoreyMLModel, testing_sat, tstate.parameters, tstate.states)[1] plot(vec(testing_sat), vec(test_y), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") plot!(vec(testing_sat), vec(pred_y), label="ML model RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") @@ -221,31 +241,36 @@ plot!(vec(testing_sat), vec(pred_y), label="ML model RelPerm", xlabel="Saturatio # This function is called by the simulator to update the relative permeability values for the liquid and vapour phase. # A potential benefit of using a neural network, is that we can compute all the cells in parallel, and access to highly optimised GPU acceleration is trivial. -struct MLModelRelativePermeabilities{M} <: JutulDarcy.AbstractRelativePermeabilities +struct MLModelRelativePermeabilities{M, P, S} <: JutulDarcy.AbstractRelativePermeabilities ML_model::M - function MLModelRelativePermeabilities(input_ML_model) - new{typeof(input_ML_model)}(input_ML_model) + parameters::P + states::S + function MLModelRelativePermeabilities(input_ML_model, parameters, states) + new{typeof(input_ML_model), typeof(parameters), typeof(states)}(input_ML_model, parameters, states) end end Jutul.@jutul_secondary function update_kr!(kr, kr_def::MLModelRelativePermeabilities, model, Saturations, ix) ML_model = kr_def.ML_model + ps = kr_def.parameters + st = kr_def.states for ph in axes(kr, 1) - sat_batch = reshape(Saturations[ph, :], 1, :) - kr[ph, :] .= vec(ML_model(sat_batch)) + sat_batch = reshape(Saturations[ph, :], 1, length(Saturations[ph, :])) + kr_pred, st = Lux.apply(ML_model, sat_batch, ps, st) + @inbounds kr[ph, :] .= vec(kr_pred) end end # Since JutulDarcy uses automatic differentiation, our new realtive permeability model needs to be differentiable. # This is not a problem for our neural network model, since differentiatiability is a necessary condition for machine learning models. -# One thing to note, is that the machine learning library Flux uses Zygote.jl for automatic differentiation, while Jutul uses ForwardDiff.jl. +# One thing to note, is that we are using Lux with Zygote.jl for automatic differentiation, while Jutul uses ForwardDiff.jl. # This is not a problem, as the gradient of our simple neural network is fully compatible with ForwardDiff.jl, so no middlelayer is needed. # We can now replace the default relative permeability model with our new model. ml_model, ml_parameters, ml_forces, ml_sys, ml_dt = setup_simulation_case() -ml_kr = MLModelRelativePermeabilities(BrooksCoreyMLModel) +ml_kr = MLModelRelativePermeabilities(BrooksCoreyMLModel, tstate.parameters, tstate.states) replace_variables!(ml_model, RelativePermeabilities = ml_kr); # We can now inspect the model to see that the relative permeability model has been replaced. From 6542dc253caf75d21aa2c68d57962e90100e943f Mon Sep 17 00:00:00 2001 From: jakobtorben Date: Tue, 22 Oct 2024 15:39:09 +0200 Subject: [PATCH 7/9] Add SimpleChains bonus section to hybrid example --- docs/Project.toml | 1 + examples/hybrid_simulation_relperm.jl | 112 ++++++++++++++++++++------ 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index a337f2d8..7b6be866 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -21,5 +21,6 @@ Optim = "429524aa-4258-5aef-a3af-852621145aeb" Optimisers = "3bd65402-5787-11e9-1adc-39752487f4e2" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SimpleChains = "de6bee2f-e2f4-4ec7-b6ed-219cc6f6e9e5" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" \ No newline at end of file diff --git a/examples/hybrid_simulation_relperm.jl b/examples/hybrid_simulation_relperm.jl index 9ec01004..8aabbad3 100644 --- a/examples/hybrid_simulation_relperm.jl +++ b/examples/hybrid_simulation_relperm.jl @@ -112,14 +112,15 @@ replace_variables!(ref_model, RelativePermeabilities = kr); # We then set up the initial state with constant pressure and liquid-filled reservoir. # The inputs (pressure and saturations) must match the model's primary variables. -# We can now run the simulation. +# We can now run the simulation (where we first run a warmup step to avoid JIT compilation overhead). ref_state0 = setup_reservoir_state(ref_model, Pressure = 120bar, Saturations = [1.0, 0.0] ) -ref_wd, ref_states, ref_t = simulate_reservoir(ref_state0, ref_model, ref_dt, parameters = ref_parameters, forces = ref_forces); +simulate_reservoir(ref_state0, ref_model, ref_dt, parameters = ref_parameters, forces = ref_forces, info_level = -1); +ref_wd, ref_states, ref_t = simulate_reservoir(ref_state0, ref_model, ref_dt, parameters = ref_parameters, forces = ref_forces, info_level = 1); # ## Training a neural network to compute relative permeability @@ -179,29 +180,32 @@ lr = 0.0005; # - parameters - the learnable parameters of the model # - states - the state of the model, e.g. the hidden states of the recurrent model # - optimiser state - the state of the optimiser, e.g. the momentum + # In addition, we need to define a rule for automatic differentiation. Here we use Zygote. -rng = MersenneTwister(42) +rng = Random.default_rng() Random.seed!(rng, 42) -function train_model(BrooksCoreyMLModel, training_sat, rel_perm_analytical, epochs, lr) - rng = MersenneTwister(42) - Random.seed!(rng, 42) - opt = Optimisers.Adam(lr) - ps, st = Lux.setup(rng, BrooksCoreyMLModel) - ps = ps |> f64 - tstate = Lux.Training.TrainState(BrooksCoreyMLModel, ps, st, opt) - vjp_rule = ADTypes.AutoZygote() - loss_function = Lux.MSELoss() - - losses = [] - for epoch in 1:epochs - epoch_losses = [] - _, loss, _, tstate = Lux.Training.single_train_step!(vjp_rule, loss_function, (training_sat, rel_perm_analytical), tstate) - push!(epoch_losses, loss) - append!(losses, epoch_losses) - if epoch % 1000 == 1 || epoch == epochs - println("Epoch: $(lpad(epoch, 3)) \t Loss: $(round(mean(epoch_losses), sigdigits=5))") +function train_model(ml_model, training_sat, rel_perm_analytical, epochs, lr) + opt = Optimisers.Adam(lr); + ps, st = Lux.setup(rng, ml_model); + ps = ps |> f64; + tstate = Lux.Training.TrainState(ml_model, ps, st, opt); + vjp_rule = ADTypes.AutoZygote(); + loss_function = Lux.MSELoss(); + + warmup_data = rand(Float64, 1, 1); + Training.compute_gradients(vjp_rule, loss_function, (warmup_data, warmup_data), tstate) + @time begin + losses = [] + for epoch in 1:epochs + epoch_losses = [] + _, loss, _, tstate = Lux.Training.single_train_step!(vjp_rule, loss_function, (training_sat, rel_perm_analytical), tstate); + push!(epoch_losses, loss) + append!(losses, epoch_losses) + if epoch % 1000 == 0 || epoch == epochs + println("Epoch: $(lpad(epoch, 3)) \t Loss: $(round(mean(epoch_losses), sigdigits=5))") + end end end @@ -262,10 +266,10 @@ Jutul.@jutul_secondary function update_kr!(kr, kr_def::MLModelRelativePermeabili end # Since JutulDarcy uses automatic differentiation, our new realtive permeability model needs to be differentiable. -# This is not a problem for our neural network model, since differentiatiability is a necessary condition for machine learning models. +# This is inherently satisfied by our neural network model, as differentiability is a core requirement for machine learning models. # One thing to note, is that we are using Lux with Zygote.jl for automatic differentiation, while Jutul uses ForwardDiff.jl. -# This is not a problem, as the gradient of our simple neural network is fully compatible with ForwardDiff.jl, so no middlelayer is needed. -# We can now replace the default relative permeability model with our new model. +# This is not a problem, as the gradient of our simple neural network is fully compatible with ForwardDiff.jl, so no middleware is needed for this integration. +# We can now replace the default relative permeability model with our new neural network-based model. ml_model, ml_parameters, ml_forces, ml_sys, ml_dt = setup_simulation_case() @@ -285,7 +289,8 @@ ml_state0 = setup_reservoir_state(ml_model, Saturations = [1.0, 0.0] ) -ml_wd, ml_states, ml_t = simulate_reservoir(ml_state0, ml_model, ml_dt, parameters = ml_parameters, forces = ml_forces) +simulate_reservoir(ml_state0, ml_model, ml_dt, parameters = ml_parameters, forces = ml_forces, info_level = -1) +ml_wd, ml_states, ml_t = simulate_reservoir(ml_state0, ml_model, ml_dt, parameters = ml_parameters, forces = ml_forces, info_level = 1) # ### Compare results @@ -330,4 +335,59 @@ plot_reservoir(ml_model, ml_states, key = :Saturations, step = 3) # This example demonstrates how to integrate a neural network model for relative # permeability into a reservoir simulation using JutulDarcy.jl. While we used a # simple Brooks-Corey model for demonstration, this approach can be extended to -# more complex scenarios where analytical models may not be sufficient. \ No newline at end of file +# more complex scenarios where analytical models may not be sufficient. + + +# ## Bonus: Improving performance with SimpleChains.jl +# +# When inspecting the simulation results, we observe that using the ML model is slower than the analytical model. +# This is not surprising, since we are comparing a 593 parameters neural network on a CPU with a simple analytical function. + +# Many popular machine learning libraries prioritize optimization for large neural networks and GPU processing, +# often at the expense of performance for smaller models and CPU-based computations. +# For instance, these libraries might use memory allocations to achieve more efficient matrix multiplications, +# which is beneficial when matrix operations dominate the computation time. + +# However, in our scenario with a relatively small network running on a CPU, we can leverage a specialised library +# to improve performance. SimpleChains.jl is designed specifically for optimising small neural networks on CPUs. +# It offers significant performance improvements over traditional deep learning frameworks in such scenarios. + +# Advantages of SimpleChains.jl include: +# 1. Efficient utilisation of CPU resources, including SIMD vectorisation. +# 2. Minimal memory allocations during forward and backward passes. +# 3. Compile-time optimisations specifically for small, fixed-size networks. + +# By using SimpleChains.jl, we can potentially reduce the training time and achieve performance closer to that of the analytical model. + +# Fortunately, Lux.jl makes it straightforward to use SimpleChains as a backend. We simply need to convert +# our model to a SimpleChains model using the `ToSimpleChainsAdaptor`. This allows us to utilise the +# SimpleChains backend while still using the Lux training API. + +# (Note: Lux also supports Flux.jl models through a similar adaptor approach.) + +using SimpleChains +adaptor = ToSimpleChainsAdaptor(static(1)); + +BrooksCoreyMLModel_sc = adaptor(BrooksCoreyMLModel); + +# We can now train the model using the SimpleChains backend, with the Lux training API. + +tstate_sc, losses_sc = train_model(BrooksCoreyMLModel_sc, training_sat, rel_perm_analytical, epochs, lr); + +# The training time should be significantly reduced, since SimpleChains is optimised for small networks. +# With the trained model, we can now replace the relative permeability model in the simulation case, and run the simulation. + +ml_sc_model, ml_sc_parameters, ml_sc_forces, ml_sc_sys, ml_sc_dt = setup_simulation_case() + +ml_sc_kr = MLModelRelativePermeabilities(BrooksCoreyMLModel_sc, tstate_sc.parameters, tstate_sc.states) +replace_variables!(ml_sc_model, RelativePermeabilities = ml_sc_kr); + +ml_sc_state0 = setup_reservoir_state(ml_sc_model, + Pressure = 120bar, + Saturations = [1.0, 0.0] +) + +simulate_reservoir(ml_sc_state0, ml_sc_model, ml_sc_dt, parameters = ml_sc_parameters, forces = ml_sc_forces, info_level = -1); +ml_sc_wd, ml_sc_states, ml_sc_t = simulate_reservoir(ml_sc_state0, ml_sc_model, ml_sc_dt, parameters = ml_sc_parameters, forces = ml_sc_forces, info_level = 1); + +# From the simulation results, we should observe a performance improvement when using the SimpleChains model. From b07f0ce91b6fe05cab03fc97be4049d47813fbce Mon Sep 17 00:00:00 2001 From: jakobtorben Date: Mon, 28 Oct 2024 10:26:02 +0100 Subject: [PATCH 8/9] Replace Plots with GLMakie --- docs/Project.toml | 1 - examples/hybrid_simulation_relperm.jl | 47 ++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 7b6be866..62d4b476 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -19,7 +19,6 @@ MultiComponentFlash = "35e5bd01-9722-4017-9deb-64a5d32478ff" NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a" Optim = "429524aa-4258-5aef-a3af-852621145aeb" Optimisers = "3bd65402-5787-11e9-1adc-39752487f4e2" -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SimpleChains = "de6bee2f-e2f4-4ec7-b6ed-219cc6f6e9e5" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" diff --git a/examples/hybrid_simulation_relperm.jl b/examples/hybrid_simulation_relperm.jl index 8aabbad3..d5267cab 100644 --- a/examples/hybrid_simulation_relperm.jl +++ b/examples/hybrid_simulation_relperm.jl @@ -17,7 +17,7 @@ # We will use Lux for the neural network model, due to its explicit representation of the model and the ability to use different optimisers, ideal for integration with Jutul. # However, Flux.jl would work just as well for this simple example. -using JutulDarcy, Jutul, Lux, ADTypes, Zygote, Optimisers, Random, Plots, Statistics +using JutulDarcy, Jutul, Lux, ADTypes, Zygote, Optimisers, Random, Statistics, GLMakie # ## Set up the simulation case # We set up a reference simulation case following the [Your first JutulDarcy.jl simulation](https://sintefmath.github.io/JutulDarcy.jl/dev/man/first_ex) example: @@ -137,7 +137,17 @@ training_sat = collect(range(Float64(0), stop=Float64(1), length=train_samples)) training_sat = reshape(training_sat, 1, :) rel_perm_analytical = JutulDarcy.brooks_corey_relperm.(training_sat, n = exponent, residual = sr_g, residual_total = r_tot) -plot(vec(training_sat), vec(rel_perm_analytical), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") +fig = Figure() +ax = Axis(fig[1,1], + xlabel = "Saturation", + ylabel = "Relative Permeability", + title = "Saturation vs. Relative Permeability", + xticks = 0:0.25:1, + yticks = 0:0.25:1 +) +lines!(ax, vec(training_sat), vec(rel_perm_analytical), label="Brooks-Corey RelPerm") +axislegend(ax, position = :lt) +fig # ### Define the neural network architecture # Next we define the neural network architecture. The model takes in a saturation value and outputs a relative permeability value. @@ -216,10 +226,20 @@ tstate, losses = train_model(BrooksCoreyMLModel, training_sat, rel_perm_analytic # The loss function is plotted to show that the model is learning. -plot(losses, xlabel="Iteration", ylabel="Loss", label="per batch", yscale=:log10) -plot!(epochs:epochs:length(losses), mean.(Iterators.partition(losses, epochs)), -label="epoch mean", dpi=200) -title!("Training Loss") +fig = Figure() +ax = Axis(fig[1,1], + xlabel = "Iteration", + ylabel = "Loss", + title = "Training Loss", + yscale = log10 +) +lines!(ax, losses, label="per batch") +lines!(ax, epochs:epochs:length(losses), + mean.(Iterators.partition(losses, epochs)), + label="epoch mean" +) +axislegend() +fig # To test the trained model , we generate some test data, different to the training set @@ -231,8 +251,18 @@ testing_sat = reshape(testing_sat, 1, :) test_y = JutulDarcy.brooks_corey_relperm.(testing_sat, n = exponent, residual = sr_g, residual_total = r_tot) pred_y = Lux.apply(BrooksCoreyMLModel, testing_sat, tstate.parameters, tstate.states)[1] -plot(vec(testing_sat), vec(test_y), label="Brooks-Corey RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") -plot!(vec(testing_sat), vec(pred_y), label="ML model RelPerm", xlabel="Saturation", ylabel="Relative Permeability", title="Saturation vs. Relative Permeability") +fig = Figure() +ax = Axis(fig[1,1], + xlabel = "Saturation", + ylabel = "Relative Permeability", + title = "Saturation vs. Relative Permeability", + xticks = 0:0.25:1, + yticks = 0:0.25:1 +) +lines!(ax, vec(testing_sat), vec(test_y), label="Brooks-Corey RelPerm") +lines!(ax, vec(testing_sat), vec(pred_y), label="ML model RelPerm") +axislegend(ax, position = :lt) +fig # The plot demonstrates that our neural network has successfully learned to approximate the Brooks-Corey relative permeability curve. # This close match between the analytical solution and the ML model's predictions indicates that we can use this trained neural network in our simulation model. @@ -297,7 +327,6 @@ ml_wd, ml_states, ml_t = simulate_reservoir(ml_state0, ml_model, ml_dt, paramete # We can now compare the results of the reference simulation and the simulation with the neural network-based relative permeability model. -using GLMakie function plot_comparison(ref_wd, ml_wd, ref_t, ml_t) fig = Figure(size = (1200, 800)) From f189efe94586fb050f1cdb1cce588723b4e8be3b Mon Sep 17 00:00:00 2001 From: jakobtorben Date: Mon, 28 Oct 2024 10:35:57 +0100 Subject: [PATCH 9/9] Fix comments --- examples/hybrid_simulation_relperm.jl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/hybrid_simulation_relperm.jl b/examples/hybrid_simulation_relperm.jl index d5267cab..75a8392d 100644 --- a/examples/hybrid_simulation_relperm.jl +++ b/examples/hybrid_simulation_relperm.jl @@ -165,8 +165,6 @@ fig # - Use of tanh activation in hidden layers for smooth first derivatives # - Sigmoid in the final layer to constrain output to [0, 1] range # - Float64 precision to match JutulDarcy's numerical precision -# - Glorot normal initialization for weights -# - Use GPU for faster training (if available) BrooksCoreyMLModel = Chain( Dense(1 => 16, tanh), @@ -176,13 +174,13 @@ BrooksCoreyMLModel = Chain( ) # Define training parameters -# We train the model using the Adam optimizer with a learning rate of 0.0005. For a total of 10 epochs. +# We train the model using the Adam optimizer with a learning rate of 0.0005. For a total of 20 000 epochs. # The `optim` object will store the optimiser momentum, etc. epochs = 20000; lr = 0.0005; -# Training loop, using the whole data set epochs number of times: +# Training loop, using the whole data set epochs number of times. # We use Adam for the optimiser, set the random seed and initialise the parameters. # Lux defaults to float32 precision, so we need to convert the parameters to float64. # Lux uses a stateless, explicit representation of the model. It consists of four parts: @@ -319,8 +317,8 @@ ml_state0 = setup_reservoir_state(ml_model, Saturations = [1.0, 0.0] ) -simulate_reservoir(ml_state0, ml_model, ml_dt, parameters = ml_parameters, forces = ml_forces, info_level = -1) -ml_wd, ml_states, ml_t = simulate_reservoir(ml_state0, ml_model, ml_dt, parameters = ml_parameters, forces = ml_forces, info_level = 1) +simulate_reservoir(ml_state0, ml_model, ml_dt, parameters = ml_parameters, forces = ml_forces, info_level = -1); +ml_wd, ml_states, ml_t = simulate_reservoir(ml_state0, ml_model, ml_dt, parameters = ml_parameters, forces = ml_forces, info_level = 1); # ### Compare results