diff --git a/Project.toml b/Project.toml index 1abf3b94..0b2c53ea 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Jutul" uuid = "2b460a1a-8a2b-45b2-b125-b5c536396eb9" authors = ["Olav Møyner "] -version = "0.2.35" +version = "0.2.36" [deps] AlgebraicMultigrid = "2169fc97-5a83-5252-b627-83903c6c433c" diff --git a/ext/JutulMakieExt/interactive_3d.jl b/ext/JutulMakieExt/interactive_3d.jl index 3afae652..3d705597 100644 --- a/ext/JutulMakieExt/interactive_3d.jl +++ b/ext/JutulMakieExt/interactive_3d.jl @@ -707,6 +707,12 @@ function basic_3d_figure(resolution = default_jutul_resolution(); z_is_depth = f return (fig, ax) end +function basic_2d_figure(resolution = default_jutul_resolution(); z_is_depth = false) + fig = Figure(size = resolution) + ax = Axis(fig[1, 1]) + return (fig, ax) +end + function symlog10(x) # Inspired by matplotlib.scale.SymmetricalLogScale diff --git a/ext/JutulMakieExt/mesh_plots.jl b/ext/JutulMakieExt/mesh_plots.jl index 7f098c9a..75b7d7b4 100644 --- a/ext/JutulMakieExt/mesh_plots.jl +++ b/ext/JutulMakieExt/mesh_plots.jl @@ -3,7 +3,12 @@ function Jutul.plot_mesh_impl(m; z_is_depth = Jutul.mesh_z_is_depth(m), kwarg... ) - fig, ax = basic_3d_figure(resolution, z_is_depth = z_is_depth) + if dim(m) == 3 + makefig = basic_3d_figure + else + makefig = basic_2d_figure + end + fig, ax = makefig(resolution, z_is_depth = z_is_depth) p = Jutul.plot_mesh!(ax, m; kwarg...) display(fig) return (fig, ax, p) @@ -11,22 +16,52 @@ end function Jutul.plot_mesh_impl!(ax, m; cells = nothing, + faces = nothing, + boundaryfaces = nothing, outer = false, color = :lightblue, kwarg... ) pts, tri, mapper = triangulate_mesh(m, outer = outer) - if !isnothing(cells) + has_cell_filter = !isnothing(cells) + has_face_filter = !isnothing(faces) + has_bface_filter = !isnothing(boundaryfaces) + if has_cell_filter || has_face_filter || has_bface_filter if eltype(cells) == Bool @assert length(cells) == number_of_cells(m) cells = findall(cells) end + if eltype(faces) == Bool + @assert length(faces) == number_of_faces(m) + faces = findall(faces) + end + if eltype(boundaryfaces) == Bool + @assert length(boundaryfaces) == number_of_boundary_faces(m) + boundaryfaces = findall(boundaryfaces) + end + if has_bface_filter + nf = number_of_faces(m) + boundaryfaces = deepcopy(boundaryfaces) + boundaryfaces .+= nf + end ntri = size(tri, 1) - keep = [false for i in 1:ntri] + keep = fill(false, ntri) cell_ix = mapper.indices.Cells + face_ix = mapper.indices.Faces for i in 1:ntri # All tris have same cell so this is ok - keep[i] = cell_ix[tri[i, 1]] in cells + tri_tmp = tri[i, 1] + keep_this = true + if has_cell_filter + keep_this = keep_this && cell_ix[tri_tmp] in cells + end + if has_face_filter + keep_this = keep_this && face_ix[tri_tmp] in faces + end + if has_bface_filter + keep_this = keep_this && face_ix[tri_tmp] in boundaryfaces + end + keep[i] = keep_this end tri = tri[keep, :] tri, pts = remove_unused_points(tri, pts) @@ -54,7 +89,13 @@ function Jutul.plot_cell_data_impl(m, data; z_is_depth = Jutul.mesh_z_is_depth(m), kwarg... ) - fig, ax = basic_3d_figure(resolution, z_is_depth = z_is_depth) + if dim(m) == 3 + makefig = basic_3d_figure + else + makefig = basic_2d_figure + end + fig, ax = makefig(resolution, z_is_depth = z_is_depth) + p = Jutul.plot_cell_data!(ax, m, data; kwarg...) min_data = minimum(data) max_data = maximum(data) @@ -114,7 +155,7 @@ function Jutul.plot_mesh_edges_impl!(ax, m; transparency = true, color = :black, cells = nothing, - outer = true, + outer = dim(m) == 3, linewidth = 0.3, kwarg...) m = physical_representation(m) diff --git a/src/Jutul.jl b/src/Jutul.jl index 5c773b8e..70e167c7 100644 --- a/src/Jutul.jl +++ b/src/Jutul.jl @@ -65,6 +65,7 @@ module Jutul # Meat and potatoes include("variable_evaluation.jl") include("conservation/flux.jl") + include("conservation/fvm_assembly.jl") include("linsolve/linsolve.jl") include("context.jl") @@ -100,4 +101,7 @@ module Jutul # Support for SI unit conversion include("units/units.jl") + # Nonlinear finite-volume discretizations + include("NFVM/NFVM.jl") + end # module diff --git a/src/NFVM/NFVM.jl b/src/NFVM/NFVM.jl new file mode 100644 index 00000000..b5cb0f49 --- /dev/null +++ b/src/NFVM/NFVM.jl @@ -0,0 +1,11 @@ +module NFVM + using Jutul + using LinearAlgebra + using StaticArrays + include("types.jl") + include("triplets.jl") + include("hap.jl") + include("decomposition.jl") + include("evaluation.jl") + +end # module NFVM diff --git a/src/NFVM/decomposition.jl b/src/NFVM/decomposition.jl new file mode 100644 index 00000000..b077ae1d --- /dev/null +++ b/src/NFVM/decomposition.jl @@ -0,0 +1,220 @@ +function get_half_face_normal(G, cell, face, normals, areas) + if G.faces.neighbors[face][2] == cell + sgn = -1 + else + sgn = 1 + end + return sgn*normals[face]*areas[face] +end + +function ntpfa_decompose_half_face(G::UnstructuredMesh{D}, cell, face, K, cell_centroids, face_centroids, normals, areas, bnd_face_centroids, bnd_normals, bnd_areas) where D + # Vector we are going to decompose + normal = get_half_face_normal(G, cell, face, normals, areas) + AKn = K[cell]*normal + # Get local set of HAPs + weights + cells = Int[] + weights = Tuple{Float64, Float64}[] + points = SVector{D, Float64}[] + K_self = K[cell] + x_self = cell_centroids[cell] + for f in G.faces.cells_to_faces[cell] + l, r = G.faces.neighbors[f] + # Don't use left and right, use cell and other. + if l == cell + other = r + sgn = 1.0 + else + other = l + sgn = -1.0 + end + x_f = face_centroids[f] + n_f = sgn*normals[f] + K_other = K[other] + x_other = cell_centroids[other] + hp, w = find_harmonic_average_point(K_self, x_self, K_other, x_other, x_f, n_f) + # @info "Harmonic point found" hp w + push!(cells, other) + push!(points, hp) + push!(weights, w) + end + for bf in G.boundary_faces.cells_to_faces[cell] + # TODO: Something a bit smarter here. + push!(cells, cell) + push!(points, bnd_face_centroids[bf]) + push!(weights, (0.5, 0.5)) + end + # Next, figure out which ones we are going to keep. + x_t = cell_centroids[cell] + + trip, trip_w = find_minimizing_basis(x_t, AKn, points, throw = false) + if any(isequal(0), trip) + out = nothing + else + l_r = NFVM.reconstruct_l(trip, trip_w, x_t, points) + # @assert norm(l_r - AKn)/norm(AKn) < 1e-8 "Mismatch in reconstruction, $l_r != $AKn" + + active_weights = map(x -> weights[x], trip) + out = ( + self = cell, + self_weights = map(first, active_weights), # Weights for self + other_cells_weights = map(last, active_weights), # Weights for other cells + other_cells = map(x -> cells[x], trip), # Other cell for each HAP + harmonic_average_points = map(x -> points[x], trip), + triplet_weights = trip_w, + Kn = AKn + ) + end + return out +end + +function remainder_trans(decomp, l, r, sgn = 1) + out = Tuple{Int, Float64}[] + for (i, c) in enumerate(decomp.other_cells) + if c != l && c != r + tw_i = decomp.triplet_weights[i] + cw_i = decomp.other_cells_weights[i] + w_i = sgn*tw_i*cw_i + push!(out, (c, w_i)) + end + end + return out +end + +function two_point_trans(decomp, cell) + # @warn "Decomposing $cell" + for (k, v) in pairs(decomp) + # @info "$k" v + end + T = 0.0 + if decomp.self == cell + T += sum(decomp.self_weights.*decomp.triplet_weights) + end + for (i, c) in enumerate(decomp.other_cells) + if c == cell + tw_i = decomp.triplet_weights[i] + cw_i = decomp.other_cells_weights[i] + # @info "Found self in other $cell" c i tw_i cw_i + T += tw_i*cw_i + end + end + # @info "Final T = $T" + return T +end + +function NFVMLinearDiscretization(t_tpfa::Real; left, right) + # Fallback constructor - essentially just a two-point flux with extra steps. + t_r = t_tpfa + t_l = -t_tpfa + t_mpfa = Tuple{Int, Float64}[] + return NFVMLinearDiscretization(left, right, t_l, t_r, t_mpfa) +end + +function NFVMLinearDiscretization(decomp; left, right) + t_l = two_point_trans(decomp, left) + t_r = two_point_trans(decomp, right) + + w_tot = -sum(decomp.triplet_weights) + if decomp.self == left + sgn = 1 + t_l += w_tot + else + sgn = -1 + t_r += w_tot + end + t_mpfa = remainder_trans(decomp, left, right, sgn) + t_l *= sgn + t_r *= sgn + + return NFVMLinearDiscretization(left, right, t_l, t_r, t_mpfa) +end + +function Jutul.subdiscretization(d::NFVMLinearDiscretization, subg, mapper::Jutul.FiniteVolumeGlobalMap, face) + (; left, right, T_left, T_right) = d + t_mpfa = d.mpfa + gmap = mapper.global_to_local + + t_mpfa_new = Tuple{Int, Float64}[] + for tm in t_mpfa + # TODO: This is a bit dangerous - may have missing MPFA connections + c, trans = tm + try + new_c = Jutul.local_cell(c, mapper) + push!(t_mpfa_new, (new_c, trans)) + catch + continue + end + end + l_new = Jutul.local_cell(left, mapper) + r_new = Jutul.local_cell(right, mapper) + return NFVMLinearDiscretization( + l_new, + r_new, + T_left, + T_right, + t_mpfa_new + ) +end + +function ntpfa_decompose_faces(G::UnstructuredMesh{D}, perm, scheme::Symbol = :avgmpfa; + faces = 1:number_of_faces(G), + tpfa_trans = missing, + extra_out = false + ) where D + geo = tpfv_geometry(G) + areas = geo.areas + Vec_t = SVector{D, Float64} + possible_schemes = (:mpfa, :avgmpfa, :ntpfa, :nmpfa, :test_tpfa) + scheme in possible_schemes || throw(ArgumentError("Scheme must be one of $possible_schemes, was :$scheme")) + + normals = reinterpret(Vec_t, geo.normals) + cell_centroids = reinterpret(Vec_t, geo.cell_centroids) + face_centroids = reinterpret(Vec_t, geo.face_centroids) + + bnd_normals = reinterpret(Vec_t, geo.boundary_normals) + bnd_areas = geo.boundary_areas + bnd_face_centroids = reinterpret(Vec_t, geo.boundary_centroids) + + if perm isa AbstractMatrix + K = SMatrix{D, D, Float64, D*D}[] + for i in axes(perm, 2) + push!(K, Jutul.expand_perm(perm[:, i], Val(D))) + end + else + perm::AbstractVector + K = perm + end + + nf = number_of_faces(G) + function ntpfa_trans_for_face(f) + @assert f <= nf && f > 0 "Face $f not in range 1:$nf" + l, r = G.faces.neighbors[f] + left_decompose = ntpfa_decompose_half_face(G, l, f, K, cell_centroids, face_centroids, normals, areas, bnd_face_centroids, bnd_normals, bnd_areas) + right_decompose = ntpfa_decompose_half_face(G, r, f, K, cell_centroids, face_centroids, normals, areas, bnd_face_centroids, bnd_normals, bnd_areas) + if isnothing(left_decompose) || isnothing(right_decompose) || scheme == :test_tpfa + # A branch for handling fallback to TPFA scheme if something has + # gone wrong in the decomposition. + if ismissing(tpfa_trans) + error("Unable to use fallback transmissibility if tpfa_trans keyword argument is defaulted.") + end + T_fallback = tpfa_trans[f] + l_trans = NFVMLinearDiscretization(T_fallback, left = l, right = r) + r_trans = NFVMLinearDiscretization(T_fallback, left = l, right = r) + else + l_trans = NFVMLinearDiscretization(left_decompose, left = l, right = r) + r_trans = NFVMLinearDiscretization(right_decompose, left = l, right = r) + end + if scheme == :avgmpfa || scheme == :mpfa || scheme == :test_tpfa + disc = merge_to_avgmpfa(l_trans, r_trans) + else + disc = NFVMNonLinearDiscretization(l_trans, r_trans, scheme) + end + if extra_out + out = (disc, left_decompose, right_decompose) + else + out = disc + end + return out + end + disc = map(ntpfa_trans_for_face, faces) + return disc +end diff --git a/src/NFVM/evaluation.jl b/src/NFVM/evaluation.jl new file mode 100644 index 00000000..357067df --- /dev/null +++ b/src/NFVM/evaluation.jl @@ -0,0 +1,88 @@ +function evaluate_flux(p, hf::NFVMLinearDiscretization{T}, ph::Int) where {T} + p_l, p_r = cell_pair_pressures(p, hf, ph) + T_l = hf.T_left + T_r = hf.T_right + q = tpfa_flux(p_l, p_r, hf) + compute_r(p, hf, ph) + return q +end + +function evaluate_flux(p, nfvm, ph::Int) + L = nfvm.ft_left + R = nfvm.ft_right + + p_l, p_r = cell_pair_pressures(p, nfvm, ph) + # Fluxes from different side with different sign + q_l, r_l = ntpfa_half_flux(p_l, p_r, p, L, ph) + q_r, r_r = ntpfa_half_flux(p_l, p_r, p, R, ph, -1) + + if nfvm.scheme == :ntpfa + r_lw = r_l + r_rw = r_r + else + # TODO: Double check this. + r_lw = abs(r_l) + r_rw = abs(r_r) + end + r_total = r_lw + r_rw + if abs(r_total) < 1e-10 + μ_l = μ_r = 0.5 + else + μ_l = r_rw/r_total + μ_r = r_lw/r_total + end + + q = μ_l*q_l - μ_r*q_r + return q +end + +@inline function ntpfa_half_flux(p_l, p_r, p, disc, ph) + q = tpfa_flux(p_l, p_r, disc) + # Add MPFA part + r = compute_r(p, disc, ph) + q += r + return (q, r) +end + +@inline function ntpfa_half_flux(p_l, p_r, p, disc, ph, sgn) + q, r = ntpfa_half_flux(p_l, p_r, p, disc, ph) + return (sgn*q, sgn*r) +end + + +@inline function compute_r(p::AbstractVector{T}, hf::NFVMLinearDiscretization, ph::Int) where T + q = zero(T) + for i in 1:length(hf.mpfa) + @inbounds c, T_c = hf.mpfa[i] + @inbounds q += p[c]*T_c + end + return q +end + +@inline function compute_r(p::AbstractMatrix{T}, hf::NFVMLinearDiscretization, ph::Int) where T + q = zero(T) + for i in 1:length(hf.mpfa) + @inbounds c, T_c = hf.mpfa[i] + @inbounds q += p[ph, c]*T_c + end + return q +end + +@inline function cell_pair_pressures(p::AbstractVector, hf, ph::Int) + l, r = Jutul.cell_pair(hf) + @inbounds p_l = p[l] + @inbounds p_r = p[r] + return (p_l, p_r) +end + +Base.@propagate_inbounds @inline function cell_pair_pressures(p::AbstractMatrix, hf, ph::Int) + l, r = Jutul.cell_pair(hf) + @inbounds p_l = p[ph, l] + @inbounds p_r = p[ph, r] + return (p_l, p_r) +end + +@inline function tpfa_flux(p_l, p_r, hf::NFVMLinearDiscretization) + T_l = hf.T_left + T_r = hf.T_right + return T_l*p_l + T_r*p_r +end diff --git a/src/NFVM/hap.jl b/src/NFVM/hap.jl new file mode 100644 index 00000000..470edc74 --- /dev/null +++ b/src/NFVM/hap.jl @@ -0,0 +1,29 @@ +function find_harmonic_average_point(K_1, x_1, K_2, x_2, x_f, n_f) + @assert length(x_1) == length(x_2) == length(n_f) == length(x_f) + + λ_1, γ_1 = compute_coefficients(K_1, n_f) + y_1, d_1 = project_point_to_plane(x_1, x_f, n_f) + + λ_2, γ_2 = compute_coefficients(K_2, n_f) + y_2, d_2 = project_point_to_plane(x_2, x_f, n_f) + + w_1 = λ_1*d_2 + w_2 = λ_2*d_1 + w_t = w_1 + w_2 + avg_pt = (w_1*y_1 + w_2*y_2 + d_1*d_2*(γ_1 - γ_2))/w_t + return (avg_pt, (w_1/w_t, w_2/w_t)) +end + +function compute_coefficients(K, n) + λ = n'*K*n + γ = K*n - λ*n + return (λ, γ) +end + +function project_point_to_plane(pt, pt_on_plane, plane_normal) + # point_on_plane = pt - cross(dot(normal, pt - pt_plane), normal) + # dist_to_plane = norm(pt - point_on_plane, 2) + dist_to_plane = dot(pt_on_plane - pt, plane_normal) + pt_projected_onto_plane = pt + dist_to_plane * plane_normal + return (pt_projected_onto_plane, abs(dist_to_plane)) +end diff --git a/src/NFVM/triplets.jl b/src/NFVM/triplets.jl new file mode 100644 index 00000000..2b716cc7 --- /dev/null +++ b/src/NFVM/triplets.jl @@ -0,0 +1,189 @@ +function duo_coefficients(t_i, t_j, l) + M = @SMatrix [ + t_i[1] t_j[1]; + t_i[2] t_j[2] + ] + if abs(det(M)) < 1e-8 + α = β = Inf + else + α, β = M\l + end + return (α, β) +end + +function triplet_coefficients(t_i, t_j, t_k, l) + M = @SMatrix [ + t_i[1] t_j[1] t_k[1]; + t_i[2] t_j[2] t_k[2]; + t_i[3] t_j[3] t_k[3] + ] + if abs(det(M)) < 1e-8 + α = β = γ = Inf + else + α, β, γ = M\l + end + return (α, β, γ) +end + +function find_minimizing_basis_inner(l::SVector{3, Num_t}, all_t, print = false, stop_early = true) where Num_t + N = length(all_t) + get_t(i) = all_t[i] + # Intermediate variables + best_triplet = (0, 0, 0) + best_triplet_W = (Inf, Inf, Inf) + best_value = Inf + + ϵ = 0.0 + for i in 1:(N-2) + t_i = get_t(i) + for j in (i+1):(N-1) + t_j = get_t(j) + for k in (j+1):N + t_k = get_t(k) + α, β, γ = triplet_coefficients(t_i, t_j, t_k, l) + # @info "$i $j $k" α β γ + # @info "Vectors" t_i t_j t_k + if α ≥ ϵ && β ≥ ϵ && γ ≥ ϵ + ijk_value = max(α, β, γ) + ijk = (i, j, k) + W = (α, β, γ) + if ijk_value ≤ 1 && stop_early + if print + @info "Found optimal triplet." (α, β, γ) ijk t_i t_j t_k l + end + return (ijk, W) + elseif ijk_value < best_value + if print + @info "Value $ijk_value < $best_value, setting current best value to $ijk" + end + best_value = ijk_value + best_triplet = ijk + best_triplet_W = W + end + end + end + end + end + if print + @info "Picking best found triplet." best_triplet_W best_triplet + end + return (best_triplet, best_triplet_W) +end + +function find_minimizing_basis_inner(l::SVector{2, Num_t}, all_t, print = false, stop_early = true) where Num_t + N = length(all_t) + get_t(i) = all_t[i] + # Intermediate variables + best_triplet = (0, 0, 0) + best_triplet_W = (Inf, Inf, Inf) + best_value = Inf + + ϵ = 0.0 + for i in 1:(N-1) + t_i = get_t(i) + for j in (i+1):N + t_j = get_t(j) + α, β = duo_coefficients(t_i, t_j, l) + if α ≥ ϵ && β ≥ ϵ + ijk_value = max(α, β) + ijk = (i, j) + W = (α, β) + if ijk_value ≤ 1 && stop_early + if print + @info "Found optimal triplet." (α, β) ijk t_i t_j l + end + return (ijk, W) + elseif ijk_value < best_value + best_value = ijk_value + best_triplet = ijk + best_triplet_W = W + if print + @info "Value $ijk_value < $best_value, setting current best value to $ijk" + end + end + end + end + end + if print + @info "Picking best found triplet." best_triplet_W best_triplet + end + return (best_triplet, best_triplet_W) +end + +function candidate_vectors(x_t, x, i; normalize = true) + t = x[i] - x_t + if normalize + t = t./norm(t, 2) + end + return t +end + +function candidate_vectors(x_t, x; normalize = true) + t = similar(x) + for i in eachindex(x) + t[i] = candidate_vectors(x_t, x, i) + end + return t +end + +function find_minimizing_basis(x_t::T, l::T, all_x::AbstractVector{T}; check = false, verbose = false, stop_early = true, throw = true) where T + all_x = copy(all_x) + all_t = candidate_vectors(x_t, all_x, normalize = true) + l_norm = norm(l, 2) + l_bar = l/l_norm + # https://en.wikipedia.org/wiki/Cosine_similarity + # function F_sort(i) + # return abs(dot(x_t + l_bar, x_t + all_t[i])) + # end + p1 = x_t + l_bar + function F_sort(i) + v1 = all_t[i] + v2 = l_bar + dotnormed = dot(v1, v2)/(norm(v1)*norm(v2)) + # Guard against noise + dotnormed = clamp(dotnormed, 0.0, 1.0) + out = acos(dotnormed) + return out + end + sorted_indices = sort(eachindex(all_t), by = F_sort) + + ijk, w = find_minimizing_basis_inner(l_bar, all_t[sorted_indices], verbose, stop_early) + if any(isequal(0), ijk) + if throw + handle_failure(x_t, l, all_x) + end + else + # OK! + ijk = map(x -> sorted_indices[x], ijk) + + function normalized_weight(i) + t = candidate_vectors(x_t, all_x, ijk[i], normalize = false) + return l_norm*w[i]/norm(t, 2) + end + w = map(normalized_weight, eachindex(w)) + end + return (ijk = ijk, w = w) +end + +function handle_failure(x_t, l, all_x) + println("Decomposition failed") + println("x_t=") + Base.show(x_t) + println("\nl=") + Base.show(l) + println("\nall_x=") + Base.show(all_x) + println("") + error("Aborting, unable to continue.") +end + +function reconstruct_l(indices, weights, x_t, all_x) + # Reconstruct decomposed l for testing. + l_r = zero(typeof(x_t)) + for (w, i) in zip(weights, indices) + t = candidate_vectors(x_t, all_x, i, normalize = false) + next = w*t + l_r = l_r .+ next + end + return l_r +end diff --git a/src/NFVM/types.jl b/src/NFVM/types.jl new file mode 100644 index 00000000..e4e81abe --- /dev/null +++ b/src/NFVM/types.jl @@ -0,0 +1,75 @@ +abstract type NFVMDiscretization <: KGradDiscretization + +end + +struct NFVMLinearDiscretization{T} <: NFVMDiscretization + left::Int + right::Int + T_left::T + T_right::T + mpfa::Vector{Tuple{Int, T}} +end + +function Base.show(io::IO, ft::NFVMLinearDiscretization{T}) where T + l, r = cell_pair(ft) + print(io, "NFVMLinearDiscretization{$T} $(l)→$(r)") + compact = get(io, :compact, false) + if !compact + avg = (abs(ft.T_left) + abs(ft.T_right))/2.0 + print(io, ", T≈$avg ($(length(ft.mpfa)) MPFA points)") + end +end + + +struct NFVMNonLinearDiscretization{T} <: NFVMDiscretization + ft_left::NFVMLinearDiscretization{T} + ft_right::NFVMLinearDiscretization{T} + scheme::Symbol +end + +function NFVMNonLinearDiscretization(l, r; scheme = :ntpfa) + @assert scheme in (:ntpfa, :nmpfa) + return NFVMNonLinearDiscretization(l, r, scheme) +end + +Jutul.cell_pair(x::NFVMNonLinearDiscretization) = Jutul.cell_pair(x.ft_left) +Jutul.cell_pair(x::NFVMLinearDiscretization) = (x.left, x.right) + +function Base.show(io::IO, ft::NFVMNonLinearDiscretization{T}) where T + l, r = cell_pair(ft) + print(io, "NFVMNonLinearDiscretization{$T} $(l)→$(r)") + compact = get(io, :compact, false) + if !compact + L = ft.ft_left + R = ft.ft_right + avg = (abs(L.T_left) + abs(L.T_right) + abs(R.T_left) + abs(R.T_right))/4.0 + print(io, ", T≈$avg ($(length(L.mpfa)) + $(length(R.mpfa)) MPFA points)") + end +end + +function merge_to_avgmpfa(a::NFVMLinearDiscretization{T}, b::NFVMLinearDiscretization{T}) where {T} + function merge_in!(next, x) + for el in x.mpfa + c, v = el + v /= 2.0 + if haskey(next, c) + next[c] += v + else + next[c] = v + end + end + return next + end + l, r = Jutul.cell_pair(a) + @assert (l, r) == Jutul.cell_pair(b) + next = Dict{Int, T}() + merge_in!(next, a) + merge_in!(next, b) + mpfa = Vector{Tuple{Int, T}}() + for (c, v) in pairs(next) + push!(mpfa, (c, v)) + end + T_l = (a.T_left + b.T_left)/2.0 + T_r = (a.T_right + b.T_right)/2.0 + return NFVMLinearDiscretization(l, r, T_l, T_r, mpfa) +end \ No newline at end of file diff --git a/src/ad/ad.jl b/src/ad/ad.jl index cf056698..2151bcfc 100644 --- a/src/ad/ad.jl +++ b/src/ad/ad.jl @@ -76,10 +76,15 @@ end end insert_residual_value(::Nothing, ix, v) = nothing -Base.@propagate_inbounds insert_residual_value(r, ix, v) = r[ix] = v +Base.@propagate_inbounds function insert_residual_value(r, ix, v) + r[ix] = v +end insert_residual_value(::Nothing, ix, e, v) = nothing -Base.@propagate_inbounds insert_residual_value(r, ix, e, v) = r[e, ix] = v +Base.@propagate_inbounds function insert_residual_value(r, ix, e, v) + # TODO: Occasionally this gets passed the transposed r, figure out where. + r[e, ix] = v +end function fill_equation_entries!(nz, r, model, cache::JutulAutoDiffCache) nu, ne, np = ad_dims(cache) diff --git a/src/ad/gradients.jl b/src/ad/gradients.jl index 7c4d8a62..1fc7d009 100644 --- a/src/ad/gradients.jl +++ b/src/ad/gradients.jl @@ -532,7 +532,13 @@ end function parameter_targets(model::SimulationModel) prm = get_parameters(model) - return collect(keys(prm)) + targets = Symbol[] + for (k, v) in prm + if parameter_is_differentiable(v, model) + push!(targets, k) + end + end + return targets end function adjoint_parameter_model(model, arg...; context = DefaultContext()) @@ -541,6 +547,7 @@ function adjoint_parameter_model(model, arg...; context = DefaultContext()) pmodel = adjoint_model_copy(model; context = context) # Swap parameters and primary variables swap_primary_with_parameters!(pmodel, model, arg...) + ensure_model_consistency!(pmodel) return sort_variables!(pmodel, :all) end @@ -638,12 +645,31 @@ function perturb_parameter!(model, param_i, target, i, j, sz, ϵ) end function evaluate_objective(G, model, states, timesteps, all_forces; large_value = 1e20) + function convert_state_to_jutul_storage(model, x::JutulStorage) + return x + end + function convert_state_to_jutul_storage(model, x::AbstractDict) + return JutulStorage(x) + end + function convert_state_to_jutul_storage(model::MultiModel, x::AbstractDict) + s = JutulStorage() + for (k, v) in pairs(x) + s[k] = JutulStorage(v) + end + return s + end if length(states) < length(timesteps) # Failure: Put to a big value. @warn "Partial data passed, objective set to large value $large_value." obj = large_value else - F = i -> G(model, states[i], timesteps[i], i, forces_for_timestep(nothing, all_forces, timesteps, i)) + F = i -> G( + model, + convert_state_to_jutul_storage(model, states[i]), + timesteps[i], + i, + forces_for_timestep(nothing, all_forces, timesteps, i) + ) obj = sum(F, eachindex(states)) end return obj diff --git a/src/composite/composite.jl b/src/composite/composite.jl index 1e92aa05..33bed08f 100644 --- a/src/composite/composite.jl +++ b/src/composite/composite.jl @@ -1,3 +1,4 @@ include("system.jl") include("utils.jl") -include("variables.jl") \ No newline at end of file +include("variables.jl") +include("conservation.jl") diff --git a/src/composite/conservation.jl b/src/composite/conservation.jl new file mode 100644 index 00000000..3a294968 --- /dev/null +++ b/src/composite/conservation.jl @@ -0,0 +1,38 @@ +# Overloads for TPFA storage type + +function declare_pattern(model::CompositeModel, eq::Pair{Symbol, <:ConservationLaw}, e_s::ConservationLawTPFAStorage, entity) + return declare_pattern(model, last(eq), e_s, entity) +end + +function update_equation!(eq_s::ConservationLawTPFAStorage, law::Pair{Symbol, <:ConservationLaw}, storage, model::CompositeModel, dt) + k, eq = law + m = composite_submodel(model, k) + return update_equation!(eq_s, eq, storage, m, dt) +end + +function setup_equation_storage( + model::CompositeModel, + eq::Pair{Symbol, ConservationLaw{S, T, H, G}}, + storage; + extra_sparsity = nothing, + kwarg...) where {S, T<:TwoPointPotentialFlowHardCoded, H, G} + return ConservationLawTPFAStorage(model, eq[2]; kwarg...) +end + +function setup_equation_storage( + model::CompositeModel, + eq::Pair{Symbol, ConservationLaw{S, PotentialFlow{:fvm, A, B, C}, H, G}}, + storage; + kwarg...) where {S, A, B, C, H, G} + k, eq = eq + return ConservationLawFiniteVolumeStorage(composite_submodel(model, k), eq, storage; kwarg...) +end + +function declare_pattern( + model::CompositeModel, + eq::Pair{Symbol, ConservationLaw{S, PotentialFlow{:fvm, A, B, C}, H, G}}, + e_s::ConservationLawFiniteVolumeStorage, + entity) where {S, A, B, C, H, G} + k, eq = eq + return declare_pattern(composite_submodel(model, k), eq, e_s, entity) +end diff --git a/src/composite/system.jl b/src/composite/system.jl index 596411a0..e14a3acf 100644 --- a/src/composite/system.jl +++ b/src/composite/system.jl @@ -83,6 +83,28 @@ function composite_generate_submodel(m::CompositeModel, label::Symbol) return model end +function composite_sync_variables!(m::CompositeModel, vartype = :all) + if vartype == :all + for k in (:parameters, :secondary, :primary) + composite_sync_variables!(m, k) + end + else + for k in keys(m.system.systems) + subm = composite_submodel(m, k) + vars0 = get_variables_by_type(subm, vartype) + empty!(vars0) + end + vars = get_variables_by_type(m, vartype) + for (k, var) in pairs(vars) + submodel_key, var = var + subm = composite_submodel(m, submodel_key) + vars0 = get_variables_by_type(subm, vartype) + vars0[k] = var + end + end + return m +end + function setup_forces(model::CompositeModel; kwarg...) force = Dict{Symbol, Any}() for k in keys(model.system.systems) diff --git a/src/composite/utils.jl b/src/composite/utils.jl index feb81d52..c8d00d67 100644 --- a/src/composite/utils.jl +++ b/src/composite/utils.jl @@ -19,6 +19,11 @@ function update_model_post_selection!(model::CompositeModel) return model end +function ensure_model_consistency!(model::CompositeModel) + update_model_pre_selection!(model) + return model +end + function composite_submodel(model::CompositeModel, k::Symbol) return model.extra[:models][k] end @@ -55,18 +60,26 @@ function variable_scale(u::Pair{Symbol, V}) where V<:JutulVariables end function values_per_entity(model::CompositeModel, u::Pair{Symbol, V}) where V<:JutulVariables + # Needs syncing + composite_sync_variables!(model, :primary) values_per_entity(composite_submodel(model, u[1]), u[2]) end function degrees_of_freedom_per_entity(model::CompositeModel, u::Pair{Symbol, V}) where V<:JutulVariables + # Needs syncing + composite_sync_variables!(model, :primary) degrees_of_freedom_per_entity(composite_submodel(model, u[1]), u[2]) end function number_of_degrees_of_freedom(model::CompositeModel, u::Pair{Symbol, V}) where V<:JutulVariables + # Needs syncing + composite_sync_variables!(model, :primary) number_of_degrees_of_freedom(composite_submodel(model, u[1]), u[2]) end function number_of_parameters(model::CompositeModel, u::Pair{Symbol, V}) where V<:JutulVariables + # Needs syncing + composite_sync_variables!(model, :parameters) number_of_parameters(composite_submodel(model, u[1]), u[2]) end @@ -75,10 +88,6 @@ function initialize_primary_variable_ad!(stateAD, model, pvar::Pair{Symbol, V}, return initialize_primary_variable_ad!(stateAD, m, pvar[2], pkey, n_partials; kwarg...) end -function declare_sparsity(model::CompositeModel, eq::Pair{Symbol, V}, eq_s, u, row_layout, col_layout) where V<:JutulEquation - k, eq = eq - return declare_sparsity(composite_submodel(model, k), eq, eq_s, u, row_layout, col_layout) -end function number_of_equations(model::CompositeModel, eq::Pair{Symbol, V}) where V<:JutulEquation k, eq = eq @@ -113,11 +122,6 @@ function update_equation_in_entity!(eq_buf, c, state, state0, eqn::Pair{Symbol, return update_equation_in_entity!(eq_buf, c, state, state0, eq, composite_submodel(model, k), dt, ldisc) end -function setup_equation_storage(model::CompositeModel, eqn::Pair{Symbol, V}, storage; kwarg...) where V<:JutulEquation - k, eq = eqn - return setup_equation_storage(composite_submodel(model, k), eq, storage; kwarg...) -end - function update_linearized_system_equation!(nz, r, model::CompositeModel, eqn::Pair{Symbol, V}, storage) where V<:JutulEquation k, eq = eqn return update_linearized_system_equation!(nz, r, composite_submodel(model, k), eq, storage) @@ -127,3 +131,12 @@ function convergence_criterion(model::CompositeModel, storage, eqn::Pair{Symbol, k, eq = eqn return convergence_criterion(composite_submodel(model, k), storage, eq, eq_s, r; kwarg...) end + +minimum_value(x::Pair) = minimum_value(last(x)) +maximum_value(x::Pair) = maximum_value(last(x)) +variable_scale(x::Pair) = variable_scale(last(x)) + +function parameter_is_differentiable(prm::Pair, model) + k, prm = prm + parameter_is_differentiable(prm, composite_submodel(model, k)) +end diff --git a/src/conservation/flux.jl b/src/conservation/flux.jl index 59bda2a6..417da189 100644 --- a/src/conservation/flux.jl +++ b/src/conservation/flux.jl @@ -18,6 +18,16 @@ struct TPFA{T} <: KGradDiscretization face_sign::T end +function subdiscretization(tpfa::TPFA, subg, mapper::Jutul.FiniteVolumeGlobalMap, face) + (; left, right, face_sign) = tpfa + gmap = mapper.global_to_local + return TPFA(gmap[left], gmap[right], face_sign) +end + +function cell_pair(tpfa::TPFA) + return (tpfa.left, tpfa.right) +end + """ Single-point upwinding. """ @@ -26,27 +36,38 @@ struct SPU{T} <: UpwindDiscretization right::T end +function subdiscretization(spu::SPU, subg, mapper::Jutul.FiniteVolumeGlobalMap, face) + (; left, right) = spu + gmap = mapper.global_to_local + return SPU(gmap[left], gmap[right]) +end + +function cell_pair(spu::SPU) + return (spu.left, spu.right) +end + export PotentialFlow -struct PotentialFlow{K, U, HF} <: FlowDiscretization +struct PotentialFlow{AD, K, U, HF} <: FlowDiscretization kgrad::K upwind::U half_face_map::HF - function PotentialFlow(kgrad::K, upwind::U, hf::HF) where {K, U, HF} - return new{K, U, HF}(kgrad, upwind, hf) + function PotentialFlow(kgrad::K, upwind::U, hf::HF; ad::Symbol = :generic) where {K, U, HF} + @assert ad in (:fvm, :generic) + return new{ad, K, U, HF}(kgrad, upwind, hf) end end function PotentialFlow(g::JutulMesh; kwarg...) N = get_neighborship(g) nc = number_of_cells(g) - PotentialFlow(N, nc; kwarg...) + return PotentialFlow(N, nc; kwarg...) end -function PotentialFlow(N::AbstractMatrix, nc = maximum(N); kgrad = nothing, upwind = nothing) +function PotentialFlow(N::AbstractMatrix, nc = maximum(N); kgrad = nothing, upwind = nothing, ad = :generic) nf = size(N, 2) hf = half_face_map(N, nc) T = eltype(N) - if isnothing(kgrad) + if isnothing(kgrad) || kgrad == :tpfa kgrad = Vector{TPFA{T}}(undef, nf) for i in 1:nf left = N[1, i] @@ -55,7 +76,7 @@ function PotentialFlow(N::AbstractMatrix, nc = maximum(N); kgrad = nothing, upwi kgrad[i] = TPFA(left, right, face_sign) end end - if isnothing(upwind) + if isnothing(upwind) || upwind == :spu upwind = Vector{SPU{T}}(undef, nf) for i in 1:nf left = N[1, i] @@ -63,7 +84,31 @@ function PotentialFlow(N::AbstractMatrix, nc = maximum(N); kgrad = nothing, upwi upwind[i] = SPU(left, right) end end - return PotentialFlow(kgrad, upwind, hf) + @assert upwind isa AbstractVector + @assert kgrad isa AbstractVector + return PotentialFlow(kgrad, upwind, hf, ad = ad) +end + +function subdiscretization(disc::PotentialFlow{ad}, subg, mapper::FiniteVolumeGlobalMap) where ad + # kgrad + # upwind + # half_face_map -> N -> remap N -> half_face_map + N, nc = half_face_map_to_neighbors(disc.half_face_map) + + faces = mapper.faces + N = N[:, faces] + # Remap cells in N + for (i, c) in enumerate(N) + N[i] = mapper.global_to_local[c] + end + kgrad = disc.kgrad[faces] + upwind = disc.upwind[faces] + for i in eachindex(kgrad, upwind, faces) + kgrad[i] = subdiscretization(kgrad[i], subg, mapper, faces[i]) + upwind[i] = subdiscretization(upwind[i], subg, mapper, faces[i]) + end + hf = half_face_map(N, nc) + return PotentialFlow(kgrad, upwind, hf, ad = ad) end function local_discretization(eq::ConservationLaw{S, D, FT, N}, i) where {S, D<:PotentialFlow, FT, N} diff --git a/src/conservation/fvm_assembly.jl b/src/conservation/fvm_assembly.jl new file mode 100644 index 00000000..6bf904b0 --- /dev/null +++ b/src/conservation/fvm_assembly.jl @@ -0,0 +1,266 @@ +struct ConservationLawFiniteVolumeStorage{A, HF, HA} + accumulation::A + accumulation_symbol::Symbol + face_flux_cells::HF + face_flux_extra_alignment::HA + unique_alignment::Vector{Int} + neighbors::Vector{Tuple{Int, Int}} +end + +function setup_equation_storage(model, eq::ConservationLaw{conserved, PotentialFlow{:fvm, A, B, C}, <:Any, <:Any}, storage; kwarg...) where {conserved, A, B, C} + return ConservationLawFiniteVolumeStorage(model, eq, storage; kwarg...) +end + +function ConservationLawFiniteVolumeStorage(model, eq, storage; extra_sparsity = nothing, kwarg...) + disc = eq.flow_discretization + conserved = conserved_symbol(eq) + + function F!(out, state, state0, face) + face_disc = (face) -> (kgrad = disc.kgrad[face], upwind = disc.upwind[face]) + local_disc = (face_disc = face_disc,) + tmp = face_disc(face) + dt = 1.0 + N = length(out) + T = eltype(out) + val = @SVector zeros(T, N) + q = face_flux!(val, face, eq, state, model, dt, disc, local_disc) + @. out = q + return out + end + + # N = number_of_entities(model, eq) + N = number_of_faces(model.domain) + n = number_of_equations_per_entity(model, eq) + e = Faces() + nt = count_active_entities(model.domain, e) + caches = create_equation_caches(model, n, N, storage, F!, nt; self_entity = e, kwarg...) + face_cache = caches.Cells + extra_alignment = similar(face_cache.jacobian_positions) + to_zero_pos = Int[] + # Accumulation term + nca = count_active_entities(model.domain, Cells()) + acc_cache = CompactAutoDiffCache(n, nca, model, entity = Cells()) + # TODO: Store unique(extra_alignment and face_cache.jacobian_positions) + @assert only(get_primary_variable_ordered_entities(model)) == Cells() + N = get_neighborship(model.domain.representation) + neighbors = Tuple{Int, Int}[] + @assert size(N, 2) == nt + for i in 1:nt + push!(neighbors, (N[1, i], N[2, i])) + end + + return ConservationLawFiniteVolumeStorage(acc_cache, conserved, face_cache, extra_alignment, to_zero_pos, neighbors) +end + +function declare_pattern(model, e::ConservationLaw, e_s::ConservationLawFiniteVolumeStorage, entity::Cells) + nc_active = count_active_entities(model.domain, Cells()) + hf_map = e.flow_discretization.half_face_map + face_cache = e_s.face_flux_cells + vpos = face_cache.vpos + vars = face_cache.variables + IJ = Vector{Tuple{Int, Int}}() + (; face_pos, cells, faces) = hf_map + N = e_s.neighbors + for c in 1:nc_active + push!(IJ, (c, c)) + end + N = e_s.neighbors + for face in eachindex(N) + lc, rc = N[face] + for fpos in vrange(face_cache, face) + cell = vars[fpos] + push!(IJ, (lc, cell)) + push!(IJ, (rc, cell)) + end + end + IJ = unique!(IJ) + return (map(first, IJ), map(last, IJ)) +end + +function declare_pattern(model, e::ConservationLaw, e_s::ConservationLawFiniteVolumeStorage, entity) + @warn "Using hard-coded conservation law for entity $entity may give incorrect Jacobian. Assuming no dependence upon this entity for conservation law." + I = Vector{Int64}() + J = Vector{Int64}() + return (I, J) +end + +function align_to_jacobian!(eq_s::ConservationLawFiniteVolumeStorage, eq::ConservationLaw, jac, model, u::Cells; equation_offset = 0, variable_offset = 0) + fd = eq.flow_discretization + M = global_map(model.domain) + + acc = eq_s.accumulation + diagonal_alignment!(acc, eq, jac, u, model.context, target_offset = equation_offset, source_offset = variable_offset) + nf = number_of_faces(model.domain) + face_cache = eq_s.face_flux_cells + vpos = face_cache.vpos + vars = face_cache.variables + nc, _, _ = ad_dims(acc) + nu, ne, np = ad_dims(face_cache) + @assert nu == nf + left_facepos = face_cache.jacobian_positions + right_facepos = eq_s.face_flux_extra_alignment + N = eq_s.neighbors + for face in 1:nf + l, r = N[face] + for fpos in vpos[face]:(vpos[face+1]-1) + cell = vars[fpos] + for e in 1:ne + for d = 1:np + pos = find_jac_position( + jac, + l, cell, + 0, 0, + equation_offset, variable_offset, + e, d, + nc, nc, + ne, np, + model.context + ) + set_jacobian_pos!(left_facepos, fpos, e, d, np, pos) + pos = find_jac_position( + jac, + r, cell, + 0, 0, + equation_offset, variable_offset, + e, d, + nc, nc, + ne, np, + model.context + ) + set_jacobian_pos!(right_facepos, fpos, e, d, np, pos) + end + end + end + end + # Store all touched elements that need to be reset to zero before assembly. + ua = eq_s.unique_alignment + for i in acc.jacobian_positions + push!(ua, i) + end + for i in left_facepos + push!(ua, i) + end + for i in right_facepos + push!(ua, i) + end + unique!(ua) + @assert minimum(ua) > 0 + return eq_s +end + +function update_equation!(eq_s::ConservationLawFiniteVolumeStorage, law::ConservationLaw, storage, model, dt) + for i in 1:number_of_entities(model, law) + prepare_equation_in_entity!(i, law, eq_s, storage.state, storage.state0, model, dt) + end + @tic "accumulation" update_accumulation!(eq_s, law, storage, model, dt) + @tic "fluxes" fvm_update_face_fluxes!(eq_s, law, storage, model, dt) +end + +function fvm_update_face_fluxes!(eq_s, law, storage, model, dt) + disc = law.flow_discretization + @inline @inbounds function face_disc(face) + return ( + kgrad = disc.kgrad[face], + upwind = disc.upwind[face] + ) + end + local_disc = (face_disc = face_disc,) + + face_cache = eq_s.face_flux_cells + nu, ne, np = ad_dims(face_cache) + T = eltype(face_cache) + val = @SVector zeros(T, ne) + local_state = local_ad(storage.state, 1, T) + vars = face_cache.variables + fvm_update_face_fluxes_inner!(face_cache, model, law, disc, local_disc, dt, vars, local_state, nu, val) +end + +function fvm_update_face_fluxes_inner!(face_cache, model, law, disc, local_disc, dt, vars, local_state, nu, val) + for face in 1:nu + @inbounds for j in vrange(face_cache, face) + v_i = @views face_cache.entries[:, j] + var = vars[j] + + state_i = new_entity_index(local_state, var) + flux = face_flux!(val, face, law, state_i, model, dt, disc, local_disc) + for i in eachindex(flux) + @inbounds v_i[i] = flux[i] + end + end + end +end + +@inline function get_diagonal_entries(eq::ConservationLaw, eq_s::ConservationLawFiniteVolumeStorage) + return eq_s.accumulation.entries +end + +function update_linearized_system_equation!(nz, r, model, eq::ConservationLaw, eq_s::ConservationLawFiniteVolumeStorage) + # Zero out the buffers + zero_ix = eq_s.unique_alignment + @inbounds for i in zero_ix + nz[i] = 0.0 + end + # Accumulation term + if false + # TODO: Something wrong with expected transpose of r in old cache for residuals. + fill_equation_entries!(nz, r, model, eq_s.accumulation) + else + acc = eq_s.accumulation + nc, ne, np = ad_dims(acc) + for i in 1:nc + @inbounds for e in 1:ne + a = get_entry(acc, i, e) + r[e, i] = a.value + for d = 1:np + update_jacobian_entry!(nz, acc, i, e, d, a.partials[d]) + end + end + end + end + # Fill fluxes + face_cache = eq_s.face_flux_cells + face_fluxes = face_cache.entries + left_facepos = face_cache.jacobian_positions + right_facepos = eq_s.face_flux_extra_alignment + nu, ne, np = ad_dims(face_cache) + vpos = face_cache.vpos + vars = face_cache.variables + + nc = number_of_cells(model.domain)::Int + @assert size(r, 1) == ne + @assert vpos[end]-1 == size(face_fluxes, 2) + N = eq_s.neighbors + fvm_face_assembly!(r, nz, vpos, face_cache, left_facepos, right_facepos, N, nu, Val(ne), Val(np)) +end + +function fvm_face_assembly!(r, nz, vpos, face_cache, left_facepos, right_facepos, N, nu, ::Val{ne}, ::Val{np}) where {ne, np} + @inbounds for face in 1:nu + lc, rc = N[face] + start = vpos[face] + stop = vpos[face+1]-1 + if start == stop + # No sparsity? A bit odd but guard against it. + continue + end + @inbounds for e in 1:ne + qval = get_entry_val(face_cache, start, e) + r[e, lc] += qval + r[e, rc] -= qval + end + for fpos in start:stop + for e in 1:ne + # Flux (with derivatives with respect to some cell) + q = get_entry(face_cache, fpos, e) + @inbounds for d in 1:np + ∂q = q.partials[d] + # Flux for left cell (l -> r) + lc_i = get_jacobian_pos(np, fpos, e, d, left_facepos) + nz[lc_i] += ∂q + # Flux for right cell (r -> l) + rc_i = get_jacobian_pos(np, fpos, e, d, right_facepos) + nz[rc_i] -= ∂q + end + end + end + end +end diff --git a/src/core_types/core_types.jl b/src/core_types/core_types.jl index c4cbf411..1eef85f2 100644 --- a/src/core_types/core_types.jl +++ b/src/core_types/core_types.jl @@ -502,9 +502,9 @@ end import Base: getindex, @propagate_inbounds, parent, size, axes struct JutulStorage{K} - data::Union{Dict{Symbol, Any}, K} - function JutulStorage(S = Dict{Symbol, Any}(); kwarg...) - if isa(S, Dict) + data::Union{OrderedDict{Symbol, Any}, K} + function JutulStorage(S = OrderedDict{Symbol, Any}(); kwarg...) + if isa(S, AbstractDict) K = Nothing for (k, v) in kwarg S[k] = v @@ -523,8 +523,30 @@ function convert_to_immutable_storage(S::JutulStorage) return JutulStorage(tup) end -Base.iterate(S::JutulStorage) = Base.iterate(data(S)) +function Base.getindex(S::JutulStorage, i::Int) + d = data(S) + if d isa OrderedDict + for (j, v) in enumerate(values(d)) + if j == i + return v + end + end + throw(BoundsError("Out of bounds $i for JutulStorage.")) + else + return d[i] + end +end +Base.length(S::JutulStorage, arg...) = Base.length(values(S), arg...) +Base.iterate(S::JutulStorage, arg...) = Base.iterate(values(S), arg...) +function Base.map(f, S::JutulStorage) + d = data(S) + if d isa OrderedDict + d = NamedTuple(d) + end + return Base.map(f, d) +end Base.pairs(S::JutulStorage) = Base.pairs(data(S)) +Base.values(S::JutulStorage) = Base.values(data(S)) function Base.getproperty(S::JutulStorage{Nothing}, name::Symbol) Base.getindex(data(S), name) @@ -561,7 +583,7 @@ function Base.haskey(S::JutulStorage{Nothing}, name::Symbol) end function Base.keys(S::JutulStorage{Nothing}) - return Base.keys(data(S)) + return Tuple(keys(data(S))) end @@ -731,6 +753,8 @@ struct GenericAutoDiffCache{N, E, ∂x, A, P, M, D, VM} <: JutulAutoDiffCache wh end end +Base.eltype(::GenericAutoDiffCache{N, E, ∂x, A, P, M, D, VM}) where {N, E, ∂x, A, P, M, D, VM} = ∂x + "Discretization of kgradp + upwind" abstract type FlowDiscretization <: JutulDiscretization end @@ -911,81 +935,116 @@ end Base.transpose(c::CrossTermPair) = CrossTermPair(c.source, c.target, c.source_equation, c.target_equation, c.cross_term,) +abstract type AbstractMultiModel{label} <: JutulModel end +multimodel_label(::AbstractMultiModel{L}) where L = L """ MultiModel(models) + MultiModel(models, :SomeLabel) A model variant that is made up of many named submodels, each a fully realized [`SimulationModel`](@ref). `models` should be a `NamedTuple` or `Dict{Symbol, JutulModel}`. """ -struct MultiModel{T} <: JutulModel - models::NamedTuple - cross_terms::Vector{CrossTermPair} - groups::Union{Vector, Nothing} - context::Union{JutulContext, Nothing} +struct MultiModel{label, T, CT, G, C, GL} <: AbstractMultiModel{label} + models::T + cross_terms::CT + groups::G + context::C reduction::Union{Symbol, Nothing} specialize_ad::Bool - group_lookup::Dict{Symbol, Int} -function MultiModel(models; cross_terms = Vector{CrossTermPair}(), groups = nothing, context = nothing, reduction = missing, specialize = false, specialize_ad = false) - group_lookup = Dict{Symbol, Any}() - if isnothing(groups) - num_groups = 1 - for k in keys(models) - group_lookup[k] = 1 - end - if ismissing(reduction) - reduction = nothing - end - else - if ismissing(reduction) - reduction = :schur_apply - end - nm = length(models) - num_groups = length(unique(groups)) - @assert maximum(groups) <= nm - @assert minimum(groups) > 0 - @assert length(groups) == nm - @assert maximum(groups) == num_groups "Groups must be ordered from 1 to n, was $(unique(groups))" - if !issorted(groups) - # If the groups aren't grouped sequentially, re-sort them so they are - # since parts of the multimodel code depends on this ordering - ix = sortperm(groups) - new_models = OrderedDict{Symbol, Any}() - old_keys = keys(models) - for i in ix - k = old_keys[i] - new_models[k] = models[k] - end - models = new_models - groups = groups[ix] - end - for (k, g) in zip(keys(models), groups) - group_lookup[k] = g - end + group_lookup::GL +end + +function MultiModel(models, label::Union{Nothing, Symbol} = nothing; cross_terms = Vector{CrossTermPair}(), groups = nothing, context = nothing, reduction = missing, specialize = false, specialize_ad = false) + group_lookup = Dict{Symbol, Int}() + if isnothing(groups) + num_groups = 1 + for k in keys(models) + group_lookup[k] = 1 end - if isa(models, AbstractDict) - models = convert_to_immutable_storage(models) + if ismissing(reduction) + reduction = nothing end - if reduction == :schur_apply - if length(groups) == 1 - reduction = nothing - end + else + if ismissing(reduction) + reduction = :schur_apply end - if isnothing(groups) && !isnothing(context) - for (i, m) in enumerate(models) - if matrix_layout(m.context) != matrix_layout(context) - error("No groups provided, but the outer context does not match the inner context for model $i") - end + groups::Vector{Int} + nm = length(models) + num_groups = length(unique(groups)) + @assert maximum(groups) <= nm + @assert minimum(groups) > 0 + @assert length(groups) == nm + @assert maximum(groups) == num_groups "Groups must be ordered from 1 to n, was $(unique(groups))" + if !issorted(groups) + # If the groups aren't grouped sequentially, re-sort them so they are + # since parts of the multimodel code depends on this ordering + ix = sortperm(groups) + new_models = OrderedDict{Symbol, Any}() + old_keys = keys(models) + for i in ix + k = old_keys[i] + new_models[k] = models[k] end + models = new_models + groups = groups[ix] end - if specialize - T = typeof(models) - else - T = nothing + for (k, g) in zip(keys(models), groups) + group_lookup[k] = g end - new{T}(models, cross_terms, groups, context, reduction, specialize_ad, group_lookup) end + if isa(models, AbstractDict) + models = JutulStorage(models) + elseif models isa NamedTuple + models_new = JutulStorage() + for (k, v) in pairs(models) + models_new[k] = v + end + models = models_new + else + models::JutuLStorage + end + if reduction == :schur_apply + if length(groups) == 1 + reduction = nothing + end + end + if isnothing(groups) && !isnothing(context) + for (i, m) in enumerate(models) + if matrix_layout(m.context) != matrix_layout(context) + error("No groups provided, but the outer context does not match the inner context for model $i") + end + end + end + if specialize + models = convert_to_immutable_storage(models) + end + T = typeof(models) + CT = typeof(cross_terms) + G = typeof(groups) + C = typeof(context) + GL = typeof(group_lookup) + return MultiModel{label, T, CT, G, C, GL}(models, cross_terms, groups, context, reduction, specialize_ad, group_lookup) +end + +function MultiModel(models, ::Val{label}; kwarg...) where label + # BattMo compatability, support ::Val for symbol + return MultiModel(models, label; kwarg...) +end + +function convert_to_immutable_storage(model::MultiModel) + (; models, cross_terms, groups, context, reduction, specialize_ad, group_lookup) = model + models = convert_to_immutable_storage(models) + cross_terms = Tuple(cross_terms) + group_lookup = convert_to_immutable_storage(group_lookup) + label = multimodel_label(model) + T = typeof(models) + CT = typeof(cross_terms) + G = typeof(groups) + C = typeof(context) + GL = typeof(group_lookup) + return MultiModel{label, T, CT, G, C, GL}(models, cross_terms, groups, context, reduction, specialize_ad, group_lookup) end """ @@ -1006,7 +1065,7 @@ struct IndirectionMap{V} @assert length(vals) == lastpos - 1 "Expected vals to have length lastpos - 1 = $(lastpos-1), was $(length(vals))" @assert pos[1] == 1 new{V}(vals, pos) - end + end end struct IndexRenumerator{T} diff --git a/src/dd/submodels.jl b/src/dd/submodels.jl index f1004d72..9be49e4a 100644 --- a/src/dd/submodels.jl +++ b/src/dd/submodels.jl @@ -27,6 +27,7 @@ function submodel(model::SimulationModel, p_i::AbstractVector; context = model.c transfer_vars!(new_model.primary_variables, model.primary_variables) transfer_vars!(new_model.secondary_variables, model.secondary_variables) transfer_vars!(new_model.parameters, model.parameters) + transfer_vars!(new_model.equations, model.equations) new_data_domain = new_model.data_domain old_data_domain = model.data_domain @@ -76,7 +77,7 @@ function submodel(model::MultiModel, mp::SimpleMultiModelPartition, index; kwarg groups_0 = model.groups has_groups = !isnothing(groups_0) if has_groups - groups = Vector{Integer}() + groups = Vector{Int}() end for (i, k) in enumerate(keys(submodels)) diff --git a/src/domains.jl b/src/domains.jl index 03c9a40c..441d99c5 100644 --- a/src/domains.jl +++ b/src/domains.jl @@ -105,6 +105,21 @@ function half_face_map(N, nc) return (cells = cells, faces = faces, face_pos = face_pos, face_sign = signs) end +function half_face_map_to_neighbors(fmap) + (; cells, faces, face_pos, face_sign) = fmap + nc = length(face_pos)-1 + nf = maximum(faces) + N = zeros(Int, 2, nf) + for (face, cell, sgn) in zip(faces, cells, face_sign) + if sgn == -1 + N[1, face] = cell + else + N[2, face] = cell + end + end + return (N, nc) +end + function local_half_face_map(cd, cell_index) loc = cd.face_pos[cell_index]:(cd.face_pos[cell_index+1]-1) faces = @views cd.faces[loc] diff --git a/src/equations.jl b/src/equations.jl index 7d1c32e2..67e6b47a 100644 --- a/src/equations.jl +++ b/src/equations.jl @@ -303,7 +303,7 @@ end Give out I, J arrays of equal length for a given equation attached to the given model. """ -function declare_sparsity(model, e::JutulEquation, eq_storage, entity, row_layout, col_layout = row_layout) +function declare_sparsity(model, e, eq_storage, entity, row_layout, col_layout = row_layout) primitive = declare_pattern(model, e, eq_storage, entity) if isnothing(primitive) out = nothing @@ -370,7 +370,7 @@ function expand_block_indices(I, J, ntotal, neqs, layout::EntityMajorLayout; equ end -function declare_sparsity(model, e::JutulEquation, eq_storage, entity, row_layout::T, col_layout::T = row_layout) where T<:BlockMajorLayout +function declare_sparsity(model, e, eq_storage, entity, row_layout::T, col_layout::T = row_layout) where T<:BlockMajorLayout primitive = declare_pattern(model, e, eq_storage, entity) if isnothing(primitive) out = nothing @@ -394,7 +394,7 @@ function declare_pattern(model, e, eq_s, entity, arg...) k = entity_as_symbol(entity) if haskey(eq_s, k) cache = eq_s[k] - return generic_cache_declare_pattern(cache, arg...) + out = generic_cache_declare_pattern(cache, arg...) else out = nothing end diff --git a/src/interpolation.jl b/src/interpolation.jl index b59368a6..6331bcac 100644 --- a/src/interpolation.jl +++ b/src/interpolation.jl @@ -116,7 +116,13 @@ function get_1d_interpolator(xs, ys; cap_start = cap_endpoints, kwarg... ) - if cap_endpoints && (cap_start || cap_end) + if length(xs) == 1 + xs = only(xs) + ys = only(ys) + u = one(typeof(xs)) + xs = [xs - u, xs, xs + u] + ys = [ys, ys, ys] + elseif cap_endpoints && (cap_start || cap_end) xs = copy(xs) ys = copy(ys) # Add perturbed points, repeat start and end value diff --git a/src/meshes/cart.jl b/src/meshes/cart.jl index 9b46c318..3c63138f 100644 --- a/src/meshes/cart.jl +++ b/src/meshes/cart.jl @@ -94,7 +94,7 @@ end Lower corner for one dimension, without any transforms applied """ coord_offset(pos, δ::AbstractFloat) = (pos-1)*δ -coord_offset(pos, δ::AbstractVector) = sum(δ[1:(pos-1)]) +coord_offset(pos, δ::Union{AbstractVector, Tuple}) = sum(δ[1:(pos-1)], init = 0.0) """ cell_index(g, pos) diff --git a/src/meshes/meshes.jl b/src/meshes/meshes.jl index 88e7ffef..67f9018e 100644 --- a/src/meshes/meshes.jl +++ b/src/meshes/meshes.jl @@ -195,6 +195,7 @@ include("mrst.jl") include("cart.jl") include("unstructured/unstructured.jl") include("coarse.jl") +include("trajectories.jl") function declare_entities(G::JutulMesh) nf = number_of_faces(G) diff --git a/src/meshes/trajectories.jl b/src/meshes/trajectories.jl new file mode 100644 index 00000000..05c4264a --- /dev/null +++ b/src/meshes/trajectories.jl @@ -0,0 +1,146 @@ +function trajectory_to_points(trajectory::Matrix{Float64}) + N = size(trajectory, 2) + @assert N in (2, 3) "2D/3D matrices are supported." + return collect(vec(reinterpret(SVector{N, Float64}, collect(trajectory')))) +end + +function trajectory_to_points(x::AbstractVector{SVector{N, Float64}}) where N + return x +end + + +""" + find_enclosing_cells(G, traj; geometry = tpfv_geometry(G), n = 25) + +Find the cell indices of cells in the mesh `G` that are intersected by a given +trajectory `traj`. `traj` can be either a matrix with equal number of columns as +dimensions in G (i.e. three columns for 3D) or a `Vector` of `SVector` instances +with the same length. + +The optional argument `geometry` is used to define the centroids and normals +used in the calculations. You can precompute this if you need to perform many +searches. The keyword argument `n` can be used to set the number of +discretizations in each segment. + +Examples: +``` +# 3D mesh +G = CartesianMesh((4, 4, 5), (100.0, 100.0, 100.0)) +trajectory = [ + 50.0 25.0 1; + 55 35.0 25; + 65.0 40.0 50.0; + 70.0 70.0 90.0 +] + +cells = Jutul.find_enclosing_cells(G, trajectory) + +# Optional plotting, requires Makie: +fig, ax, plt = Jutul.plot_mesh_edges(G) +plot_mesh!(ax, G, cells = cells, alpha = 0.5, transparency = true) +lines!(ax, trajectory, linewidth = 10) +fig +# 2D mesh +G = CartesianMesh((50, 50), (1.0, 2.0)) +trajectory = [ + 0.1 0.1; + 0.2 0.4; + 0.3 1.2 +] +fig, ax, plt = Jutul.plot_mesh_edges(G) +cells = Jutul.find_enclosing_cells(G, trajectory) +# Plotting, needs Makie +fig, ax, plt = Jutul.plot_mesh_edges(G) +plot_mesh!(ax, G, cells = cells, alpha = 0.5, transparency = true) +lines!(ax, trajectory[:, 1], trajectory[:, 2], linewidth = 3) +fig +``` +""" +function find_enclosing_cells(G, traj; geometry = missing, n = 25) + G = UnstructuredMesh(G) + if ismissing(geometry) + geometry = tpfv_geometry(G) + end + pts = trajectory_to_points(traj) + T = eltype(pts) + normals = vec(reinterpret(T, geometry.normals)) + face_centroids = vec(reinterpret(T, geometry.face_centroids)) + cell_centroids = vec(reinterpret(T, geometry.cell_centroids)) + + boundary_centroids = vec(reinterpret(T, geometry.boundary_centroids)) + boundary_normals = vec(reinterpret(T, geometry.boundary_normals)) + + # Start search near middle of trajectory + mean_pt = mean(pts) + nc = number_of_cells(G) + cells_by_dist = sort(1:nc, by = cell -> norm(cell_centroids[cell] - mean_pt, 2)) + + nseg = length(pts)-1 + intersected_cells = Int[] + lengths = Float64 + for i in 1:nseg + pt_start = pts[i] + pt_end = pts[i+1] + for pt in range(pt_start, pt_end, n) + ix = find_enclosing_cell(G, pt, normals, face_centroids, boundary_normals, boundary_centroids, cells_by_dist) + if !isnothing(ix) + push!(intersected_cells, ix) + end + end + end + return unique!(intersected_cells) +end + +""" + find_enclosing_cell(G::UnstructuredMesh{D}, pt::SVector{D, T}, + normals::AbstractVector{SVector{D, T}}, + face_centroids::AbstractVector{SVector{D, T}}, + boundary_normals::AbstractVector{SVector{D, T}}, + boundary_centroids::AbstractVector{SVector{D, T}}, + cells = 1:number_of_cells(G) + ) where {D, T} + +Find enclosing cell of a point. This can be a bit expensive for larger meshes. +Recommended to use the more high level `find_enclosing_cells` instead. +""" +function find_enclosing_cell(G::UnstructuredMesh{D}, pt::SVector{D, T}, + normals::AbstractVector{SVector{D, T}}, + face_centroids::AbstractVector{SVector{D, T}}, + boundary_normals::AbstractVector{SVector{D, T}}, + boundary_centroids::AbstractVector{SVector{D, T}}, + cells = 1:number_of_cells(G) + ) where {D, T} + inside_normal(pt, normal, centroid) = dot(normal, pt - centroid) <= 0 + for cell in cells + inside = true + for face in G.faces.cells_to_faces[cell] + if G.faces.neighbors[face][1] == cell + sgn = 1 + else + sgn = -1 + end + normal = sgn*normals[face] + center = face_centroids[face] + inside = inside && inside_normal(pt, normal, center) + if !inside + break + end + end + if !inside + continue + end + + for bface in G.boundary_faces.cells_to_faces[cell] + normal = boundary_normals[bface] + center = boundary_centroids[bface] + inside = inside && inside_normal(pt, normal, center) + if !inside + break + end + end + if inside + return cell + end + end + return nothing +end diff --git a/src/meshes/unstructured/geometry.jl b/src/meshes/unstructured/geometry.jl index 6ac398a9..3d970f44 100644 --- a/src/meshes/unstructured/geometry.jl +++ b/src/meshes/unstructured/geometry.jl @@ -1,16 +1,16 @@ -function compute_centroid_and_measure(G::UnstructuredMesh{3}, ::Faces, i) +function compute_centroid_and_measure(G::UnstructuredMesh, ::Faces, i) nodes = G.faces.faces_to_nodes[i] pts = G.node_points return face_centroid_and_measure(nodes, pts) end -function compute_centroid_and_measure(G::UnstructuredMesh{3}, ::BoundaryFaces, i) +function compute_centroid_and_measure(G::UnstructuredMesh, ::BoundaryFaces, i) nodes = G.boundary_faces.faces_to_nodes[i] pts = G.node_points return face_centroid_and_measure(nodes, pts) end -function face_centroid_and_measure(nodes, pts) +function face_centroid_and_measure(nodes, pts::Vector{SVector{3, Num}}) where {Num} T = eltype(pts) c_node = zero(T) for n in nodes @@ -40,7 +40,15 @@ function face_centroid_and_measure(nodes, pts) return (centroid./area, area) end -function compute_centroid_and_measure(G::UnstructuredMesh{3}, ::Cells, i) +function face_centroid_and_measure(nodes, pts::Vector{SVector{2, Num}}) where {Num} + @assert length(nodes) == 2 + l, r = nodes + centroid = (pts[l] + pts[r])/2.0 + area = norm(pts[l] - pts[r], 2) + return (centroid, area) +end + +function compute_centroid_and_measure(G::UnstructuredMesh, ::Cells, i) pts = G.node_points T = eltype(pts) c_node = zero(T) @@ -68,9 +76,8 @@ function compute_centroid_and_measure(G::UnstructuredMesh{3}, ::Cells, i) return (centroid./vol, vol) end -function sum_centroid_volumes_helper(pts, c_node, faces, centroid, vol, i) - T = eltype(pts) - c_node::T +function sum_centroid_volumes_helper(pts::Vector{SVector{N, E}}, c_node::SVector{N, E}, faces, centroid::SVector{N, E}, vol, i) where {N, E} + T = SVector{N, E} for face in faces.cells_to_faces[i] nodes = faces.faces_to_nodes[face] # Compute center point (not centroid) for face @@ -79,29 +86,97 @@ function sum_centroid_volumes_helper(pts, c_node, faces, centroid, vol, i) c_node_face += pts[node] end c_node_face /= length(nodes) - # Then create tets and compute volume - for i in eachindex(nodes) - if i == 1 - l = nodes[end] - else - l = nodes[i-1] + if N == 3 + # Then create tets and compute volume + for i in eachindex(nodes) + if i == 1 + l = nodes[end] + else + l = nodes[i-1] + end + r = nodes[i] + l_node = pts[l] + r_node = pts[r] + if N == 3 + M = SMatrix{4, 4, Float64, 16}( + l_node[1], r_node[1], c_node[1], c_node_face[1], + l_node[2], r_node[2], c_node[2], c_node_face[2], + l_node[3], r_node[3], c_node[3], c_node_face[3], + 1.0, 1.0, 1.0, 1.0 + ) + local_volume = (1.0/6.0)*abs(det(M)) + local_centroid = (1.0/4.0)*(l_node + r_node + c_node_face + c_node) + else + A = r_node - c_node + B = l_node - c_node + local_volume = abs(cross(A, B)/4) + local_centroid = (l_node + r_node + c_node)/3.0 + @assert local_volume >= 0 + end + vol += local_volume + centroid += local_centroid*local_volume end - r = nodes[i] + else + # 2D is much simpler (area = volume in Jutul) + @assert length(nodes) == 2 + l, r = nodes l_node = pts[l] r_node = pts[r] - - M = SMatrix{4, 4, Float64, 16}( - l_node[1], r_node[1], c_node[1], c_node_face[1], - l_node[2], r_node[2], c_node[2], c_node_face[2], - l_node[3], r_node[3], c_node[3], c_node_face[3], - 1.0, 1.0, 1.0, 1.0 - ) - local_volume = (1.0/6.0)*abs(det(M)) - local_centroid = (1.0/4.0)*(l_node + r_node + c_node_face + c_node) - + A = l_node - c_node + B = r_node - c_node + local_volume = abs(cross(A, B)/2.0) + local_centroid = (l_node + r_node + c_node)/3.0 vol += local_volume centroid += local_centroid*local_volume end end return (centroid, vol) end + +function face_normal(G::UnstructuredMesh{3}, f, e = Faces()) + get_nodes(::Faces) = G.faces + get_nodes(::BoundaryFaces) = G.boundary_faces + nodes = get_nodes(e).faces_to_nodes[f] + pts = G.node_points + n = length(nodes) + # If the geometry is well defined it would be sufficient to take the first + # triplet and use that to generate the normals. We assume it isn't and + # create a weighted sum where each weight corresponds to the areal between + # the triplets. + normal = zero(eltype(pts)) + for i in 1:n + if i == 1 + a = pts[nodes[n]] + else + a = pts[nodes[i-1]] + end + b = pts[nodes[i]] + if i == n + c = pts[nodes[1]] + else + c = pts[nodes[i+1]] + end + normal += cross(c - b, a - b) + end + normal /= norm(normal, 2) + return normal +end + +function face_normal(G::UnstructuredMesh{2}, f, e = Faces()) + get_nodes(::Faces) = G.faces + get_nodes(::BoundaryFaces) = G.boundary_faces + nodes = get_nodes(e).faces_to_nodes[f] + pts = G.node_points + n = length(nodes) + @assert n == 2 + T = eltype(pts) + l, r = nodes + pt_l = pts[l] + pt_r = pts[r] + + v = pt_r - pt_l + normal = T(v[2], -v[1]) + + return normal/norm(normal, 2) +end + diff --git a/src/meshes/unstructured/plotting.jl b/src/meshes/unstructured/plotting.jl index 2da95fa5..3a4fe9c6 100644 --- a/src/meshes/unstructured/plotting.jl +++ b/src/meshes/unstructured/plotting.jl @@ -1,7 +1,6 @@ -function Jutul.triangulate_mesh(m::UnstructuredMesh{3}; outer = false, flatten = true) - N = 3 - pts = Vector{SVector{N, Float64}}() - tri = Vector{SVector{N, Int64}}() +function Jutul.triangulate_mesh(m::UnstructuredMesh{D}; outer = false, flatten = true) where D + pts = Vector{SVector{D, Float64}}() + tri = Vector{SVector{3, Int64}}() cell_index = Vector{Int64}() face_index = Vector{Int64}() @@ -12,12 +11,24 @@ function Jutul.triangulate_mesh(m::UnstructuredMesh{3}; outer = false, flatten = # Assume hexahedral, 6 faces per cell, triangulated into 4 parts each sizehint!(d, 24*number_of_cells(m)) end + if D == 2 + cell_centroids = SVector{D, Float64}[] + nc = number_of_cells(m) + for i in 1:nc + c, v = compute_centroid_and_measure(m, Cells(), i) + push!(cell_centroids, c) + end + else + # Not needed. + cell_centroids = missing + end - add_points!(e, e_def, offset) = triangulate_and_add_faces!(dest, m, e, e_def, offset = offset) + add_points!(e, e_def, offset, face_offset) = triangulate_and_add_faces!(dest, m, e, e_def, cell_centroids, offset = offset, face_offset = face_offset) if !outer - offset = add_points!(Faces(), m.faces, offset) + offset = add_points!(Faces(), m.faces, offset, 0) end - offset = add_points!(BoundaryFaces(), m.boundary_faces, offset) + face_offset = number_of_faces(m) + offset = add_points!(BoundaryFaces(), m.boundary_faces, offset, face_offset) if flatten pts = plot_flatten_helper(pts) @@ -25,6 +36,7 @@ function Jutul.triangulate_mesh(m::UnstructuredMesh{3}; outer = false, flatten = end cell_buffer = zeros(length(cell_index)) face_buffer = zeros(length(face_index)) + mapper = ( Cells = (cell_data) -> mesh_data_to_tris!(cell_buffer, cell_data, cell_index)::Vector{Float64}, Faces = (face_data) -> mesh_data_to_tris!(face_buffer, face_data, face_index)::Vector{Float64}, @@ -43,7 +55,7 @@ function mesh_data_to_tris!(out::Vector{Float64}, cell_data, cell_index) return out::Vector{Float64} end -function triangulate_and_add_faces!(dest, m, e, faces; offset = 0) +function triangulate_and_add_faces!(dest, m, e, faces, cell_centroids; offset = 0, face_offset = 0) node_pts = m.node_points T = eltype(node_pts) for f in 1:count_entities(m, e) @@ -54,13 +66,33 @@ function triangulate_and_add_faces!(dest, m, e, faces; offset = 0) C += node_pts[node] end C /= n - offset = triangulate_and_add_faces!(dest, f, faces.neighbors[f], C, nodes, node_pts, n; offset = offset) + offset = triangulate_and_add_faces!(dest, f + face_offset, faces.neighbors[f], C, nodes, node_pts, cell_centroids, n; offset = offset) + end + return offset +end + +function triangulate_and_add_faces!(dest, face, neighbors, C, nodes, node_pts::Vector{SVector{2, T}}, cell_centroids, n; offset = 0) where {T} + cell_index, face_index, pts, tri = dest + new_vert_count = n + 1 + @assert new_vert_count == 3 + for cell in neighbors + if cell > 0 + for i in 1:new_vert_count + push!(cell_index, cell) + push!(face_index, face) + push!(pts, svector_local_point(cell_centroids[cell], i-1, nodes, node_pts)) + end + for i in 1:n + push!(tri, svector_cyclical_tesselation(n, i, offset)) + end + offset = offset + new_vert_count + end end return offset end -function triangulate_and_add_faces!(dest, face, neighbors, C, nodes, node_pts, n; offset = 0) +function triangulate_and_add_faces!(dest, face, neighbors, C, nodes, node_pts::Vector{SVector{3, T}}, cell_centroids, n; offset = 0) where {T} cell_index, face_index, pts, tri = dest if n == 4 # TODO: Could add a 3 mesh specialization here diff --git a/src/meshes/unstructured/types.jl b/src/meshes/unstructured/types.jl index 20913918..63295ad4 100644 --- a/src/meshes/unstructured/types.jl +++ b/src/meshes/unstructured/types.jl @@ -79,7 +79,7 @@ function convert_neighborship(N::Vector{Tuple{Int, Int}}; nc = nothing, nf = not end if !isnothing(nc) for (f, t) in enumerate(N) - for (c, i) in enumerate(t) + for (i, c) in enumerate(t) @assert c <= nc "Neighborship exceeded $nc in face $f cell $i: neighborship was $t" if allow_zero @assert c >= 0 "Neighborship was negative in face $f cell $i: neighborship was $t" @@ -125,9 +125,14 @@ function UnstructuredMesh(cells_faces, cells_facepos, faces_nodes, faces_nodespo n = length(npos) bnd = l == 0 || r == 0 if bnd + if l == 0 + npos = reverse(npos) + end + for i in npos push!(boundary_faces_nodes, faces_nodes[i]) end + push!(boundary_faces_nodespos, boundary_faces_nodespos[end] + n) added_boundary += 1 # Minus sign means boundary index @@ -284,27 +289,197 @@ end """ UnstructuredMesh(g::CartesianMesh) -Convert `CartesianMesh` instance to unstructured grid (3D only) +Convert `CartesianMesh` instance to unstructured grid. Note that the mesh must +be 2D and 3D for a 1-to-1 conversion. 1D meshes are implicitly converted to 2D. """ -function UnstructuredMesh(g::CartesianMesh; kwarg...) +function UnstructuredMesh(g::CartesianMesh; warn_1d = true, kwarg...) d = dim(g) - nx, ny, nz = grid_dims_ijk(g) - if d < 3 - @warn "Conversion from CartesianMesh to UnstructuredMesh is only fully supported for 3D grids. Converting $(d)D grid to 3D." + if d == 1 + if warn_1d + @warn "Conversion from CartesianMesh to UnstructuredMesh is only fully supported for 2D/3D grids. Converting 1D grid to 2D." + end + nx = number_of_cells(g) dy = 1.0 - dz = 1.0 - if d == 2 - dx, dy = g.deltas - X0, Y0 = g.origin - else - @assert d == 1 - dx = only(g.deltas) - X0 = only(g.origin) - Y0 = 0.0 + dx = only(g.deltas) + X0 = only(g.origin) + Y0 = 0.0 + wrap(x::AbstractFloat, n) = fill(x, n) + wrap(x, n) = x + g = CartesianMesh( + (nx, 1), + (wrap(dx, nx), wrap(1.0, 1)), + origin = [X0, Y0] + ) + out = UnstructuredMesh(g) + else + out = unstructured_from_cart(g, Val(d); kwarg...) + end +end + +function unstructured_from_cart(g, ::Val{2}; kwarg...) + d = dim(g) + @assert d == 2 + nx, ny, = grid_dims_ijk(g) + + X0, Y0 = g.origin + + nc = number_of_cells(g) + nf = number_of_faces(g) + nbf = number_of_boundary_faces(g) + num_nodes_x = nx+1 + num_nodes_y = ny+1 + num_nodes = num_nodes_x*num_nodes_y + nodeix = reshape(1:num_nodes, num_nodes_x, num_nodes_y) + + node_points = Vector{SVector{2, Float64}}() + dx, dy = g.deltas + function get_point(D::T, i) where {T<:Real} + newpt = (i-1)*D + return newpt::T + end + function get_point(D::Union{NTuple{N, T}, Vector{T}}, i) where {T<:Real, N} + pt = zero(T) + for j in 1:(i-1) + pt += D[j] + end + return pt::T + end + for j in 1:num_nodes_y + Y = get_point(dy, j) + for i in 1:num_nodes_x + X = get_point(dx, i) + XY = SVector{2, Float64}(X + X0, Y + Y0) + push!(node_points, XY) + end + end + @assert length(node_points) == length(nodeix) + cell_to_faces = Vector{Vector{Int}}() + sizehint!(cell_to_faces, nc) + for i in 1:nc + push!(cell_to_faces, Int[]) + end + cell_to_boundary = Vector{Vector{Int}}() + sizehint!(cell_to_boundary, nc) + for i in 1:nc + push!(cell_to_boundary, Int[]) + end + + int_neighbors = Vector{Tuple{Int, Int}}() + sizehint!(int_neighbors, nf) + # Note: The following loops are arranged to reproduce the MRST ordering. + function insert_face!(nodes, pos, arg...) + for node in arg + push!(nodes, node) + end + push!(pos, pos[end] + length(arg)) + end + function add_internal_neighbor!(t, D) + x, y = t + index = cell_index(g, t) + l = index + r = cell_index(g, (x + (D == 1), y + (D == 2))) + push!(int_neighbors, (l, r)) + end + faces_nodes = Int[] + faces_nodespos = Int[1] + sizehint!(faces_nodes, 4*nf) + sizehint!(faces_nodespos, nf+1) + # Faces with X-normal > 0 + for y in 1:ny + for x in 2:nx + p1 = nodeix[x, y] + p2 = nodeix[x, y+1] + insert_face!(faces_nodes, faces_nodespos, p1, p2) + add_internal_neighbor!((x-1, y), 1) + end + end + # Faces with Y-normal > 0 + for y in 2:ny + for x in 1:nx + p1 = nodeix[x, y] + p2 = nodeix[x+1, y] + insert_face!(faces_nodes, faces_nodespos, p2, p1) + add_internal_neighbor!((x, y-1), 2) + end + end + + boundary_faces_nodes = Int[] + boundary_faces_nodespos = Int[1] + + sizehint!(boundary_faces_nodes, 4*nbf) + sizehint!(boundary_faces_nodespos, nbf+1) + + bnd_cells = Int[] + sizehint!(bnd_cells, nbf) + function add_boundary_cell!(t, D) + index = cell_index(g, t) + push!(bnd_cells, index) + end + for y in 1:ny + for x in [1, nx+1] + p1 = nodeix[x, y] + p2 = nodeix[x, y+1] + if x == 1 + insert_face!(boundary_faces_nodes, boundary_faces_nodespos, p2, p1) + add_boundary_cell!((x, y), 1) + else + insert_face!(boundary_faces_nodes, boundary_faces_nodespos, p1, p2) + add_boundary_cell!((x-1, y), 1) + end + end + end + # Faces with Y-normal > 0 + for x in 1:nx + for y in [1, ny+1] + p1 = nodeix[x, y] + p2 = nodeix[x+1, y] + if y == 1 + insert_face!(boundary_faces_nodes, boundary_faces_nodespos, p1, p2) + add_boundary_cell!((x, y), 2) + else + insert_face!(boundary_faces_nodes, boundary_faces_nodespos, p2, p1) + add_boundary_cell!((x, y-1), 2) + end end - g = CartesianMesh((nx, ny, nz), (dx, dy, dz), origin = [X0, Y0, 0.0]) - return UnstructuredMesh(g) end + cells_faces, cells_facepos = get_facepos(reinterpret(reshape, Int, int_neighbors), nc) + + for (bf, bc) in enumerate(bnd_cells) + push!(cell_to_boundary[bc], bf) + end + + boundary_cells_faces = Int[] + boundary_cells_facepos = Int[1] + for bfaces in cell_to_boundary + n = length(bfaces) + for bf in bfaces + push!(boundary_cells_faces, bf) + end + push!(boundary_cells_facepos, boundary_cells_facepos[end]+n) + end + + return UnstructuredMesh( + cells_faces, + cells_facepos, + boundary_cells_faces, + boundary_cells_facepos, + faces_nodes, + faces_nodespos, + boundary_faces_nodes, + boundary_faces_nodespos, + node_points, + int_neighbors, + bnd_cells; + structure = CartesianIndex(nx, ny), + cell_map = 1:nc, + kwarg... + ) +end + +function unstructured_from_cart(g, ::Val{3}; kwarg...) + d = dim(g) + @assert d == 3 + nx, ny, nz = grid_dims_ijk(g) X0, Y0, Z0 = g.origin nc = number_of_cells(g) @@ -322,7 +497,7 @@ function UnstructuredMesh(g::CartesianMesh; kwarg...) newpt = (i-1)*D return newpt::T end - function get_point(D::Vector{T}, i) where {T<:Real} + function get_point(D::Union{NTuple{N, T}, Vector{T}}, i) where {T<:Real, N} pt = zero(T) for j in 1:(i-1) pt += D[j] @@ -534,7 +709,7 @@ function UnstructuredMesh(G_raw::AbstractDict) return UnstructuredMesh(faces_raw, facePos_raw, nodes_raw, nodePos_raw, coord, N_raw) end -function mesh_linesegments(m; cells = 1:number_of_cells(m), outer = true) +function mesh_linesegments(m; cells = 1:number_of_cells(m), outer = dim(m) == 3) if !(m isa UnstructuredMesh) m = UnstructuredMesh(m) end @@ -549,7 +724,6 @@ function mesh_linesegments(m; cells = 1:number_of_cells(m), outer = true) a = outer && ((l_in && !r_in) || (r_in && !l_in)) b = !outer && (l_in || r_in) if a || b - @assert !b prev_node = missing f2n = m.faces.faces_to_nodes[face] for node in f2n diff --git a/src/meshes/unstructured/unstructured.jl b/src/meshes/unstructured/unstructured.jl index 3e1b93bc..91425649 100644 --- a/src/meshes/unstructured/unstructured.jl +++ b/src/meshes/unstructured/unstructured.jl @@ -20,35 +20,6 @@ function get_neighborship(G::UnstructuredMesh; internal = true) return N end -function face_normal(G::UnstructuredMesh, f, e = Faces()) - get_nodes(::Faces) = G.faces - get_nodes(::BoundaryFaces) = G.boundary_faces - nodes = get_nodes(e).faces_to_nodes[f] - pts = G.node_points - n = length(nodes) - # If the geometry is well defined it would be sufficient to take the first - # triplet and use that to generate the normals. We assume it isn't and - # create a weighted sum where each weight corresponds to the areal between - # the triplets. - normal = zero(eltype(pts)) - for i in 1:n - if i == 1 - a = pts[nodes[n]] - else - a = pts[nodes[i-1]] - end - b = pts[nodes[i]] - if i == n - c = pts[nodes[1]] - else - c = pts[nodes[i+1]] - end - normal += cross(c - b, a - b) - end - normal /= norm(normal, 2) - return normal -end - function grid_dims_ijk(g::UnstructuredMesh{D, CartesianIndex{D}}) where D dims = Tuple(g.structure) if D == 1 diff --git a/src/models.jl b/src/models.jl index cd325eb4..368efccd 100644 --- a/src/models.jl +++ b/src/models.jl @@ -71,7 +71,7 @@ export get_variable Get implementation of variable or parameter with name `name` for the model. """ -function get_variable(model::SimulationModel, name::Symbol) +function get_variable(model::SimulationModel, name::Symbol; throw = true) pvar = model.primary_variables svar = model.secondary_variables prm = model.parameters @@ -82,7 +82,11 @@ function get_variable(model::SimulationModel, name::Symbol) elseif haskey(prm, name) var = prm[name] else - error("Variable $name not found in primary/secondary variables or parameters.") + if throw + error("Variable $name not found in primary/secondary variables or parameters.") + else + var = nothing + end end return var end @@ -1127,3 +1131,6 @@ function setup_equations_views(storage, model, r) return convert_to_immutable_storage(out) end +function ensure_model_consistency!(model::JutulModel) + return model +end diff --git a/src/multimodel/composite.jl b/src/multimodel/composite.jl index c284e764..b7978215 100644 --- a/src/multimodel/composite.jl +++ b/src/multimodel/composite.jl @@ -8,11 +8,11 @@ function cross_term_entities_source(ct::CrossTerm, eq::Pair{Symbol, E}, model) w return e end -function declare_sparsity(target_model, source_model, eq::Pair{Symbol, E}, x::CrossTerm, x_storage, entity_indices, target_entity, source_entity, row_layout, col_layout) where E<:JutulEquation - k, eq = eq - target_model = composite_submodel(target_model, k) - if source_model.system isa CompositeSystem - source_model = composite_submodel(source_model, k) - end - return declare_sparsity(target_model, source_model, eq, x, x_storage, entity_indices, target_entity, source_entity, row_layout, col_layout) -end +# function declare_sparsity(target_model, source_model, eq::Pair{Symbol, E}, x::CrossTerm, x_storage, entity_indices, target_entity, source_entity, row_layout, col_layout) where E<:JutulEquation +# k, eq = eq +# target_model = composite_submodel(target_model, k) +# if source_model.system isa CompositeSystem +# source_model = composite_submodel(source_model, k) +# end +# return declare_sparsity(target_model, source_model, eq, x, x_storage, entity_indices, target_entity, source_entity, row_layout, col_layout) +# end diff --git a/src/multimodel/crossterm.jl b/src/multimodel/crossterm.jl index 9d0452e4..1e35acbf 100644 --- a/src/multimodel/crossterm.jl +++ b/src/multimodel/crossterm.jl @@ -1,6 +1,6 @@ local_discretization(::CrossTerm, i) = nothing -function declare_sparsity(target_model, source_model, eq::JutulEquation, x::CrossTerm, x_storage, entity_indices, target_entity, source_entity, row_layout, col_layout; equation_offset = 0, block_size = 1) +function declare_sparsity(target_model, source_model, eq, x::CrossTerm, x_storage, entity_indices, target_entity, source_entity, row_layout, col_layout; equation_offset = 0, block_size = 1) primitive = declare_pattern(target_model, x, x_storage, source_entity, entity_indices) if isnothing(primitive) out = nothing @@ -264,7 +264,7 @@ function update_offdiagonal_blocks!(storage, model, targets, sources; if !ismissing(lsys) models = model.models # for (ctp, ct_s) in zip(model.cross_terms, storage.cross_terms) - for i in eachindex(model.cross_terms, storage.cross_terms) + for i in eachindex(model.cross_terms) ctp = model.cross_terms[i] ct_s = storage.cross_terms[i] update_offdiagonal_block_pair!(lsys, ctp, ct_s, storage, model, models, targets, sources) @@ -339,7 +339,7 @@ function crossterm_subsystem(model, lsys, target, source; diag = false) # neqs = map(number_of_equations, model.models) # ndofs = map(number_of_degrees_of_freedom, model.models) - model_keys = submodel_symbols(model) + model_keys = submodels_symbols(model) groups = model.groups function get_group(s) @@ -497,10 +497,15 @@ function cross_term(storage, target::Symbol) end function cross_term_mapper(model, storage, f) - ind = map(f, model.cross_terms) + ind = findall(f, model.cross_terms) return (model.cross_terms[ind], storage[:cross_terms][ind]) end +# function cross_term_mapper(model, storage, f) +# ind = map(f, model.cross_terms) +# return (model.cross_terms[ind], storage[:cross_terms][ind]) +# end + has_symmetry(x) = !isnothing(symmetry(x)) has_symmetry(x::CrossTermPair) = has_symmetry(x.cross_term) @@ -535,7 +540,10 @@ end function extra_cross_term_sparsity(model, storage, target, include_symmetry = true) # Get sparsity of cross terms so that they can be included in any generic equations ct_pairs, ct_storage = cross_term_target(model, storage, target, include_symmetry) - sparsity = Dict{Union{Symbol, Pair}, Any}() + # TODO: Maybe this should just be Symbol? + # Old def was + # sparsity = Dict{Union{Symbol, Pair}, Any}() + sparsity = Dict{Symbol, Any}() for (ct_p, ct_s) in zip(ct_pairs, ct_storage) # Loop over all cross terms that impact target and grab the global sparsity # so that this can be added when doing sparsity detection for the model itself. @@ -550,6 +558,10 @@ function extra_cross_term_sparsity(model, storage, target, include_symmetry = tr eq = ct_p.source_equation @assert has_symmetry(ct_p.cross_term) end + if eq isa Pair + eq = last(eq) + end + eq::Symbol if !haskey(sparsity, eq) sparsity[eq] = Dict{Symbol, Any}() end diff --git a/src/multimodel/gradients.jl b/src/multimodel/gradients.jl index 156e6e52..ee8d5207 100644 --- a/src/multimodel/gradients.jl +++ b/src/multimodel/gradients.jl @@ -1,6 +1,6 @@ function swap_primary_with_parameters!(pmodel::MultiModel, model::MultiModel, targets = parameter_targets(model)) - for k in submodel_symbols(pmodel) + for k in submodels_symbols(pmodel) swap_primary_with_parameters!(pmodel.models[k], model.models[k], targets[k]) end return pmodel @@ -36,7 +36,7 @@ function convert_state_ad(model::MultiModel, state, tag = nothing) end function merge_state_with_parameters(model::MultiModel, state, parameters) - for k in submodel_symbols(model) + for k in submodels_symbols(model) merge_state_with_parameters(model[k], state[k], parameters[k]) end return state @@ -51,7 +51,7 @@ function state_gradient_outer!(∂F∂x, F, model::MultiModel, state, extra_arg; local_view(F::AbstractVector, offset, n) = view(F, (offset+1):(offset+n)) local_view(F::AbstractMatrix, offset, n) = view(F, :, (offset+1):(offset+n)) - for k in submodel_symbols(model) + for k in submodels_symbols(model) m = model[k] n = number_of_degrees_of_freedom(m) ∂F∂x_k = local_view(∂F∂x, offset, n) @@ -69,7 +69,7 @@ end function store_sensitivities(model::MultiModel, result, prm_map) out = Dict{Symbol, Any}() - for k in submodel_symbols(model) + for k in submodels_symbols(model) m = model[k] out[k] = Dict{Symbol, Any}() store_sensitivities!(out[k], m, result, prm_map[k]) @@ -89,7 +89,7 @@ end function parameter_targets(model::MultiModel) targets = Dict{Symbol, Any}() - for k in submodel_symbols(model) + for k in submodels_symbols(model) targets[k] = parameter_targets(model[k]) end return targets @@ -97,7 +97,7 @@ end function variable_mapper(model::MultiModel, arg...; targets = nothing, config = nothing, offset = 0) out = Dict{Symbol, Any}() - for k in submodel_symbols(model) + for k in submodels_symbols(model) if isnothing(targets) t = nothing else @@ -114,14 +114,14 @@ function variable_mapper(model::MultiModel, arg...; targets = nothing, config = end function rescale_sensitivities!(dG, model::MultiModel, parameter_map) - for k in submodel_symbols(model) + for k in submodels_symbols(model) rescale_sensitivities!(dG, model[k], parameter_map[k]) end end function optimization_config(model::MultiModel, param, active = nothing; kwarg...) out = Dict{Symbol, Any}() - for k in submodel_symbols(model) + for k in submodels_symbols(model) m = model[k] if isnothing(active) || !haskey(active, k) v = optimization_config(m, param[k]; kwarg...) @@ -135,21 +135,21 @@ end function optimization_targets(config::Dict, model::MultiModel) out = Dict{Symbol, Any}() - for k in submodel_symbols(model) + for k in submodels_symbols(model) out[k] = optimization_targets(config[k], model[k]) end return out end function optimization_limits!(lims, config, mapper, param, model::MultiModel) - for k in submodel_symbols(model) + for k in submodels_symbols(model) optimization_limits!(lims, config[k], mapper[k], param[k], model[k]) end return lims end function transfer_gradient!(dFdy, dFdx, x, mapper, config, model::MultiModel) - for k in submodel_symbols(model) + for k in submodels_symbols(model) transfer_gradient!(dFdy, dFdx, x, mapper[k], config[k], model[k]) end return dFdy @@ -157,7 +157,7 @@ end function swap_variables(state, parameters, model::MultiModel; kwarg...) out = Dict{Symbol, Any}() - for k in submodel_symbols(model) + for k in submodels_symbols(model) out[k] = swap_variables(state[k], parameters[k], model[k]; kwarg...) end return out @@ -172,7 +172,7 @@ end function determine_sparsity_simple(F, model::MultiModel, state, state0 = nothing) @assert isnothing(state0) "Not implemented." outer_sparsity = Dict() - for mod_k in submodel_symbols(model) + for mod_k in submodels_symbols(model) sparsity = Dict() substate = state[mod_k] entities = ad_entities(substate) @@ -202,7 +202,7 @@ end function solve_numerical_sensitivities(model::MultiModel, states, reports, G; kwarg...) out = Dict() - for mk in submodel_symbols(model) + for mk in submodels_symbols(model) inner = Dict() for k in keys(model[mk].parameters) inner[k] = solve_numerical_sensitivities(model, states, reports, G, (mk, k); kwarg...) diff --git a/src/multimodel/model.jl b/src/multimodel/model.jl index 28feb1f8..5261aa56 100644 --- a/src/multimodel/model.jl +++ b/src/multimodel/model.jl @@ -78,7 +78,9 @@ function replace_variables!(model::MultiModel; kwarg...) return model end -@inline submodel_symbols(model::MultiModel) = keys(model.models) +@inline function submodels_symbols(model::MultiModel) + return keys(model.models) +end function setup_state!(state, model::MultiModel, init_values) error("Mutating version of setup_state not supported for multimodel.") @@ -139,7 +141,7 @@ function setup_multimodel_maps!(storage, model) end function setup_equations_and_primary_variable_views!(storage, model::MultiModel, lsys) - mkeys = submodel_symbols(model) + mkeys = submodels_symbols(model) groups = model.groups no_groups = isnothing(groups) if no_groups @@ -192,7 +194,7 @@ end function specialize_simulator_storage(storage::JutulStorage, model::MultiModel, specialize) specialize_outer = multi_model_is_specialized(model) specialize = specialize || specialize_outer - sym = submodel_symbols(model) + sym = submodels_symbols(model) for (k, v) in data(storage) if k in sym storage[k] = specialize_simulator_storage(v, model[k], specialize) @@ -219,7 +221,7 @@ end function align_equations_to_linearized_system!(storage, model::MultiModel; equation_offset = 0, variable_offset = 0) models = model.models - model_keys = submodel_symbols(model) + model_keys = submodels_symbols(model) neqs = sub_number_of_equations(model) ndofs = sub_number_of_degrees_of_freedom(model) bz = sub_block_sizes(model) @@ -416,8 +418,8 @@ function add_sparse_local!(I, J, x, eq_label, s, target_model, source_model, ind end function get_sparse_arguments(storage, model::MultiModel, targets::Vector{Symbol}, sources::Vector{Symbol}, row_context, col_context) - I = [] - J = [] + I = Int[] + J = Int[] outstr = "Determining sparse pattern of $(length(targets))×$(length(sources)) models:\n" equation_offset = 0 variable_offset = 0 @@ -444,13 +446,18 @@ function get_sparse_arguments(storage, model::MultiModel, targets::Vector{Symbol bz_n = treat_block_size(bz_n, sarg.block_n) bz_m = treat_block_size(bz_m, sarg.block_m) if length(i) > 0 - push!(I, i .+ equation_offset) - push!(J, j .+ variable_offset) @assert maximum(i) <= n "I index exceeded $n for $source → $target (largest value: $(maximum(i))" @assert maximum(j) <= m "J index exceeded $m for $source → $target (largest value: $(maximum(j))" @assert minimum(i) >= 1 "I index was lower than 1 for $source → $target" @assert minimum(j) >= 1 "J index was lower than 1 for $source → $target" + + for ii in i + push!(I, ii + equation_offset) + end + for jj in j + push!(J, jj + variable_offset) + end end outstr *= "$source → $target: $n rows and $m columns starting at $(equation_offset+1), $(variable_offset+1).\n" variable_offset += m @@ -459,8 +466,6 @@ function get_sparse_arguments(storage, model::MultiModel, targets::Vector{Symbol equation_offset += n end @debug outstr - I = vec(vcat(I...)) - J = vec(vcat(J...)) bz_n = finalize_block_size(bz_n) bz_m = finalize_block_size(bz_m) return SparsePattern(I, J, equation_offset, variable_offset, matrix_layout(row_context), matrix_layout(col_context), bz_n, bz_m) @@ -470,7 +475,7 @@ function setup_linearized_system!(storage, model::MultiModel) models = model.models context = model.context - candidates = [i for i in submodel_symbols(model)] + candidates = [i for i in submodels_symbols(model)] if has_groups(model) ndof = values(map(number_of_degrees_of_freedom, models)) n = sum(ndof) @@ -558,7 +563,7 @@ function initialize_storage!(storage, model::MultiModel; kwarg...) end end -function update_equations!(storage, model::MultiModel, dt; targets = submodel_symbols(model)) +function update_equations!(storage, model::MultiModel, dt; targets = submodels_symbols(model)) @tic "model equations" for k in targets update_equations!(storage[k], model[k], dt) end @@ -577,7 +582,7 @@ function update_equations_and_apply_forces!(storage, model::MultiModel, dt, forc @tic "crossterm forces" apply_forces_to_cross_terms!(storage, model, dt, forces; time = time, kwarg...) end -function update_cross_terms!(storage, model::MultiModel, dt; targets = submodel_symbols(model), sources = submodel_symbols(model)) +function update_cross_terms!(storage, model::MultiModel, dt; targets = submodels_symbols(model), sources = submodels_symbols(model)) models = model.models for (ctp, ct_s) in zip(model.cross_terms, storage.cross_terms) target = ctp.target::Symbol @@ -681,8 +686,8 @@ end function update_linearized_system!(storage, model::MultiModel, executor = default_executor(); equation_offset = 0, - targets = submodel_symbols(model), - sources = submodel_symbols(model), + targets = submodels_symbols(model), + sources = submodels_symbols(model), kwarg...) @assert equation_offset == 0 "The multimodel version assumes offset == 0, was $offset" # Update diagonal blocks (model with respect to itself) @@ -695,7 +700,7 @@ function update_linearized_system!(storage, model::MultiModel, executor = defaul end function update_diagonal_blocks!(storage, model::MultiModel, targets; lsys = storage.LinearizedSystem, kwarg...) - model_keys = submodel_symbols(model) + model_keys = submodels_symbols(model) if has_groups(model) ng = number_of_groups(model) groups = model.groups @@ -729,7 +734,7 @@ end function setup_parameters(model::MultiModel, arg...; kwarg...) data_domains = Dict{Symbol, DataDomain}() - for k in submodel_symbols(model) + for k in submodels_symbols(model) data_domains[k] = model[k].data_domain end return setup_parameters(data_domains, model, arg...; kwarg...) @@ -871,7 +876,7 @@ function check_convergence(storage, model::MultiModel, cfg; end end -function update_primary_variables!(storage, model::MultiModel; targets = submodel_symbols(model), kwarg...) +function update_primary_variables!(storage, model::MultiModel; targets = submodels_symbols(model), kwarg...) models = model.models report = Dict{Symbol, AbstractDict}() for key in targets @@ -941,7 +946,7 @@ end function get_output_state(storage, model::MultiModel) out = JUTUL_OUTPUT_TYPE() models = model.models - for key in submodel_symbols(model) + for key in submodels_symbols(model) out[key] = get_output_state(storage[key], models[key]) end return out @@ -985,3 +990,10 @@ function sort_variables!(model::MultiModel, t = :primary) end return model end + +function ensure_model_consistency!(model::MultiModel) + for (k, m) in pairs(model.models) + ensure_model_consistency!(m) + end + return model +end diff --git a/src/multimodel/types.jl b/src/multimodel/types.jl index b80153d8..4decb4be 100644 --- a/src/multimodel/types.jl +++ b/src/multimodel/types.jl @@ -1,5 +1,5 @@ multi_model_is_specialized(m::MultiModel) = true -multi_model_is_specialized(m::MultiModel{nothing}) = false +multi_model_is_specialized(m::MultiModel{JutulStorage{Nothing}}) = false function submodel_ad_tag(m::MultiModel, tag) if m.specialize_ad @@ -10,8 +10,7 @@ function submodel_ad_tag(m::MultiModel, tag) return out end -submodels(m::MultiModel{nothing}) = m.models -submodels(m::MultiModel{T}) where T = m.models::T +submodels(m::MultiModel) = m.models Base.getindex(m::MultiModel, i::Symbol) = submodels(m)[i] diff --git a/src/multimodel/utils.jl b/src/multimodel/utils.jl index b5bc3e35..152006c5 100644 --- a/src/multimodel/utils.jl +++ b/src/multimodel/utils.jl @@ -51,10 +51,6 @@ function group_index(model, symbol) return index::Int end -function submodels_symbols(model::MultiModel) - return keys(model.models) -end - export setup_cross_term, add_cross_term! function setup_cross_term(cross_term::CrossTerm; target::Symbol, source::Symbol, equation) @assert target != source @@ -73,7 +69,7 @@ function group_linearized_system_offset(model::MultiModel, target, fn = number_o if isnothing(groups) groups = ones(Int, length(models)) end - skeys = submodel_symbols(model) + skeys = submodels_symbols(model) pos = findfirst(isequal(target), skeys) g = groups[pos] offset = 0 diff --git a/src/simulator/optimization.jl b/src/simulator/optimization.jl index 3f0e471b..5d37f45a 100644 --- a/src/simulator/optimization.jl +++ b/src/simulator/optimization.jl @@ -76,7 +76,7 @@ function setup_parameter_optimization(case::JutulCase, G, opt_cfg = optimization low = lims[1][k] high = lims[2][k] @assert low <= x0[k] "Computed lower limit $low for parameter #$k was larger than provided x0[k]=$(x0[k])" - @assert high >= x0[k] "Computer upper limit $hi for parameter #$k was smaller than provided x0[k]=$(x0[k])" + @assert high >= x0[k] "Computer upper limit $high for parameter #$k was smaller than provided x0[k]=$(x0[k])" end data = Dict() data[:n_objective] = 1 @@ -94,11 +94,13 @@ function setup_parameter_optimization(case::JutulCase, G, opt_cfg = optimization data[:sim_config] = config if grad_type == :adjoint - adj_storage = setup_adjoint_storage(model, state0 = state0, - parameters = parameters, - targets = targets, - use_sparsity = use_sparsity, - param_obj = param_obj) + adj_storage = setup_adjoint_storage(model, + state0 = state0, + parameters = parameters, + targets = targets, + use_sparsity = use_sparsity, + param_obj = param_obj + ) data[:adjoint_storage] = adj_storage grad_adj = zeros(adj_storage.n) else @@ -209,10 +211,11 @@ function objective_and_gradient_opt!(F, dFdx, x, data, arg...) return obj end -function optimization_config(model, param, active = keys(model.parameters); - rel_min = nothing, - rel_max = nothing, - use_scaling = false) +function optimization_config(model, param, active = parameter_targets(model); + rel_min = nothing, + rel_max = nothing, + use_scaling = false + ) out = Dict{Symbol, Any}() for k in active var = model.parameters[k] @@ -359,7 +362,9 @@ function optimization_limits!(lims, config, mapper, param, model) low_group = min(low_group, low) high_group = max(high_group, hi) end - high_group = max(high_group, low_group + 1e-8*(low_group + high_group) + 1e-18) + if high_group != Inf + high_group = max(high_group, low_group + 1e-8*(low_group + high_group) + 1e-18) + end @assert !isnan(low_group) @assert !isnan(high_group) cfg[:low] = low_group diff --git a/src/simulator/simulator.jl b/src/simulator/simulator.jl index 560399c4..31d7befa 100644 --- a/src/simulator/simulator.jl +++ b/src/simulator/simulator.jl @@ -182,6 +182,7 @@ function simulate!(sim::JutulSimulator, timesteps::AbstractVector; # Initialize loop p = start_simulation_message(info_level, timesteps, config) early_termination = false + stopnow = false if initialize && first_step <= no_steps check_forces(sim, forces, timesteps, per_step = forces_per_step) forces_step = forces_for_timestep(sim, forces, timesteps, first_step, per_step = forces_per_step) @@ -210,17 +211,45 @@ function simulate!(sim::JutulSimulator, timesteps::AbstractVector; subrep = JUTUL_OUTPUT_TYPE() subrep[:ministeps] = rep subrep[:total_time] = t_step + if step_done - @tic "output" store_output!(states, reports, step_no, sim, config, subrep, substates = substates) + + if begin + lastrep = rep[end] + if haskey(lastrep, :stopnow) && lastrep[:stopnow] + true + else + false + end end + + subrep[:output_time] = 0.0 + push!(reports, subrep) + stopnow = true + + else + + @tic "output" store_output!(states, reports, step_no, sim, config, subrep, substates = substates) + + end + else + subrep[:output_time] = 0.0 push!(reports, subrep) + end + t_elapsed += t_step + subrep[:output_time] + if early_termination n_solved = step_no-1 break end + + if stopnow + break + end + end states, reports = retrieve_output!(sim, states, reports, config, n_solved) final_simulation_message(sim, p, rec, t_elapsed, reports, timesteps, config, start_date, early_termination) @@ -273,6 +302,9 @@ function solve_timestep!(sim, dT, forces, max_its, config; # Onto the next one done = true break + elseif haskey(s, :stopnow) && s[:stopnow] + done = true + break else # Add to output of intermediate states. if !ismissing(substates) diff --git a/src/variable_evaluation.jl b/src/variable_evaluation.jl index b13ff8aa..f6df674a 100644 --- a/src/variable_evaluation.jl +++ b/src/variable_evaluation.jl @@ -175,6 +175,10 @@ end select_minimum_output_variables!(outputs, ::Any, model) = nothing +function ensure_model_consistency!(model) + return model +end + function sort_variables!(model, type = :primary) if type == :all sort_variables!(model, :primary) diff --git a/src/variables/utils.jl b/src/variables/utils.jl index 92510f93..53a11967 100644 --- a/src/variables/utils.jl +++ b/src/variables/utils.jl @@ -105,6 +105,8 @@ Lower (inclusive) limit for variable. """ minimum_value(::JutulVariables) = nothing +parameter_is_differentiable(::JutulVariables, model) = true + function update_primary_variable!(state, p::JutulVariables, state_symbol, model, dx, w) entity = associated_entity(p) active = active_entities(model.domain, entity, for_variables = true) @@ -308,7 +310,7 @@ function initialize_parameter_value!(parameters, data_domain, model, param, symb s = "computed defaulted" end if eltype(vals)<:AbstractFloat && any(x -> !isfinite(x), vals) - @error "Non-finite entries in $s parameter $symb" + @error "Non-finite entries in $s parameter $symb" vals end return initialize_variable_value!(parameters, model, param, symb, vals; kwarg...) end diff --git a/test/mesh.jl b/test/mesh.jl index 31598302..db940072 100644 --- a/test/mesh.jl +++ b/test/mesh.jl @@ -112,44 +112,81 @@ using MAT end @testset "cartesian to unstructured" begin - test_meshes = [ + meshes_1d = [ + CartesianMesh((3,)), + CartesianMesh((3,), ([1.0, 3.0, 4.0], )), + ] + meshes_2d = [ + CartesianMesh((3, 2)), + CartesianMesh((3, 2), ([1.0, 3.0, 4.0], [1.0, 2.0])), + ] + meshes_3d = [ + CartesianMesh((3, 2, 2), ((1.0, 3.0, 4.0), (1.0, 2.0), 1.0)), CartesianMesh((3, 2, 2)), CartesianMesh((4, 1, 1)), CartesianMesh((9, 7, 5), origin = [0.2, 0.6, 10.1]), CartesianMesh((3, 2, 2), (10.0, 3.0, 5.0)), CartesianMesh((3, 2, 2), ([10.0, 5.0, π], 3.0, 5.0)), CartesianMesh((100, 3, 7)) - ] - for g in test_meshes - G = UnstructuredMesh(g) - geo1 = tpfv_geometry(g) - geo2 = tpfv_geometry(G) + ] + for mdim in 1:3 + @testset "$(mdim)D conversion" begin + if mdim == 1 + test_meshes = meshes_1d + subs = 1:1 + else + if mdim == 2 + test_meshes = meshes_2d + else + test_meshes = meshes_3d + end + subs = 1:mdim + end + for g in test_meshes + G = UnstructuredMesh(g) + geo1 = tpfv_geometry(g) + geo2 = tpfv_geometry(G) - @testset "cells" begin - @test geo1.volumes ≈ geo2.volumes - @test geo1.cell_centroids ≈ geo2.cell_centroids - end - @testset "faces" begin - @test geo1.neighbors == geo2.neighbors - @test geo1.normals == geo2.normals - @test geo1.areas ≈ geo2.areas - @test geo1.face_centroids ≈ geo2.face_centroids - end - @testset "boundary" begin - @test geo1.boundary_normals == geo2.boundary_normals - @test geo1.boundary_neighbors == geo2.boundary_neighbors - @test geo1.boundary_areas ≈ geo2.boundary_areas - @test geo1.boundary_centroids ≈ geo2.boundary_centroids - end - @testset "half-faces" begin - @test geo1.half_face_faces == geo2.half_face_faces - @test geo1.half_face_cells == geo2.half_face_cells + @testset "cells" begin + @test geo1.volumes ≈ geo2.volumes + @test geo1.cell_centroids ≈ geo2.cell_centroids[1:mdim, :] + end + @testset "faces" begin + @test geo1.neighbors == geo2.neighbors + @test geo1.normals == geo2.normals[1:mdim, :] + @test geo1.areas ≈ geo2.areas + @test geo1.face_centroids ≈ geo2.face_centroids[1:mdim, :] + end + if mdim > 1 + @testset "boundary" begin + # Note: 1D grids are currently converted to 2D, so + # the boundary will not be the same after + # conversion. + @test geo1.boundary_normals == geo2.boundary_normals[1:mdim, :] + @test geo1.boundary_neighbors == geo2.boundary_neighbors + @test geo1.boundary_areas ≈ geo2.boundary_areas + @test geo1.boundary_centroids ≈ geo2.boundary_centroids[1:mdim, :] + end + end + @testset "half-faces" begin + @test geo1.half_face_faces == geo2.half_face_faces + @test geo1.half_face_cells == geo2.half_face_cells + end + @testset "triangulate_mesh" begin + try + triangulate_mesh(G) + catch + @test false + finally + @test true + end + end + end end end - # 2D support missing - @test_warn "Conversion from CartesianMesh to UnstructuredMesh is only fully supported for 3D grids. Converting 2D grid to 3D." UnstructuredMesh(CartesianMesh((3, 2))) # 1D support missing - @test_warn "Conversion from CartesianMesh to UnstructuredMesh is only fully supported for 3D grids. Converting 1D grid to 3D." UnstructuredMesh(CartesianMesh((3,))) + @test_warn "Conversion from CartesianMesh to UnstructuredMesh is only fully supported for 2D/3D grids. Converting 1D grid to 2D." UnstructuredMesh(CartesianMesh((3,))) + @test_nowarn UnstructuredMesh(CartesianMesh((3,)), warn_1d = false) end @testset "extract_submesh + cart convert" begin g = CartesianMesh((2, 2, 2)) @@ -181,3 +218,24 @@ end @test getfield(geo_c2, f) ≈ getfield(geo, f) end end + +@testset "Trajectories" begin + G = CartesianMesh((4, 4, 5), (100.0, 100.0, 100.0)) + trajectory = [ + 50.0 25.0 1; + 55 35.0 25; + 65.0 40.0 50.0; + 70.0 70.0 90.0 + ] + cells_3d = Jutul.find_enclosing_cells(G, trajectory) + @test cells_3d == [7, 23, 39, 55, 59, 75] + + G = CartesianMesh((5, 5), (1.0, 2.0)) + trajectory = [ + 0.1 0.1; + 0.2 0.4; + 0.3 1.2 + ] + cells_2d = Jutul.find_enclosing_cells(G, trajectory) + @test cells_2d == [1, 7, 12] +end diff --git a/test/nfvm.jl b/test/nfvm.jl new file mode 100644 index 00000000..5c19e11a --- /dev/null +++ b/test/nfvm.jl @@ -0,0 +1,170 @@ +using Test +using Jutul +using StaticArrays +import Jutul.NFVM as NFVM + +@testset "triplets" begin + u_val = π + factor = 9.13172 + offsets = [0.0, 100.0, 1e6] + n = 10 + + # TODO: Test offsets. + @testset "2D" begin + T = SVector{2, Float64} + x_t = T(0.0, 0.0) + l = T(u_val, 0.0) + all_x = [ + factor.*T(-1.0, 0.0), + factor.*T(0.0, -1.0), + factor.*T(1.0, 0.0), + factor.*T(0.0, 1.0), + ] + pair, w = NFVM.find_minimizing_basis(x_t, l, all_x) + l_r = NFVM.reconstruct_l(pair, w, x_t, all_x) + # Should be the same. + @test l_r ≈ l + + ix = findfirst(isequal(3), pair) + @test !isnothing(ix) + @test w[ix]/norm(w, 2) ≈ 1.0 + for offset in offsets + for xi in range(0.0, 1.0, n) + for yi in range(0.0, 1.0, n) + if xi == yi == 0.0 + continue + end + l = u_val.*T(xi, yi) + x_t_i = x_t .+ offset + x = copy(all_x) + for i in eachindex(x) + x[i] = x[i] .+ offset + end + pair, w = NFVM.find_minimizing_basis(x_t_i, l, x) + l_r = NFVM.reconstruct_l(pair, w, x_t_i, x) + @test l_r ≈ l + end + end + end + end + @testset "3D" begin + T = SVector{3, Float64} + x_t = T(0.0, 0.0, 0.0) + l = T(u_val, 0.0, 0.0) + all_x = [ + factor.*T(-1.0, 0.0, 0.0), + factor.*T(0.0, -1.0, 0.0), + factor.*T(0.0, 0.0, -1.0), + factor.*T(1.0, 0.0, 0.0), + factor.*T(0.0, 1.0, 0.0), + factor.*T(0.0, 0.0, 1.0) + ] + triplet, w = NFVM.find_minimizing_basis(x_t, l, all_x) + + l_r = NFVM.reconstruct_l(triplet, w, x_t, all_x) + @test l_r ≈ l + + ix = findfirst(isequal(4), triplet) + @test !isnothing(ix) + @test w[ix]/norm(w, 2) ≈ 1.0 + for offset in offsets + for xi in range(0.0, 1.0, n) + for yi in range(0.0, 1.0, n) + for zi in range(0.0, 1.0, n) + if xi == yi == zi == 0.0 + continue + end + l = u_val.*T(xi, yi, zi) + x_t_i = x_t .+ offset + x = copy(all_x) + for i in eachindex(x) + x[i] = x[i] .+ offset + end + trip, w = NFVM.find_minimizing_basis(x_t_i, l, x) + l_r = NFVM.reconstruct_l(trip, w, x_t_i, x) + @test l_r ≈ l + end + end + end + end + @testset "special cases" begin + Tt = SVector{2, Float64} + x_t = Tt(0.16666666666666666, 0.8333333333333334) + points = [ + Tt(0.3333333333333333, 0.8333333333333335), + Tt(0.16666666666666663, 1.666666666666667), + Tt(0.0, 0.8333333333333334), + Tt(0.16666666666666666, 0.0) + ] + l = Tt(0.0, 0.0010471975511965976) + triplet, w = NFVM.find_minimizing_basis(x_t, l, points, verbose = false) + l_r = NFVM.reconstruct_l(triplet, w, x_t, points) + @test l_r ≈ l + + Tv = SVector{2, Float64} + x_t = Tv(0.16875801386133837, 0.8567381844756641) + AKn = Tv(0.005227888394818919, -6.757589394032612e-6) + points = [ + Tv(0.33463202673281195, 0.8540976239029396), + Tv(0.16912337164668087, 1.689472036394667), + Tv(0.003131953514408839, 0.855309095397525), + Tv(0.16852587628122123, 0.021593752012451912) + ] + ijk, w = NFVM.find_minimizing_basis(x_t, AKn, points, verbose = true) + @test NFVM.reconstruct_l(ijk, w, x_t, points) ≈ AKn + @test ijk == (1, 2) + @test w ≈ [0.03151702159776656, 9.182407443929852e-5] + end + @testset "2D and 3D comparison" begin + Tv = SVector{3, Float64} + x_t = + Tv( + 0.0054697673655655955, + 0.0054697673655655955, + 0.5 + ) + AKn = + Tv( + 1.079510556183061e-15, + 4.127374112001815e-19, + 0.0 + ) + points = + Tv[ + Tv(0.010940232272671423, 0.005467680910047275, 0.4999999999999999), + Tv(0.005467680910047275, 0.010940232272671423, 0.5000000000000001), + Tv(0.0, 0.005471161386171925, 0.5), + Tv(0.005471161386171925, 0.0, 0.5), + Tv(0.0054697673655655955, 0.0054697673655655955, 0.0), + Tv(0.0054697673655655955, 0.0054697673655655955, 1.0), + ] + trip_3d, trip_w_3d = Jutul.NFVM.find_minimizing_basis(x_t, AKn, points, verbose = true) + l_r = Jutul.NFVM.reconstruct_l(trip_3d, trip_w_3d, x_t, points) + @test l_r ≈ AKn + Tv = SVector{2, Float64} + x_t = + Tv( + 0.0054697673655655955, + 0.0054697673655655955 + ) + AKn = + Tv( + 1.079510556183061e-15, + 4.127374112001815e-19 + ) + points = + Tv[ + Tv(0.010940232272671423, 0.005467680910047275), + Tv(0.005467680910047275, 0.010940232272671423), + Tv(0.0, 0.005471161386171925), + Tv(0.005471161386171925, 0.0), + ] + trip_2d, trip_w_2d = Jutul.NFVM.find_minimizing_basis(x_t, AKn, points, verbose = true) + l_r = Jutul.NFVM.reconstruct_l(trip_2d, trip_w_2d, x_t, points) + @test l_r ≈ AKn + # Test that 2D and trivially 3D gives the same + @test trip_2d == trip_3d[1:2] + @test trip_w_2d ≈ trip_w_3d[1:2] + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index b2fa3dac..76a0a4db 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,3 +7,4 @@ include("utils.jl") include("partitioning.jl") include("units.jl") include("sparsity.jl") +include("nfvm.jl")