From bdff926116a548ed1a5003c8fd25bf645b752564 Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 23 Mar 2026 21:42:05 +0000 Subject: [PATCH 01/36] Added dimensionless normalization to reduce cond(A). Added GR-decomposition to prevent pile-up of y functions. Updated fluid1d, spatial heating solution is now directly obtained from prescribed profile. --- src/Obliqua.jl | 281 ++++++++++++++++++++++++++++++++++++------------- src/solid1d.jl | 163 ++++++++++++++++++++++------ 2 files changed, 335 insertions(+), 109 deletions(-) diff --git a/src/Obliqua.jl b/src/Obliqua.jl index 98f3271..d4b3b36 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -307,6 +307,8 @@ module Obliqua if spectrum == "adaptive" s_min = cfg["orbit"]["obliqua"]["s_min"] s_max = cfg["orbit"]["obliqua"]["s_max"] + s_min = nothing_if_none(s_min) + s_max = nothing_if_none(s_max) elseif spectrum == "full" N_σ = cfg["orbit"]["obliqua"]["N_sigma"] p_min = cfg["orbit"]["obliqua"]["p_min"] @@ -342,8 +344,7 @@ module Obliqua module_fluid = nothing_if_none(module_fluid) module_mushy = nothing_if_none(module_mushy) - s_min = nothing_if_none(s_min) - s_max = nothing_if_none(s_max) + # shear .= ifelse.((shear .< 1e10), 3.2e9, shear) # convert interior profiles to BigFloat ρ = convert(Vector{prec}, rho) @@ -415,7 +416,7 @@ module Obliqua t_range = 10 .^ range(p_min, stop=p_max, length=N_σ) # periods [1e3 yr] σ_range = 2π ./ (t_range .* 1e3 .* 365.25 .* 24 .* 3600) # freq [s-1] σ_range = reshape(σ_range, :) - + print("Using full spectrum with $N_σ frequencies from $(σ_range[1]) /s to $(σ_range[end]) /s.") end # get forcing frequency dependent complex shear modulus @@ -542,10 +543,20 @@ module Obliqua elseif module_fluid=="fluid1d" prf_seg[iss,:], kT, kL = run_fluid1d( σ, ρ_seg, r_seg, - g_seg, ρ_mean_lower, + g_seg, ρ_ratio, S_mass, sma, R; n=n, - σ_R=sigma_R, - σ_inf=sigma_R_inf, + sigma_R=sigma_R, + sigma_inf=sigma_R_inf, + sigma_R_prf=sigma_R_prf, + H_R=H_R, efficiency=efficiency_seg + ) + elseif module_fluid=="fluid1d_RD" + prf_seg[iss,:], kT, kL = run_fluid1d_RD( + σ, ρ_seg, r_seg, + g_seg, ρ_ratio, + S_mass, sma, R; n=n, + sigma_R=sigma_R, + sigma_inf=sigma_R_inf, sigma_R_prf=sigma_R_prf, H_R=H_R, efficiency=efficiency_seg ) @@ -558,7 +569,16 @@ module Obliqua # don't model mush tides if module_mushy===nothing kT, kL = 0., 0. - # elseif heating profile from neighbouring segments + elseif module_solid=="solid1d" + # calculate tides in solid region + prf_seg[iss,:], kT, kL = run_solid1d( + σ, ρ_seg, + r_seg, η_seg, + μc_seg[:, iss], + κ_seg, R; + ncalc=ncalc, n=n, m=m + ) + # elseif heating profile from neighbouring segments elseif module_mushy=="interp" # turn on interpolation mode interp_active = true @@ -705,13 +725,13 @@ module Obliqua # Hansen coefficient at s=1 _, X = Hansen.get_hansen(ecc, n, m, 1, 1) - A = 2 * sqrt(4π * factorial(n-m) / ((2*n+1) * factorial(n+m))) * Plm.(n, m, 0.) * X + A = 2 * sqrt(4π * factorial(n-m) / ((2*n+1) * factorial(n+m))) * Plm(n, m, 0.) * X U = (G*S_mass/sma) * (R/sma)^n * A prefactor = (2*n + 1) * R / (8π*G) .* σ_range - U2 = abs2(U) + U2 = abs2.(U) # return power profile at each frequency P_T_1_prf = zeros(prec, N_σ, length(shear)) @@ -723,7 +743,7 @@ module Obliqua # return bulk heating at each frequency P_T_1_blk = prefactor .* imag_k2 .* U2 - @info "Mapping 1 --> $(σ_range[1]) /s, and 50 --> $(σ_range[end]) /s." + @info "Mapping 1 --> $(σ_range[1]) /s, and $N_σ --> $(σ_range[end]) /s." # plot heating profile from full spectrum at s=1 plt = plotting.plot_segment_heating( @@ -1129,11 +1149,12 @@ module Obliqua solid1d.define_spherical_grid(res, n, m) # get y-functions - M, y1_4 = solid1d.compute_M(rr, ρ, g, μc, κ, n; core="liquid") + M, y1_4, matrices_R = solid1d.compute_M(omega, rr, ρ, g, μc, κ, n; core="liquid") + # M, y1_4 = solid1d.compute_M_fluid(omega, rr, ρ, g, μc, κ, n; core="liquid") # Tidal - tidal_solution_T = solid1d.compute_y(rr, g, M, R, y1_4, n; load=false) + tidal_solution_T = solid1d.compute_y(rr, g, M, R, y1_4, matrices_R, n; load=false) # Load - tidal_solution_L = solid1d.compute_y(rr, g, M, R, y1_4, n; load=true) + tidal_solution_L = solid1d.compute_y(rr, g, M, R, y1_4, matrices_R, n; load=true) # get k2 tidal Love Number (complex-valued) k2_T = tidal_solution_T[5, end, end] - 1 @@ -1500,6 +1521,106 @@ module Obliqua end + """ + run_fluid1d(omega, rho, radius, gravity, ρ_ratio, S_mass, sma, R; kwargs...) + + Compute tidal heating profile and Love numbers for a 1D fluid model. + + # Arguments + - `omega::Float64` : Forcing frequency + - `rho::Vector{prec}` : Density profile + - `radius::Vector{prec}` : Radial grid (core → surface) + - `gravity::Vector{prec}` : Gravity profile + - `ρ_ratio::prec` : Density ratio of lower layer + - `S_mass::prec` : Stellar mass + - `sma::prec` : Semi-major axis + - `R::prec` : Planet radius + + # Keyword Arguments + - `n::Int=2` : Radial power (dominant term n=2) + - `sigma_R::Float64=1e-3` : Rayleigh drag at interface + - `sigma_inf::Float64=1e-7` : Drag in fluid interior + - `sigma_R_prf::String="uniform"` : Drag profile type + - `H_R::Float64=1e3` : Drag scale height + - `efficiency::Float64=0.3` : Interface efficiency factor + + # Returns + - `power_prf::Vector{prec}` : Heating profile + - `k2_T::precc` : Tidal Love number + - `k2_L::precc` : Load Love number + """ + function run_fluid1d( omega::Float64, + rho::Vector{prec}, + radius::Vector{prec}, + gravity::Vector{prec}, + ρ_ratio::prec, + S_mass::prec, + sma::prec, + R::prec; + n::Int = 2, + sigma_R::Float64 = 1e-3, + sigma_inf::Float64 = 1e-7, + sigma_R_prf::String = "uniform", + H_R::Float64 = 1e3, + efficiency::Float64 = 0.3 + )::Tuple{Vector{prec}, precc, precc} + + # internal structure arrays + r = convert(Vector{prec}, radius) + + # volumes + Vs = (4/3) * π * (r[end]^3 - r[1]^3) + dVs = (4/3) * π .* (r[2:end].^3 .- r[1:end-1].^3) + + # Love numbers (interface vs fluid interior) + k2_T, k2_L = run_fluid0d(omega, rho, r, ρ_ratio; n=n, sigma_R=efficiency * sigma_R) + k2_T_inf, _ = run_fluid0d(omega, rho, r, ρ_ratio; n=n, sigma_R=sigma_inf) + + # total heating (bulk) + prefactor = (2n + 1) * R * omega / (8π * G) + + power_blk = prefactor * -imag(k2_T) / Vs + power_blk_inf = prefactor * -imag(k2_T_inf) / Vs + + Δpower = max(power_blk - power_blk_inf, 0) + + # radial positions + r_mid = 0.5 .* (r[1:end-1] .+ r[2:end]) + z = abs.(r_mid .- r[1]) # distance from interface + + # profile shape function + shape = ones(length(z)) + + if sigma_R_prf == "exp" + shape .= exp.(-z ./ H_R) + + elseif sigma_R_prf == "linear" + shape .= max.(0, 1 .- z ./ H_R) + + elseif sigma_R_prf == "quadratic" + shape .= max.(0, 1 .- z ./ H_R).^2 + + elseif sigma_R_prf == "dynamic" + l_mix = max.(min.(z, H_R), 1e-12) + shape .= exp.(-z ./ l_mix) + + elseif sigma_R_prf != "uniform" + error("Unknown sigma_R_prf: $sigma_R_prf") + end + + # heating profile + power_prf = power_blk_inf .+ Δpower .* shape + + # normalize to match bulk heating + unorm = sum(power_prf .* dVs) / Vs + if unorm > 0 + power_prf .*= power_blk / unorm + end + + return power_prf, k2_T, k2_L + end + + """ run_fluid1d(omega, rho, radius, gravity, ρ_mean_lower, S_mass, sma; n=2, sigma_R=1e-3, sigma_R_prf="uniform", H_R=1e3, efficiency=0.3) @@ -1528,90 +1649,98 @@ module Obliqua - `k2_T::precc` : Complex Tidal k2 Lovenumber. - `k2_L::precc` : Complex Load k2 Lovenumber. """ - function run_fluid1d( omega::Float64, - rho::Array{prec,1}, - radius::Array{prec,1}, - gravity::Array{prec,1}, - ρ_mean_lower::prec, - S_mass::prec, - sma::prec, - R::prec; - n::Int64=2, - σ_R::Float64=1e-3, - σ_inf::Float64=1e-7, - sigma_R_prf::String="uniform", - H_R::Float64=1e3, - efficiency::Float64=0.3 - )::Tuple{Array{prec,1},precc,precc} - - # internal structure arrays - ρ = convert(Vector{prec}, rho) - r = convert(Vector{prec}, radius) + function run_fluid1d_RD( omega::Float64, + rho::Vector{prec}, + radius::Vector{prec}, + gravity::Vector{prec}, + ρ_mean_lower::prec, + S_mass::prec, + sma::prec, + R::prec; + n::Int = 2, + sigma_R::Float64 = 1e-3, + sigma_inf::Float64 = 1e-7, + sigma_R_prf::String = "uniform", + H_R::Float64 = 1e3, + efficiency::Float64 = 0.3 + )::Tuple{Vector{prec}, precc, precc} + + # internal structure arrays + ρ = convert(Vector{prec}, rho) + r = convert(Vector{prec}, radius) g = convert(Vector{prec}, gravity) - - # get shell volumes - Vs = 4/3 * π * (r[2:end].^3 .- r[1:end-1].^3) - # calculate density contrast - ρs = vcat(ρ_mean_lower, ρ) - ρ_ratios = ρs[2:end]./ρs[1:end-1] + ns = length(r) - 1 # number of shells - # fluid magma ocean layer heights + # volumes + dVs = (4/3) * π .* (r[2:end].^3 .- r[1:end-1].^3) # shell volumes + + # magma ocean height H_magma = diff(r) - - # define Rayleigh-drag profile - σ_R_prf = zeros(Float64, length(r)-1) - # closest solid-like boundary, where Rayleigh-drag is maximal - r_int = r[1] + # density contrast + ρs = vcat(ρ_mean_lower, ρ) + ρ_ratios = ρs[2:end] ./ ρs[1:end-1] + + # radial coordinates r_mid = 0.5 .* (r[1:end-1] .+ r[2:end]) - z = abs.(r_mid .- r_int) # distance from interface + z = abs.(r_mid .- r[1]) # distance from interface - # Rayleigh-drag profiles - if sigma_R_prf == "uniform" - σ_R_prf .= σ_inf .+ max((efficiency * σ_R .- σ_inf), 0) + # Rayleigh drag contrast + Δσ = max(efficiency * sigma_R - sigma_inf, 0.0) - elseif sigma_R_prf == "exp" - σ_R_prf .= σ_inf .+ max((efficiency * σ_R .- σ_inf), 0) .* exp.(-z ./ H_R) + # shape function + shape = ones(ns) + + if sigma_R_prf == "exp" + shape .= exp.(-z ./ H_R) elseif sigma_R_prf == "linear" - σ_R_prf .= σ_inf .+ max((efficiency * σ_R .- σ_inf), 0) .* max.(0.0, 1 .- z ./ H_R) + shape .= max.(0.0, 1 .- z ./ H_R) elseif sigma_R_prf == "quadratic" - σ_R_prf .= σ_inf .+ max((efficiency * σ_R .- σ_inf), 0) .* max.(0.0, 1 .- z ./ H_R).^2 + shape .= max.(0.0, 1 .- z ./ H_R).^2 elseif sigma_R_prf == "dynamic" - # dynamic mixing length - l_mix = min.(z, H_R) - - # avoid division by zero at interface - l_mix .= max.(l_mix, 1e-12) + l_mix = max.(min.(z, H_R), 1e-12) + shape .= exp.(-z ./ l_mix) - # Rayleigh-drag profile - σ_R_prf .= σ_inf .+ max((efficiency * σ_R .- σ_inf), 0) .* exp.(-z ./ l_mix) + elseif sigma_R_prf != "uniform" + error("Unknown sigma_R_prf: $sigma_R_prf") end - # obtain heating profile and Imk2 Love and load numbers - power_prf = zeros(prec, length(r)-1) - k2_T = zeros(precc, length(r)-1) - k2_L = zeros(precc, length(r)-1) - - # calculate prefactor and total availible heat - prefactor = (2*n+1) * R * omega / (8π*G) - - for (is, s) in pairs(r[1:end-1]) - # get k2 Lovenumbers - k2_T[is], k2_L[is] = fluid0d.compute_fluid_lovenumbers( - omega, r[is+1], H_magma[is], - g[is], ρ_ratios[is], n, σ_R_prf[is] + # Rayleigh drag profile + sigma_profile = sigma_inf .+ Δσ .* shape + + # outputs + power_prf = zeros(prec, ns) + k2_T = zeros(precc, ns) + k2_L = zeros(precc, ns) + + # prefactor + prefactor = (2n + 1) * R * omega / (8π * G) + + # loop over shells + for i in 1:ns + k2_T[i], k2_L[i] = fluid0d.compute_fluid_lovenumbers( + omega, + r[i+1], + H_magma[i], + g[i], + ρ_ratios[i], + n, + sigma_profile[i] ) - - # calculate total heat input at forcing frequency - power_prf[is] = prefactor * -imag(k2_T[is]) / Vs[is] + + power_prf[i] = prefactor * -imag(k2_T[i]) / dVs[i] end - return power_prf, sum(k2_T), sum(k2_L) + # effective Love numbers (volume-weighted) + Vtot = sum(dVs) + k2_T_eff = sum(k2_T .* dVs) / Vtot + k2_L_eff = sum(k2_L .* dVs) / Vtot + return power_prf, k2_T_eff, k2_L_eff end diff --git a/src/solid1d.jl b/src/solid1d.jl index 3cb9b69..2ca5ef1 100644 --- a/src/solid1d.jl +++ b/src/solid1d.jl @@ -1,13 +1,12 @@ -module solid1d +module solid1d#_scaled using LinearAlgebra using DoubleFloats using AssociatedLegendrePolynomials using StaticArrays - prec = BigFloat precc = Complex{BigFloat} @@ -205,6 +204,27 @@ module solid1d end + function get_scales(r, ρ, R0, M0, s0) + + ρ0 = M0 / R0^3 # 1000 kg/m^3 + μ0 = M0 / (R0 * s0^2) # 1e9 Pa (1 GPa) + g0 = R0 / s0^2 # 0.1 m/s^2 (Note: check if you want 10 or 0.1) + G0 = R0^3 / (M0 * s0^2) # Gravity constant scaling + + S = Diagonal(precc[ + R0, # y1: radial displacement (m) + R0, # y2: tangential displacement (m) + μ0, # y3: radial stress (Pa) + μ0, # y4: tangential stress (Pa) + g0*R0, # y5: potential (m^2/s^2) + g0 # y6: potential gradient/gravity (m/s^2) + ]) + + Sinv = inv(S) + return R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv + end + + """ get_Ic(r, ρ, g, μ, type, n; M=6, N=3) @@ -281,9 +301,9 @@ module solid1d # Notes See also [`get_A!`](@ref) """ - function get_A(r, ρ, g, μ, K, n) + function get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing) A = zeros(precc, 6, 6) - get_A!(A, r, ρ, g, μ, K, n) + get_A!(A, ω, r, ρ, g, μ, K, n; G0=G0, λ=λ) return A end @@ -310,27 +330,39 @@ module solid1d # Notes See also [`get_A`](@ref) """ - function get_A!(A::Matrix, r, ρ, g, μ, K, n; λ=nothing) + function get_A!(A::Matrix, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing) if isnothing(λ) λ = K - 2μ/3 end + # if abs(μ) < 1e-9 + # # Scale it down to the max allowed magnitude, preserving phase + # μ = μ / abs(μ) * 1e-9 + # end + + # if abs(K) < 1e-5 + # # Scale it down to the max allowed magnitude, preserving phase + # K = K / abs(K) * 1e-5 + # end + + G_norm = G / G0 + r_inv = 1.0/r β_inv = 1.0/(2μ + λ) rβ_inv = r_inv * β_inv A[1,1] = -2λ * r_inv*β_inv A[2,1] = -r_inv - A[3,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) #- ω^2 * ρ# + A[3,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) - ω^2 * ρ A[4,1] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[5,1] = 4π * G * ρ - A[6,1] = 4π*(n+1)*G*ρ*r_inv + A[5,1] = 4π * G_norm * ρ + A[6,1] = 4π*(n+1)*G_norm*ρ*r_inv A[1,2] = n*(n+1) * λ * r_inv*β_inv A[2,2] = r_inv A[3,2] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[4,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) #- ω^2 * ρ# - A[6,2] = -4π*n*(n+1)*G*ρ*r_inv + A[4,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) - ω^2 * ρ + A[6,2] = -4π*n*(n+1)*G_norm*ρ*r_inv A[1,3] = β_inv A[3,3] = r_inv*β_inv * (-4μ ) @@ -371,21 +403,22 @@ module solid1d # Notes See 'get_B!' for definition. """ - function get_B(r1, r2, g1, g2, ρ, μ, K, n) + function get_B(ω, r1, r2, g1, g2, ρ, μ, K, n; G0=1) B = zeros(precc, 6, 6) - get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n; G0=G0) return B end """ - get_B!(B, r1, r2, g1, g2, ρ, μ, K) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K) Compute the 6x6 numerical integrator matrix, which integrates dy/dr from `r1` to `r2` for the solid-body problem. `B` here represnts the RK4 integrator, given by Eq. S5.5 in Hay et al., (2025). # Arguments - `B::Array{precc,2}` : 6x6 numerical integrator matrix for integrating dy/dr from r1 to r2 for the solid-body problem. + - `ω::prec` : Tidal frequency. - `r1::prec` : Starting radius for integration. - `r2::prec` : Ending radius for integration. - `g1::prec` : Gravity at radius r1. @@ -398,15 +431,15 @@ module solid1d # Notes See also [`get_B`](@ref) """ - function get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) + function get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n; G0=1) dr = r2 - r1 rhalf = r1 + 0.5dr ghalf = g1 + 0.5*(g2 - g1) - A1 = get_A(r1, ρ, g1, μ, K, n) - Ahalf = get_A(rhalf, ρ, ghalf, μ, K, n) - A2 = get_A(r2, ρ, g2, μ, K, n) + A1 = get_A(ω, r1, ρ, g1, μ, K, n; G0=G0) + Ahalf = get_A(ω, rhalf, ρ, ghalf, μ, K, n; G0=G0) + A2 = get_A(ω, r2, ρ, g2, μ, K, n; G0=G0) k16 = zeros(precc, 6, 6) k26 = zeros(precc, 6, 6) @@ -441,7 +474,7 @@ module solid1d - `K::Array{prec,1}` : 1D array of layer bulk moduli. - `n::Int` : Tidal degree. """ - function get_B_product!(Bprod2, r, ρ, g, μ, K, n) + function get_B_product!(Bprod2, ω, r, ρ, g, μ, K, n; G0=1) Bstart = Matrix{precc}(I, 6, 6) B = zeros(precc, 6, 6) @@ -453,7 +486,7 @@ module solid1d g1 = g[j] g2 = g[j+1] - get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n; G0=G0) Bprod2[:,:,j] .= B * (j==1 ? Bstart : Bprod2[:,:,j-1]) r1 = r2 @@ -481,25 +514,67 @@ module solid1d - `M::Array{precc,2}` : 3x3 M matrix, which is used to propagate the solution across the entire interior. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. """ - function compute_M(r, ρ, g, μ, K, n; core="liquid") + function compute_M(ω, r, ρ, g, μ, K, n; core="liquid") r, ρ, g, μ, K = convert_params_to_prec(r, ρ, g, μ, K) nlayers = size(r)[2] nsublayers = size(r)[1] + R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(r, ρ, 4.6e6, 1.6e22, 3.6e5) + + # scale inputs + ω_scaled = ω * s0 + r_scaled = r ./ R0 + ρ_scaled = ρ ./ ρ0 + g_scaled = g ./ g0 + μ_scaled = μ ./ μ0 + K_scaled = K ./ μ0 + y_start = get_Ic(r[end,1], ρ[1], g[end,1], μ[1], core, n; M=6, N=3) + y_start .= Sinv * y_start # Scale starting vector + + # To store QR factors for back-propagation + # matrices_Q stores the orthonormal basis at each layer interface + # matrices_R stores the mixing coefficients + matrices_R_sublayer = Array{Matrix{precc}}(undef, nlayers, nsublayers-1) y1_4 = zeros(precc, 6, 3, nsublayers-1, nlayers) # Three linearly independent y solutions - + + current_basis = y_start + for i in 2:nlayers Bprod = zeros(precc, 6, 6, nsublayers-1) - @views get_B_product!(Bprod, r[:, i], ρ[i], g[:, i], μ[i], K[i], n) + @views get_B_product!(Bprod, ω_scaled, r_scaled[:, i], ρ_scaled[i], g_scaled[:, i], μ_scaled[i], K_scaled[i], n; G0=G0) for j in 1:nsublayers-1 - y1_4[:,:,j,i] = @view(Bprod[:,:,j]) * y_start + # Multiply by the current basis + y1_4[:,:,j,i] = @view(Bprod[:,:,j]) * current_basis + + # --- QR decomposition at the sublayer --- + F = qr(y1_4[:,:,j,i]) + + Qthin = Matrix(F.Q)[:, 1:3] # extract 6x3 + Rj = Matrix(F.R) + + y1_4[:,:,j,i] = Qthin # orthonormalized + current_basis = Qthin # update the basis for next sublayer + + # store the R matrix if you need it for back-propagation + # matrices_R[i] could be replaced by a 2D array (nlayers x nsublayers) or a vector of matrices + matrices_R_sublayer[i,j] = Rj end - y_start[:,:] .= @view(y1_4[:,:,end,i]) # Set starting vector for next layer + current_basis[:,:] .= y1_4[:,:,end,i] # Update the current basis to the last sublayer of the current layer + + if cond(current_basis) > 1e12 + @warn "Current basis at layer $i is ill-conditioned: cond = $(cond(current_basis))" + end + + end + + # Convert y1_4 back to physical units + for i in axes(y1_4,4), j in axes(y1_4,3) + y1_4[:,:,j,i] .= S * y1_4[:,:,j,i] # Scale back to physical units end M = zeros(precc, 3,3) @@ -508,12 +583,15 @@ module solid1d M[2, :] .= y1_4[4,:,end,end] # Row 2 - Tangential Stress M[3, :] .= y1_4[6,:,end,end] # Row 3 - Potential Stress - return M, y1_4 + println("Forcing frequency ω = ", ω) + println("cond(M) = ", cond(M)) + + return M, y1_4, matrices_R_sublayer end """ - compute_y(r, g, M, R, y1_4, n; load=false) + compute_y(r, g, M, R, y1_4, matrices_R_sublayer, n; load=false) Compute the solution vector `y` across the entire interior, given the M matrix and the y1_4 solutions across each layer. This is used to compute the strain tensor and heating profile. @@ -532,26 +610,36 @@ module solid1d # Returns - `y::Array{ComplexF64,3}` : 3D array of the solution vector y across the interior. """ - function compute_y(r, g, M, R, y1_4, n; load=false) + function compute_y(r, g, M, R, y1_4, matrices_R_sublayer, n; load=false) nlayers = size(r)[2] nsublayers = size(r)[1] + # --- Boundary condition --- b = zeros(precc, 3) if load - b[1] = -(2n+1)*g[end,end]/(4π*(R)^2) ## Fix g[end,end] !!!!! + b[1] = -(2n+1)*g[end,end]/(4π*(R)^2) b[3] = -(2n+1)*G/(R)^2 else - b[3] = (2n+1)/R + b[3] = (2n+1)/R end - C = M \ b + # --- Solve surface coefficients --- + C_current = M \ b + # --- Allocate --- y = zeros(ComplexF64, 6, nsublayers-1, nlayers) - for i in 2:nlayers - for j in 1:nsublayers-1 - y[:,j,i] = @view(y1_4[:,:,j,i])*C + # --- Backward propagation --- + for i in nlayers:-1:2 + for j in (nsublayers-1):-1:1 + + # Compute solution + y[:,j,i] .= @view(y1_4[:,:,j,i]) * C_current + + # Update coefficients using local R + Rj = matrices_R_sublayer[i,j] + C_current = Rj \ C_current # ← CRITICAL: solve, not multiply end end @@ -588,6 +676,15 @@ module solid1d y3 = y[3] y4 = y[4] + # if abs(μr) < 1e-9 + # # Scale it down to the max allowed magnitude, preserving phase + # μr = μr / abs(μr) * 1e-9 + # end + # if abs(Ksr) < 1e-5 + # # Scale it down to the max allowed magnitude, preserving phase + # Ksr = Ksr / abs(Ksr) * 1e-5 + # end + λr = Ksr .- 2μr/3 βr = λr + 2μr From 32c2041003b9ed14a4598be0001f4ebd60082c7e Mon Sep 17 00:00:00 2001 From: Marijn Date: Wed, 1 Apr 2026 21:09:48 +0000 Subject: [PATCH 02/36] Added solid1d stabilization to docs. Fixed typos in fluid1d docs. Removes author statement from README. --- README.md | 11 ----- docs/src/reference/liquid-phase.md | 12 ++--- docs/src/reference/solid-phase.md | 71 +++++++++++++++++++++++------- 3 files changed, 61 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index f4fabc1..8fc3473 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,6 @@ Forked from the [original repository](https://github.com/hamishHay/Love.jl) of H ### Documentation https://proteus-framework.org/Obliqua -## Contributors - -| Name | Email address | -| - | - | -| Marijn van Dijk | m.r.van.dijk.3[at]student.rug.nl | -| Tim Lichtenberg | tim.lichtenberg[at]rug.nl | -| Harrison Nicholls | harrison.nicholls[at]physics.ox.ac.uk | -| Mohammad Farhat | farhat[at]berkeley.edu | -| Hamish Hay | hamish[at]tides.rocks | - - ### Repository structure * `README.md` - This file * `LICENSE.txt` - License for modification, distribution, etc. diff --git a/docs/src/reference/liquid-phase.md b/docs/src/reference/liquid-phase.md index 376007a..003c8fb 100644 --- a/docs/src/reference/liquid-phase.md +++ b/docs/src/reference/liquid-phase.md @@ -53,9 +53,9 @@ This corresponds to the simplest assumption that turbulence or small-scale mixin The exponential profile concentrates dissipation near the lower boundary and decreases with height: -[ +```math D(z) \propto e^{-z/H_R} -] +``` This can approximate scenarios where dissipation is strongest near the interface with the underlying solid layer, for example when tidal flows interact with boundary roughness or generate shear-driven turbulence. @@ -69,9 +69,9 @@ This provides a simple way to model dissipation that is concentrated toward the The quadratic profile falls off more steeply with height: -[ +```math D(z) \propto (1 - z/H_R)^2 -] +``` This concentrates dissipation even more strongly toward the base of the layer. It can approximate cases where turbulence is primarily generated near the boundary and decays rapidly away from it. @@ -79,9 +79,9 @@ This concentrates dissipation even more strongly toward the base of the layer. I The dynamic profile introduces a depth-dependent mixing length -[ +```math \ell_{\text{mix}} = \min(z, H_R) -] +``` so that the dissipation scale adjusts with distance from the boundary. diff --git a/docs/src/reference/solid-phase.md b/docs/src/reference/solid-phase.md index 5c11b72..c267136 100644 --- a/docs/src/reference/solid-phase.md +++ b/docs/src/reference/solid-phase.md @@ -19,19 +19,14 @@ To describe tidal and rotational deformations of a spherically symmetric body, ` where -* ``U_{\ell m}``: radial displacement -* ``V_{\ell m}``: tangential displacement -* ``R_{\ell m}``: radial stress -* ``S_{\ell m}``: tangential stress -* ``\Phi_{\ell m}``: gravitational potential perturbation -* ``Q_{\ell m}``: “potential stress,” defined as - -```math -Q_{\ell m} -= \frac{\partial \Phi_{\ell m}}{\partial r} - + \frac{\ell+1}{r}\Phi_{\ell m} - + 4\pi G \rho_0 U_{\ell m}. -``` +| Component | Physical Meaning | Units (SI) | Normalization Scale | Notes | +| --------------- | ---------------------------------------------------------------------------------------------- | ---------- | ------------------- | ------------------------------- | +| ``U_{\ell m}`` | Radial displacement | m | ``R_0`` | Typical planetary radius | +| ``V_{\ell m}`` | Tangential displacement | m | ``R_0`` | Same as radial displacement | +| ``R_{\ell m}`` | Radial stress | Pa | ``\mu_0`` | Characteristic shear modulus | +| ``S_{\ell m}`` | Tangential stress | Pa | ``\mu_0`` | Same as radial stress | +| ``\Phi_{\ell m}`` | Gravitational potential perturbation | m``^2/``s``^2`` | ``g_0 R_0`` | ``g_0`` is characteristic gravity | +| ``Q_{\ell m}`` | Potential stress | m/s``^2`` | ``g_0`` | Same units as gravity | The spheroidal vector satisfies the first-order ODE system @@ -43,7 +38,7 @@ The spheroidal vector satisfies the first-order ODE system Here, the coefficient matrix ``\mathbf{A}_{\ell}(r)`` represents the responds of the mantle to deformations, and is given by ```math -A(r) = +\mathbf{A}_\ell(r) = \begin{pmatrix} -\frac{2\lambda}{r\beta} & \frac{\ell(\ell+1)\lambda}{r\beta} & @@ -93,6 +88,20 @@ and \lambda = \kappa - \frac{2}{3}\mu. ``` +When solving the spheroidal displacement–stress–gravity system, the 6-vector can span vastly different physical units. This disparity can make the coefficient matrix ``\mathbf{A}_\ell(r)`` highly ill-conditioned, leading to numerical instability when computing linearly independent solutions or performing matrix inversions. + +To mitigate this, we introduce a unit-normalization scaling matrix ``\mathbf{S}``, defined as +```math + \mathbf{S} = \mathrm{diag}\Big(R_0, R_0, \mu_0, \mu_0, g_0 R_0, g_0 \Big), +``` + +where ``R_0, \mu_0, g_0`` are characteristic scales for length, stress, and gravity, respectively. The scaled variables are then + +```math + \tilde{\mathbf{y}}_{\ell m} = \mathbf{S}^{-1}\mathbf{y}_{\ell m}, \quad + \tilde{\mathbf{A}}_\ell = \mathbf{S}^{-1} \mathbf{A}_\ell \mathbf{S}. +``` + --- ### Core–Mantle Boundary @@ -118,6 +127,12 @@ q_\ell(r_C) & 0 & 4\pi G \rho_0(r_C^-) \end{pmatrix}. ``` +The assumed normalization implies + +```math +\mathbf{\tilde{I}}_C = \mathbf{S}^{-1} \mathbf{I}_C. +``` + Once the constants ``\mathbf{C}`` are determined, the full perturbed state of the solid mantle is known. --- @@ -129,7 +144,17 @@ We propagate the solution using the so-called propagator matrix (``\pmb{\Pi}_\el ```math \frac{d\pmb{\Pi}_\ell(r, r')}{dr} = \pmb{A}_\ell(r)\,\pmb{\Pi}_\ell(r, r'), ``` -at radius ``r`` w.r.t. the solution at the previous layer ``r'``, this is also know as the Cauchy data at radius (``r'``). If ``r = r'`` we have +at radius ``r`` w.r.t. the solution at the previous layer ``r'``, this is also know as the Cauchy data at radius (``r'``). Equivalently, we may write the normalized version of the propagator matrix as + +```math +\frac{d\tilde{\pmb{\Pi}}_\ell(r, r')}{dr} = \tilde{\pmb{A}}_\ell(r)\,\tilde{\pmb{\Pi}}_\ell(r, r'), +``` + +Given that ``\tilde{\pmb{\Pi}}_\ell(r, r')`` is constructed from the normalized matrix ``\tilde{\pmb{A}}_\ell``, the normailization in ``\mathbf{A}_\ell`` enters ``\tilde{\pmb{\Pi}}_\ell`` as + +```math +\tilde{\pmb{\Pi}}_\ell(r, r') = \left[\left[\left[ \mathbf{1} \times \mathbf{S}^{-1} \tilde{\pmb{\Pi}}_\ell(r_1, r_1') \mathbf{S}\right] \times \mathbf{S}^{-1} \tilde{\pmb{\Pi}}_\ell(r_2, r_2') \mathbf{S} \right] \times \dots \right] = \mathbf{S}^{-1} \pmb{\Pi}_\ell(r, r') \mathbf{S}, +``` the normalization does not alter the physical solution. If ``r = r'`` we have ```math \pmb{\Pi}_\ell(r', r') = \pmb{1}. @@ -141,6 +166,12 @@ Each column of the propagator matrix is one of the six linearly independent solu \frac{d\pmb{y}_{\ell m}}{dr} = \pmb{A}_\ell(r)\,\pmb{y}_{\ell m}. ``` +The six linearly independent solutions are multiplied by the propagator, forming a basis matrix. To prevent ill-conditioning due to widely differing units and growing/decaying solutions, we perform a QR decomposition at every sublayer: + +```math +\pmb{\Pi}_\ell(r, r') = \mathbf{Q}\,\mathbf{R}, +``` where ``\mathbf{Q}`` is an orthogonal matrix and ``\mathbf{R}`` is an upper triangular matrix. The orthogonal matrix ``\mathbf{Q}`` forms an orthonormalized basis used to propagate to the next sublayer, while the upper triangular matrix ``\mathbf{R}`` stores the mixing coefficients for back-propagation, ensuring accurate reconstruction of the physical solution. The process is repeated across all layers. + We impose continuity: ```math @@ -160,6 +191,13 @@ Therefore, \pmb{\Pi}_\ell(r, r_C^+)\,\pmb{I}_C\,\pmb{C}. ``` +or in the normalized form + +```math +\pmb{y}_{\ell m}(r) = +\mathbf{S} \, \tilde{\pmb{\Pi}}_\ell(r, r_C^+)\,\tilde{\pmb{I}}_C\,{\pmb{C}}. +``` + This equation can be solved iteratively, up till the surface to yield the general responds of the interior to any form of tidal- or load induced deformation. --- @@ -227,4 +265,5 @@ Thus, To solve this system we thus need only provide ``\pmb{P}_1\,\pmb{y}(a^-)``, we will provide some examples in Chapter 7 at the end of this component. ---- \ No newline at end of file +--- + From c171e92ea599912d041e44f96a93f83f977ae189 Mon Sep 17 00:00:00 2001 From: Marijn Date: Wed, 1 Apr 2026 21:19:15 +0000 Subject: [PATCH 03/36] Fixed inline comments. --- src/Obliqua.jl | 10 +++++----- src/solid1d.jl | 38 +++++++++++++++++++------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Obliqua.jl b/src/Obliqua.jl index d4b3b36..2995b42 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -1572,8 +1572,8 @@ module Obliqua Vs = (4/3) * π * (r[end]^3 - r[1]^3) dVs = (4/3) * π .* (r[2:end].^3 .- r[1:end-1].^3) - # Love numbers (interface vs fluid interior) - k2_T, k2_L = run_fluid0d(omega, rho, r, ρ_ratio; n=n, sigma_R=efficiency * sigma_R) + # Love numbers (solid-fluid interface and fluid interior) + k2_T, k2_L = run_fluid0d(omega, rho, r, ρ_ratio; n=n, sigma_R=efficiency * sigma_R) k2_T_inf, _ = run_fluid0d(omega, rho, r, ρ_ratio; n=n, sigma_R=sigma_inf) # total heating (bulk) @@ -1582,7 +1582,7 @@ module Obliqua power_blk = prefactor * -imag(k2_T) / Vs power_blk_inf = prefactor * -imag(k2_T_inf) / Vs - Δpower = max(power_blk - power_blk_inf, 0) + D_power_blk = max(power_blk - power_blk_inf, 0) # radial positions r_mid = 0.5 .* (r[1:end-1] .+ r[2:end]) @@ -1609,7 +1609,7 @@ module Obliqua end # heating profile - power_prf = power_blk_inf .+ Δpower .* shape + power_prf = power_blk_inf .+ D_power_blk .* shape # normalize to match bulk heating unorm = sum(power_prf .* dVs) / Vs @@ -1649,7 +1649,7 @@ module Obliqua - `k2_T::precc` : Complex Tidal k2 Lovenumber. - `k2_L::precc` : Complex Load k2 Lovenumber. """ - function run_fluid1d_RD( omega::Float64, + function run_fluid1d_RD(omega::Float64, rho::Vector{prec}, radius::Vector{prec}, gravity::Vector{prec}, diff --git a/src/solid1d.jl b/src/solid1d.jl index 2ca5ef1..fafc09e 100644 --- a/src/solid1d.jl +++ b/src/solid1d.jl @@ -1,6 +1,6 @@ -module solid1d#_scaled +module solid1d using LinearAlgebra using DoubleFloats @@ -206,18 +206,19 @@ module solid1d#_scaled function get_scales(r, ρ, R0, M0, s0) - ρ0 = M0 / R0^3 # 1000 kg/m^3 - μ0 = M0 / (R0 * s0^2) # 1e9 Pa (1 GPa) - g0 = R0 / s0^2 # 0.1 m/s^2 (Note: check if you want 10 or 0.1) + ρ0 = M0 / R0^3 # kg/m^3 + μ0 = M0 / (R0 * s0^2) # Pa + g0 = R0 / s0^2 # m/s^2 + G0 = R0^3 / (M0 * s0^2) # Gravity constant scaling S = Diagonal(precc[ - R0, # y1: radial displacement (m) - R0, # y2: tangential displacement (m) - μ0, # y3: radial stress (Pa) - μ0, # y4: tangential stress (Pa) - g0*R0, # y5: potential (m^2/s^2) - g0 # y6: potential gradient/gravity (m/s^2) + R0, # y1: radial displacement (m) + R0, # y2: tangential displacement (m) + μ0, # y3: radial stress (Pa) + μ0, # y4: tangential stress (Pa) + g0*R0, # y5: potential (m^2/s^2) + g0 # y6: potential gradient/gravity (m/s^2) ]) Sinv = inv(S) @@ -513,6 +514,7 @@ module solid1d#_scaled # Returns - `M::Array{precc,2}` : 3x3 M matrix, which is used to propagate the solution across the entire interior. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. + - `matrices_R_sublayer::Array{Matrix{precc},2}` : 2D array of the R matrices from the QR decomposition at each sublayer, which is used for back-propagation in the `compute_y` function to compute the solution vector across the interior. """ function compute_M(ω, r, ρ, g, μ, K, n; core="liquid") r, ρ, g, μ, K = convert_params_to_prec(r, ρ, g, μ, K) @@ -534,8 +536,6 @@ module solid1d#_scaled y_start .= Sinv * y_start # Scale starting vector # To store QR factors for back-propagation - # matrices_Q stores the orthonormal basis at each layer interface - # matrices_R stores the mixing coefficients matrices_R_sublayer = Array{Matrix{precc}}(undef, nlayers, nsublayers-1) y1_4 = zeros(precc, 6, 3, nsublayers-1, nlayers) # Three linearly independent y solutions @@ -550,7 +550,7 @@ module solid1d#_scaled # Multiply by the current basis y1_4[:,:,j,i] = @view(Bprod[:,:,j]) * current_basis - # --- QR decomposition at the sublayer --- + # QR decomposition at the sublayer F = qr(y1_4[:,:,j,i]) Qthin = Matrix(F.Q)[:, 1:3] # extract 6x3 @@ -560,7 +560,6 @@ module solid1d#_scaled current_basis = Qthin # update the basis for next sublayer # store the R matrix if you need it for back-propagation - # matrices_R[i] could be replaced by a 2D array (nlayers x nsublayers) or a vector of matrices matrices_R_sublayer[i,j] = Rj end @@ -602,6 +601,7 @@ module solid1d#_scaled - `M::Array{precc,2}` : 3x3 M matrix, which is used to propagate the solution across the entire interior. - `R::prec` : Surface radius of the body. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. + - `matrices_R_sublayer::Array{Matrix{precc},2}` : 2D array of the R matrices from the QR decomposition at each sublayer, which is used for back-propagation in the `compute_y` function to compute the solution vector across the interior. - `n::Int` : Tidal degree. # Keyword Arguments @@ -615,7 +615,7 @@ module solid1d#_scaled nlayers = size(r)[2] nsublayers = size(r)[1] - # --- Boundary condition --- + # Boundary condition b = zeros(precc, 3) if load b[1] = -(2n+1)*g[end,end]/(4π*(R)^2) @@ -624,13 +624,13 @@ module solid1d#_scaled b[3] = (2n+1)/R end - # --- Solve surface coefficients --- + # Solve surface coefficients C_current = M \ b - # --- Allocate --- + # Allocate y = zeros(ComplexF64, 6, nsublayers-1, nlayers) - # --- Backward propagation --- + # Backward propagation for i in nlayers:-1:2 for j in (nsublayers-1):-1:1 @@ -639,7 +639,7 @@ module solid1d#_scaled # Update coefficients using local R Rj = matrices_R_sublayer[i,j] - C_current = Rj \ C_current # ← CRITICAL: solve, not multiply + C_current = Rj \ C_current end end From 31597631caa871ec41bf77e1db83fc629d102785 Mon Sep 17 00:00:00 2001 From: Marijn Date: Tue, 14 Apr 2026 21:15:52 +0000 Subject: [PATCH 04/36] Added mostly functional relaxation/trasition matrix method. Added grid resample function. Added plot function to display bavavior of y functions. --- src/Obliqua.jl | 203 ++++++++++ src/plotting.jl | 43 +++ src/solid1d_relax.jl | 879 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1125 insertions(+) create mode 100644 src/solid1d_relax.jl diff --git a/src/Obliqua.jl b/src/Obliqua.jl index 2995b42..4b42660 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -20,6 +20,7 @@ module Obliqua # Include local jl files include("solid0d.jl") include("solid1d.jl") + include("solid1d_relax.jl") include("solid1d_mush.jl") include("fluid0d.jl") include("Hansen.jl") @@ -29,6 +30,7 @@ module Obliqua # Import submodules import .solid0d import .solid1d + import .solid1d_relax import .solid1d_mush import .fluid0d import .Hansen @@ -38,6 +40,7 @@ module Obliqua # Export submodules (mostly for autodoc purposes) export solid0d export solid1d + export solid1d_relax export solid1d_mush export fluid0d export Hansen @@ -513,6 +516,16 @@ module Obliqua κ_seg, R; ncalc=ncalc, n=n, m=m ) + # elseif 1D interior and heating profile from strain tensor + elseif module_solid=="solid1d_relax" + # calculate tides in solid region + prf_seg[iss,:], kT, kL = run_solid1d_relax( + σ, ρ_seg, r_seg, + g_seg, η_seg, + μc_seg[:, iss], + κ_seg, R; + ncalc=ncalc, n=n, m=m + ) # elseif 1D interior with mush interface and heating profile from strain tensor elseif module_solid=="solid1d-mush" prf_seg[iss,:], kT, kL = run_solid1d_mush( @@ -1156,6 +1169,8 @@ module Obliqua # Load tidal_solution_L = solid1d.compute_y(rr, g, M, R, y1_4, matrices_R, n; load=true) + plotting.plot_relaxation_solution(tidal_solution_T[1:6, end, :], r[1:end-1], filename="$OUT_DIR/shooting_solution.png") + # get k2 tidal Love Number (complex-valued) k2_T = tidal_solution_T[5, end, end] - 1 k2_L = tidal_solution_L[5, end, end] - 1 @@ -1176,6 +1191,194 @@ module Obliqua end + function resample_profiles(radius, rho, visc, shear, bulk, grav, ncalc) + + r_b = convert(Vector{prec}, radius) + + ρ_old = convert(Vector{prec}, rho) + η_old = convert(Vector{prec}, visc) + μ_old = convert(Vector{precc}, shear) + κ_old = convert(Vector{prec}, bulk) + g_old = convert(Vector{prec}, grav) + + N = length(ρ_old) + + # centers of original grid + r_c = 0.5 .* (r_b[1:N] .+ r_b[2:N+1]) + + rmin = first(r_b) + rmax = last(r_b) + + # detect steep viscosity transitions + logη = log.(η_old) + + dlogη = abs.(diff(logη)) # logarithmic gradient + + threshold = log(1e3) # threshold for significant viscosity jump + + jump_idx = findall(dlogη .> threshold) + + # base stretched grid (without refinement) using a power-law stretching function + s = range(0, 1, length=ncalc+1) + + γ = 3 + f_base(s) = 1 - (1 - s)^γ + + r_base = rmin .+ (rmax - rmin) .* f_base.(s) + + # build refinement weight function + refine = zeros(eltype(r_base), length(r_base)) + + σ = 0.12 * (rmax - rmin) # width of refinement region + + for idx in jump_idx + r_jump = r_c[idx] + + # exponential bump + for i in eachindex(r_base) + if r_base[i] > r_jump + refine[i] += exp(-(r_base[i] - r_jump) / σ) # only above transition + end + end + end + + # normalize refinement + if maximum(refine) > 0 + refine ./= maximum(refine) + end + + # distort grid + α = 0.5 # strength of refinement + + r_new_b = similar(r_base) + + r_new_b[1] = rmin + for i in 2:length(r_base) + dr = r_base[i] - r_base[i-1] + + # shrink spacing near jumps + dr_mod = dr * (1 - α * refine[i]) + + r_new_b[i] = r_new_b[i-1] + dr_mod + end + + # rescale to enforce exact endpoints + r_new_b .= rmin .+ (rmax - rmin) .* (r_new_b .- r_new_b[1]) ./ (r_new_b[end] - r_new_b[1]) + + r_new_c = 0.5 .* (r_new_b[1:end-1] .+ r_new_b[2:end]) + + # interpolate profiles onto new grid using linear interpolation (log–log for viscosity and shear modulus) + itp_ρ = linear_interpolation(r_c, ρ_old, extrapolation_bc=Line()) + itp_g = linear_interpolation(r_c, g_old, extrapolation_bc=Line()) + + itp_η = linear_interpolation(r_c, log.(η_old), extrapolation_bc=Line()) + itp_κ = linear_interpolation(r_c, log.(κ_old), extrapolation_bc=Line()) + + μ_mag = abs.(μ_old) + μ_phase = angle.(μ_old) + + itp_μ_mag = linear_interpolation(r_c, log.(μ_mag), extrapolation_bc=Line()) + itp_μ_phase = linear_interpolation(r_c, μ_phase, extrapolation_bc=Line()) + + # evaluate interpolants on new grid + ρ = itp_ρ.(r_new_c) + g = itp_g.(r_new_c) + + η = exp.(itp_η.(r_new_c)) + κ = exp.(itp_κ.(r_new_c)) + + μ = exp.(itp_μ_mag.(r_new_c)) .* cis.(itp_μ_phase.(r_new_c)) + + return r_new_b, ρ, η, μ, κ, g + end + + + function run_solid1d_relax( omega::Float64, + rho::Array{prec,1}, + radius::Array{prec,1}, + g::Array{prec,1}, + visc::Array{prec,1}, + shear::Array{precc,1}, + bulk::Array{prec,1}, + R::prec; + ncalc::Int=2000, + n::Int=2, + m::Int=2 + )::Tuple{Array{prec,1},precc,precc} + + # for debugging + println("Running 1D relaxation method...") + + # convert inputs + ρ = convert(Vector{prec}, rho) + r = convert(Vector{prec}, radius) + η = convert(Vector{prec}, visc) + μ = convert(Vector{precc}, shear) + κ = convert(Vector{prec}, bulk) + g = convert(Vector{prec}, g) + + # determine number of layers based on frequency (more layers for higher frequencies) + if omega > 1e-7 + Nr = trunc(Int, (length(r) - 1) * 100) + elseif omega > 1e-10 + Nr = trunc(Int, (length(r) - 1) * 20) + else + Nr = trunc(Int, (length(r) - 1) * 10) + end + + # resample profiles onto new grid + r_grid, ρ, η, μ, κ, g = resample_profiles(r, ρ, η, μ, κ, g, Nr) + + # use cell centers + r_centers = 0.5 .* (r_grid[1:end-1] .+ r_grid[2:end]) + + # define angular grid + solid1d_relax.define_spherical_grid(res, n, m) + + # solve y functions across grid + y = solid1d_relax.compute_y_relaxation(r_centers, ρ, g, μ, κ, omega, n, R) + + # for debugging: plot y-function relaxation solution + # in particular, observe the oscillating behavior near transition zones + # and also near the surface for high frequencies + plotting.plot_relaxation_solution(y, r_centers, + filename="$OUT_DIR/relaxation_solution.png") + + # Love numbers + k2_T = y[5, end] - 1 + k2_L = 0.0 + 0im + + # enforce sign consistency with omega + # this should not be necessary, but due to numerical issues k2_T can + # end up in the wrong quadrant of the complex plane + if omega < 0 + k2_T = real(k2_T) + 1im * abs(imag(k2_T)) + else + k2_T = real(k2_T) - 1im * abs(imag(k2_T)) + end + + # heating profile + (Eμ, Eκ) = solid1d_relax.get_heating_profile( + y, r_grid, ρ, g, μ, κ, n, omega + ) + + Eμ_tot, _ = Eμ + Eκ_tot, _ = Eκ + + power_prf = abs.(Eμ_tot .+ Eκ_tot) .* (R ./ maximum(r)).^(2) + + # interpolate from grid back to original radius points + itp = linear_interpolation(r_centers, power_prf, extrapolation_bc=Line()) + + # original centers + r_orig_centers = 0.5 .* (r[1:end-1] .+ r[2:end]) + + power_prf = itp.(r_orig_centers) + + return power_prf, k2_T, k2_L + end + + # """ # run_solid1d(rho, radius, visc, shear, bulk; ncalc=2000, n=2) diff --git a/src/plotting.jl b/src/plotting.jl index 28c575f..a22d0e9 100644 --- a/src/plotting.jl +++ b/src/plotting.jl @@ -327,5 +327,48 @@ module plotting end + function plot_relaxation_solution(y, r; filename="relaxation_solution.png") + + R = maximum(r) + rnorm = r ./ R + + plt = plot(layout=(2,3), size=(1000,600)) + + for i in 1:6 + yi = y[i, :] + + # plot directly into subplot + plot!( + plt[i], + real(yi), rnorm, + label = "Re(y$i)", + lw = 2 + ) + + plot!( + plt[i], + imag(yi), rnorm, + label = "Im(y$i)", + lw = 2, + ls = :dash + ) + + xlabel!(plt[i], "y$i") + ylabel!(plt[i], "r / R") + title!(plt[i], "y$i") + + # fix axis direction explicitly + ylims!(plt[i], minimum(rnorm), 1) + + # zero reference line + vline!(plt[i], [0], color=:black, lw=1, ls=:dot, label=false) + end + + savefig(plt, filename) + @info "Saved relaxation solution plot to $filename" + + return plt + end + end \ No newline at end of file diff --git a/src/solid1d_relax.jl b/src/solid1d_relax.jl new file mode 100644 index 0000000..2358ed5 --- /dev/null +++ b/src/solid1d_relax.jl @@ -0,0 +1,879 @@ + + +module solid1d_relax + + using LinearAlgebra + using DoubleFloats + using AssociatedLegendrePolynomials + using StaticArrays + using SpecialFunctions + using SparseArrays + using Statistics + + prec = BigFloat + precc = Complex{BigFloat} + + const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 + + clats = 0.0 + lons = 0.0 + Y = 0.0 + dYdθ = 0.0 + dYdϕ = 0.0 + Z = 0.0 + X = 0.0 + res = 20.0 + + + """ + expand_layers(r; nr::Int=80) + + Discretize the primary layers given by `r` into `nr` discrete secondary layers. + + # Arguments + - `r::Array{Float64,2}` : 2D array of primary layer boundaries. + + # Keyword Arguments + - `nr::Int=80` : Number of secondary layers to discretize. + + # Returns + - `rs::Array{Float64,2}` : 2D array of secondary layer boundaries/ + """ + function expand_layers(r; nr::Int=80) + + rs = zeros(Float64, (nr+1, length(r)-1)) + + for i in 1:length(r)-1 + rfine = LinRange(r[i], r[i+1], nr+1) + rs[:, i] .= rfine[1:end] + end + + return rs + end + + + """ + get_g(r, ρ) + + Compute the radial gravity structure associated with a density profile `r` at intervals given by `r`. + + # Arguments + - `r::Array{Float64,2}` : 2D array of layer boundaries. + - `ρ::Array{Float64,1}` : 1D array of layer densities. The length of `ρ` must be equal to the number of columns in `r`. + + # Returns + - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. The dimensions of `g` are the same as `r`. + + # Notes + `r` must be be a 2D array, with index 1 representing the top radius of secondary layers, and index 2 + representing the top radius of primary layers. + """ + function get_g(r, ρ) + g = zeros(Float64, size(r)) + M = zeros(Float64, size(r)) + + for i in 1:size(r)[2] + M[2:end,i] = 4.0/3.0 * π .* diff(r[:,i].^3) .* ρ[i] + end + + g[2:end,:] .= G*accumulate(+,M[2:end,:]) ./ r[2:end,:].^2 + g[1,2:end] = g[end,1:end-1] + + return g + end + + + """ + Ynm(n, m, theta, phi) + + Compute the spherical harmonic Ynm for given n, m, theta, and phi. + + # Arguments + - `n::Int` : Tidal degree. + - `m::Int` : Tidal order. + - `theta::Array{Float64,1}` : Array of colatitudes in radians. + - `phi::Array{Float64,1}` : Array of longitudes in radians. + + # Returns + - `Ynm::Array{ComplexF64,2}` : 2D array of spherical harmonic values for each combination of theta and phi. + """ + function Ynm(n, m, theta, phi) + return Plm.(n, m, cos.(theta)) .* exp.(1im * m .* phi) + end + + + """ + define_spherical_grid(res) + + Create the spherical grid of angular resolution `res` in degrees. This is used for + numerical integrations over solid angle. A new grid can easily be defined by + recalling the function with a new `res`. + + # Arguments + - `res::Float64` : Desired angular resolution in degrees. + - `n::Int` : Tidal degree. + - `m::Int` : Tidal order. + + # Notes + The grid is internal to solid1d, but can be accessed with + + solid1d_relax.clats[:] # colatitude grid + solid1d_relax.lons[:] # longitude grid + """ + function define_spherical_grid(res, n, m) + solid1d_relax.res = res + + # θ and φ grids + lons = deg2rad.(collect(0:res:360-0.001))' + clats = deg2rad.(collect(0:res:180)) + clats[1] += 1e-6 + clats[end] -= 1e-6 + + # allocate arrays + solid1d_relax.Y = zeros(ComplexF64, 1, length(clats), length(lons)) + solid1d_relax.dYdθ = similar(solid1d_relax.Y) + solid1d_relax.dYdϕ = similar(solid1d_relax.Y) + solid1d_relax.Z = similar(solid1d_relax.Y) + solid1d_relax.X = similar(solid1d_relax.Y) + + sinθ = sin.(clats) + cosθ = cos.(clats) + cotθ = cosθ ./ sinθ + cscθ = csc.(clats) + + # Normalization factor for spherical harmonics + norm = sqrt((2*n+1) * factorial(n-m) / (4π * factorial(n+m))) + + i = 1 + + # Y + solid1d_relax.Y[i,:,:] = Ynm(n,m,clats,lons) + + # ∂Y/∂θ + Pn = Plm.(n, m, cosθ) + if n > m + Pn_1 = Plm.(n-1, m, cosθ) + dPdθ = (n .* cosθ .* Pn .- (n + m) .* Pn_1) ./ (sinθ) + else + # m == n -> P_{n-1}^m = 0 + dPdθ = (n .* cosθ .* Pn) ./ (sinθ) + end + solid1d_relax.dYdθ[i,:,:] .= dPdθ .* exp.(1im .* m .* lons) + + # ∂Y/∂ϕ + solid1d_relax.dYdϕ[i,:,:] .= 1im * m .* solid1d_relax.Y[i,:,:] + + # Z = 2 ((1/sinθ) ∂²Y/∂θ∂ϕ - cotθ cscθ ∂Y/∂ϕ) + solid1d_relax.Z[i,:,:] .= 2 .* (1im * m ./ sinθ .* solid1d_relax.dYdθ[i,:,:] .- cotθ .* cscθ .* solid1d_relax.dYdϕ[i,:,:]) + + # X = -2 (cotθ ∂Y/∂θ + csc²θ ∂²Y/∂ϕ²) - n(n+1)) Y + solid1d_relax.X[i,:,:] .= -2 .* (cotθ .* solid1d_relax.dYdθ[i,:,:] .- cscθ.^2 .* m^2 .* solid1d_relax.Y[i,:,:]) .- n*(n+1) .* solid1d_relax.Y[i,:,:] + + # Normalize + solid1d_relax.Y[i,:,:] .*= norm + solid1d_relax.dYdθ[i,:,:] .*= norm + solid1d_relax.dYdϕ[i,:,:] .*= norm + solid1d_relax.Z[i,:,:] .*= norm + solid1d_relax.X[i,:,:] .*= norm + + # save grids + solid1d_relax.clats = clats + solid1d_relax.lons = lons + end + + + """ + convert_params_to_prec(r, ρ, g, μ, κs) + + Convert input parameters into the required precision. + # Arguments + - `r::Array{Float64,2}` : 2D array of layer boundaries. + - `ρ::Array{Float64,1}` : 1D array of layer densities. + - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. + - `μ::Array{Float64,1}` : 1D array of layer shear moduli. + - `κs::Array{Float64,1}` : 1D array of layer bulk moduli. + + # Returns + Tuple of converted parameters in the required precision. + """ + function convert_params_to_prec(r, ρ, g, μ, κs) + r_prec = convert(Array{prec}, r) + ρ_prec = convert(Array{prec}, ρ) + g_prec = convert(Array{prec}, g) + μ_prec = convert(Array{precc}, μ) + κs_prec = convert(Array{precc}, κs) + + return (r_prec, ρ_prec, g_prec, μ_prec, κs_prec) + end + + + function get_scales(R0, M0, s0) + + ρ0 = M0 / R0^3 # 1000 kg/m^3 + μ0 = M0 / (R0 * s0^2) # 1e9 Pa (1 GPa) + g0 = R0 / s0^2 # 0.1 m/s^2 (Note: check if you want 10 or 0.1) + G0 = R0^3 / (M0 * s0^2) # Gravity constant scaling + + S = Diagonal(precc[ + R0, # y1: radial displacement (m) + R0, # y2: tangential displacement (m) + μ0, # y3: radial stress (Pa) + μ0, # y4: tangential stress (Pa) + g0*R0, # y5: potential (m^2/s^2) + g0 # y6: potential gradient/gravity (m/s^2) + ]) + + Sinv = inv(S) + return R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv + end + + + function doublefactorial(n::Integer) + n < 0 && error("doublefactorial not defined for negative n") + n == 0 && return one(n) + n == 1 && return one(n) + + result = one(n) + for k in n:-2:1 + result *= k + end + return result + end + + + function safe_sph_besselj(l, x) + try + return sphericalbesselj(l, x) + catch + # asymptotic fallback + return sin(x - l*pi/2) / x + end + end + + + """ + get_Ic(ω, r, ρ, g, μ, K, type, n; M=6, N=3) + + Get the core solution vector. + + # Arguments + - `ω::prec` : Angular frequency. + - `r::prec` : Radius of the core boundary. + - `ρ::prec` : Density of the core. + - `g::prec` : Gravity at the core boundary. + - `μ::prec` : Shear modulus of the core. + - `K::prec` : Bulk modulus of the core. + - `type::String` : Type of core, either "liquid" or "solid". + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `M::Int=6` : Number of rows in the Ic matrix. This should be 6 for the solid-body problem. + - `N::Int=3` : Number of linearly independent solutions to compute. This should be 3 for the solid-body problem. + + # Returns + - `Ic::Array{precc,2}` : MxN array of linearly independent solutions at the core boundary. These are used as starting vectors for the numerical integration across the interior. + """ + function get_Ic(ω, r, ρ, g, μ, K, type, n; G0=1, M=6, N=3) + Ic = zeros(precc, M, N) + + G_norm = G / G0 + + if type=="liquid" + Ic[1,1] = -r^n / g + Ic[1,3] = 1.0 + Ic[2,2] = 1.0 + Ic[3,3] = g*ρ + Ic[5,1] = r^n + Ic[6,1] = 2(n-1)*r^(n-1) + Ic[6,3] = 4π * G_norm * ρ + elseif type == "inertial" + φ = 4π * G_norm * ρ / 3 + + Ic[1,1] = n * r^(n-1) + Ic[2,1] = r^(n-1) + Ic[3,1] = 0.0 + Ic[4,1] = 0.0 + Ic[5,1] = -(n*φ - ω^2) * r^n + Ic[6,1] = -(2*(n-1)*n*φ - (2*n + 1)*ω^2) * r^(n-1) + + α = sqrt(K / ρ) + f = -ω^2 / φ + h = f - (n + 1) + k2 = (ω^2 + 4φ - n*(n+1)*φ^2 / ω^2) / α^2 + k = sqrt(Complex{BigFloat}(k2)) + x = k * r + + x64 = ComplexF64(x) + + jl_n = safe_sph_besselj(n, x64) + jl_np1 = safe_sph_besselj(n+1, x64) + + ϕl = doublefactorial(2n+1) / x^n * jl_n + ϕlp1 = doublefactorial(2n+3) / x^(n+1) * jl_np1 + ψl = 2*(2n+3)/x^2 * (1 - ϕl) + pref = -r^(n+1) / (2n + 3) + + Ic[1,2] = pref * (0.5 * n * h * ψl + f * ϕlp1) + Ic[2,2] = pref * (0.5 * h * ψl - ϕlp1) + Ic[3,2] = -φ * r^n * f * ϕl + Ic[4,2] = 0.0 + Ic[5,2] = -r^(n+2) * ( + (α^2 * f)/r^2 - (3φ*f)/(2*(2n+3)) * ψl + ) + Ic[6,2] = -r^(n+1) * ( + (2n+1)*(α^2*f)/r^2 - + (3φ*((2n+1)*f - n*h))/(2*(2n+3)) * ψl + ) + + Ic[:,3] .= 0.0 + Ic[2,3] = 1.0 # tangential slip + else # incompressible solid core + # First column + Ic[1, 1] = n*r^( n+1 ) / ( 2*( 2n + 3) ) + Ic[2, 1] = ( n+3 )*r^( n+1 ) / ( 2*( 2n+3 ) * ( n+1 ) ) + Ic[3, 1] = ( n*ρ*g*r + 2*( n^2 - n - 3)*μ ) * r^n / ( 2*( 2n + 3) ) + Ic[4, 1] = n *( n+2 ) * μ * r^n / ( ( 2n + 3 )*( n+1 ) ) + Ic[6, 1] = 2π*G_norm*ρ*n*r^( n+1 ) / ( 2n + 3 ) + + # Second column + Ic[1, 2] = r^( n-1 ) + Ic[2, 2] = r^( n-1 ) / n + Ic[3, 2] = ( ρ*g*r + 2*( n-1 )*μ ) * r^( n-2 ) + Ic[4, 2] = 2*( n-1 ) * μ * r^( n-2 ) / n + Ic[6, 2] = 4π*G_norm*ρ*r^( n-1 ) + + # Third column + Ic[3, 3] = -ρ * r^n + Ic[5, 3] = -r^n + Ic[6, 3] = -( 2n + 1) * r^( n-1 ) + + end + + return Ic + end + + + """ + get_A(r, ρ, g, μ, K, n) + + Compute the 6x6 `A` matrix in the ODE for the solid-body problem. + + # Arguments + - `r::prec` : Radius at which to compute the A matrix. + - `ρ::prec` : Density at radius r. + - `g::prec` : Gravity at radius r. + - `μ::prec` : Shear modulus at radius r. + - `K::prec` : Bulk modulus at radius r. + - `n::Int` : Tidal degree. + + # Returns + - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. + + # Notes + See also [`get_A!`](@ref) + """ + function get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing) + A = zeros(precc, 6, 6) + get_A!(A, ω, r, ρ, g, μ, K, n; G0=G0, λ=λ) + return A + end + + + """ + get_A!(A, r, ρ, g, μ, K, n; λ=nothing) + + Compute the 6x6 `A` matrix in the ODE for the solid-body problem. These correspond to + the coefficients given in Equation S4.6 in Hay et al., (2025) when α=φ=0, as well as Sabadini and Vermeersen + (2016) Eq. 1.95. + + # Arguments + - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. + - `r::prec` : Radius at which to compute the A matrix. + - `ρ::prec` : Density at radius r. + - `g::prec` : Gravity at radius r. + - `μ::prec` : Shear modulus at radius r. + - `K::prec` : Bulk modulus at radius r. + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. + + # Notes + See also [`get_A`](@ref) + """ + function get_A!(A::Matrix, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing) + if isnothing(λ) + λ = K - 2μ/3 + end + + G_norm = G / G0 + + r_inv = 1.0/r + β_inv = 1.0/(2μ + λ) + rβ_inv = r_inv * β_inv + + A[1,1] = -2λ * r_inv*β_inv + A[2,1] = -r_inv + A[3,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) - ω^2 * ρ + A[4,1] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) + A[5,1] = 4π * G_norm * ρ + A[6,1] = 4π*(n+1)*G_norm*ρ*r_inv + + A[1,2] = n*(n+1) * λ * r_inv*β_inv + A[2,2] = r_inv + A[3,2] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) + A[4,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) - ω^2 * ρ + A[6,2] = -4π*n*(n+1)*G_norm*ρ*r_inv + + A[1,3] = β_inv + A[3,3] = r_inv*β_inv * (-4μ ) + A[4,3] = -λ * r_inv*β_inv + + A[2,4] = 1.0 / μ + A[3,4] = n*(n+1)*r_inv + A[4,4] = -3r_inv + + A[3,5] = ρ * (n+1)*r_inv + A[4,5] = -ρ*r_inv + A[5,5] = -(n+1)r_inv + + A[3,6] = -ρ + A[5,6] = 1.0 + A[6,6] = (n-1)r_inv + end + + + function solve_radial_system(r, ρ, g, μ, K, ω, n, R_planet; core="inertial") + + Nr = length(r) + + # scaling + # R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(1, 1, 1) + R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(9.8e6, 4.3e22, 4.3e3) + + # Extract sub-blocks for BC scaling + Su = S[1:3, 1:3] + Sl = S[4:6, 4:6] + Sinv_u = Sinv[1:3, 1:3] + Sinv_l = Sinv[4:6, 4:6] + + ωs = ω * s0 + rs = r ./ R0 + ρs = ρ ./ ρ0 + gs = g ./ g0 + μs = μ ./ μ0 + Ks = K ./ μ0 + + # boundary conditions + B1 = zeros(precc, 3, 6) + + function isbad(A) + any(x -> !isfinite(real(x)) || !isfinite(imag(x)), A) + end + + try + B1 .= get_core_bc!(ω, r[1], ρ[1], g[1], μ[1], K[1], core, n; G0=1) + + if isbad(B1) + println("NaN/Inf detected in core BC → switching to liquid core") + B1 .= get_core_bc!(ω, r[1], ρ[1], g[1], μ[1], K[1], "liquid", n; G0=1) + end + + catch e + println("Error in core BC → switching to liquid core") + B1 .= get_core_bc!(ω, r[1], ρ[1], g[1], μ[1], K[1], "liquid", n; G0=1) + end + + BN, b = get_surface_bc!(R_planet, n) + # BN, b = get_surface_bc!(R_planet, n) + + # storage + R = Vector{Matrix{precc}}(undef, Nr) + + # first layer (n = 1) + dr = rs[2] - rs[1] + + A1 = S * get_A(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], n; G0=G0) * Sinv + A2 = S * get_A(ωs, rs[2], ρs[2], gs[2], μs[2], Ks[2], n; G0=G0) * Sinv + + I6 = Matrix{precc}(I, 6, 6) + + C1 = I6 + 0.5 * dr * A1 + D2 = -I6 + 0.5 * dr * A2 + + # split matrices + C1u, C1l = C1[1:3, :], C1[4:6, :] + D2u, D2l = D2[1:3, :], D2[4:6, :] + + # build S1 and Q1 + S1 = [B1; C1u] # 6×6 + Q1 = [zeros(3,6); D2u] # 6×6 + + # initial recursion + R[1] = -pinv(S1) * Q1 + + # forward recursion + for i in 2:Nr-1 + + dr = rs[i+1] - rs[i] + + A_n = S * get_A(ωs, rs[i], ρs[i], gs[i], μs[i], Ks[i], n; G0=G0) * Sinv + A_np = S * get_A(ωs, rs[i+1], ρs[i+1], gs[i+1], μs[i+1], Ks[i+1], n; G0=G0) * Sinv + + Cn = I6 + 0.5 * dr * A_n + Dnp = -I6 + 0.5 * dr * A_np + + # split + Cn_u, Cn_l = Cn[1:3, :], Cn[4:6, :] + Dnp_u, Dnp_l = Dnp[1:3, :], Dnp[4:6, :] + + # build blocks + Pn = [Cn_l; zeros(3,6)] + Sn = [Dnp_l; Cn_u] + Qn = [zeros(3,6); Dnp_u] + + # recursion + Xn = Pn * R[i-1] + Sn + R[i] = -pinv(Xn) * Qn + end + + # final layer (n = N) + dr = rs[Nr] - rs[Nr-1] + + + A_Nm = S * get_A(ωs, rs[end-1], ρs[end-1], gs[end-1], μs[end-1], Ks[end-1], n; G0=G0) * Sinv + A_N = S * get_A(ωs, rs[end], ρs[end], gs[end], μs[end], Ks[end], n; G0=G0) * Sinv + + CNm = I6 + 0.5 * dr * A_Nm + DN = -I6 + 0.5 * dr * A_N + + CNm_l = CNm[4:6, :] + DN_l = DN[4:6, :] + + PN = [CNm_l; zeros(3,6)] + SN = [DN_l; BN] + + XN = PN * R[Nr-1] + SN + + return XN, R, b + end + + # function solve_radial_system(r, ρ, g, μ, K, ω, n, R_planet; core="inertial") + + # Nr = length(r) + + # # scaling + # # R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(1, 1, 1) + # R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(9.8e6, 4.3e22, 4.3e3) + + # ωs = ω * s0 + # rs = r ./ R0 + # ρs = ρ ./ ρ0 + # gs = g ./ g0 + # μs = μ ./ μ0 + # Ks = K ./ μ0 + + # println(g0) + + # # boundary conditions + # B1 = zeros(precc, 3, 6) + + # # function isbad(A) + # # any(x -> !isfinite(real(x)) || !isfinite(imag(x)), A) + # # end + + # # try + # # B1 .= get_core_bc!(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], core, n) + + # # if isbad(B1) + # # println("NaN/Inf detected in core BC → switching to liquid core") + # # B1 .= get_core_bc!(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], "liquid", n) + # # end + + # # catch e + # # println("Error in core BC → switching to liquid core") + # # B1 .= get_core_bc!(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], "liquid", n) + # # end + # B1 .= get_core_bc!(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], core, n; G0=G0) + # BN, b = get_surface_bc!(R_planet/R0, n) + + # # storage + # R = Vector{Matrix{precc}}(undef, Nr) + + # # first layer (n = 1) + # dr = rs[2] - rs[1] + + # A1 = get_A(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], n; G0=G0) + # A2 = get_A(ωs, rs[2], ρs[2], gs[2], μs[2], Ks[2], n; G0=G0) + + # I6 = Matrix{precc}(I, 6, 6) + + # C1 = I6 + 0.5 * dr * A1 + # D2 = -I6 + 0.5 * dr * A2 + + # # split matrices + # C1u, C1l = C1[1:3, :], C1[4:6, :] + # D2u, D2l = D2[1:3, :], D2[4:6, :] + + # # build S1 and Q1 + # S1 = [B1; C1u] # 6×6 + # Q1 = [zeros(3,6); D2u] # 6×6 + + # # initial recursion + # R[1] = -pinv(S1) * Q1 + + # # forward recursion + # for i in 2:Nr-1 + + # dr = rs[i+1] - rs[i] + + # A_n = get_A(ωs, rs[i], ρs[i], gs[i], μs[i], Ks[i], n; G0=G0) + # A_np = get_A(ωs, rs[i+1], ρs[i+1], gs[i+1], μs[i+1], Ks[i+1], n; G0=G0) + + # Cn = I6 + 0.5 * dr * A_n + # Dnp = -I6 + 0.5 * dr * A_np + + # # split + # Cn_u, Cn_l = Cn[1:3, :], Cn[4:6, :] + # Dnp_u, Dnp_l = Dnp[1:3, :], Dnp[4:6, :] + + # # build blocks + # Pn = [Cn_l; zeros(3,6)] + # Sn = [Dnp_l; Cn_u] + # Qn = [zeros(3,6); Dnp_u] + + # # recursion + # Xn = Pn * R[i-1] + Sn + # R[i] = -pinv(Xn) * Qn + # end + + # # final layer (n = N) + # dr = rs[Nr] - rs[Nr-1] + + + # A_Nm = get_A(ωs, rs[end-1], ρs[end-1], gs[end-1], μs[end-1], Ks[end-1], n; G0=G0) + # A_N = get_A(ωs, rs[end], ρs[end], gs[end], μs[end], Ks[end], n; G0=G0) + + # CNm = I6 + 0.5 * dr * A_Nm + # DN = -I6 + 0.5 * dr * A_N + + # CNm_l = CNm[4:6, :] + # DN_l = DN[4:6, :] + + # PN = [CNm_l; zeros(3,6)] + # SN = [DN_l; BN] + + # XN = PN * R[Nr-1] + SN + + # return XN, R, b + # end + + + function get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1) + + Ic = get_Ic(ω, r, ρ, g, μ, K, type, n; G0=G0, M=6, N=3) + + # Define indices based on Takeuchi & Saito (1972) + # u vectors (Displacements/Potential): U=1, V=2, phi=5 + # s vectors (Stresses/Potential Flux): X=3, Y=4, psi=6 + idx_u = [1, 2, 5] + idx_s = [3, 4, 6] + + Mu = Ic[idx_u, :] + Ms = Ic[idx_s, :] + + # Eq 91 + b = -Mu * pinv(Ms) + + # Construct the 3x6 B matrix + T = eltype(b) + B = zeros(T, 3, 6) + + for i in 1:3 + B[i, idx_u[i]] = 1.0 # Identity for u components + B[i, idx_s[1]] = b[i, 1] # Coefficient for X + B[i, idx_s[2]] = b[i, 2] # Coefficient for Y + B[i, idx_s[3]] = b[i, 3] # Coefficient for psi + end + + return B + end + + + function get_surface_bc!(R, n) + + b = zeros(precc, 6) + b[6] = (2n+1)/R + + # s vectors (Stresses/Potential Flux): X=3, Y=4, psi=6 + idx_s = [3, 4, 6] + + # Construct the 3x6 B matrix + B = zeros(3, 6) + + for i in 1:3 + B[i, idx_s[i]] = 1.0 # Identity for s components + end + + return B, b + end + + + function compute_y_relaxation(r, ρ, g, μ, K, ω, n, R) + + if ω < 1e-6 + core = "liquid" + else + core = "inertial" + end + + # core = "liquid" + + XN, R, b = solve_radial_system(r, ρ, g, μ, K, ω, n, R; core=core) + + Nr = length(r) + T = eltype(XN) + + # allocate as 6 x N matrix + y = Matrix{T}(undef, 6, Nr) + + # solve outer boundary + y[:, Nr] = pinv(XN) * b + + # back-substitution + for i in Nr-1:-1:1 + y[:, i] = R[i] * y[:, i+1] + end + + return y + end + + + """ + compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr) + + Calculate the strain tensor ϵ at a particular radial level. + + # Arguments + - `ϵ::Array{ComplexF64,3}` : 3D array to store the strain tensor at a particular radial level, with dimensions corresponding to latitude, longitude, and the 6 independent components of the strain tensor. + - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 6 components. + - `n::Int` : Tidal degree. + - `rr::prec` : Radius at which to compute the strain tensor. + - `ρr::prec` : Density at radius rr. + - `gr::prec` : Gravity at radius rr. + - `μr::prec` : Shear modulus at radius rr. + - `Ksr::prec` : Bulk modulus at radius rr. + """ + function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr) + + i = 1 + + @views Y = solid1d_relax.Y[i,:,:] + @views dYdθ = solid1d_relax.dYdθ[i,:,:] + @views dYdϕ = solid1d_relax.dYdϕ[i,:,:] + @views Z = solid1d_relax.Z[i,:,:] + @views X = solid1d_relax.X[i,:,:] + + y1 = y[1] + y2 = y[2] + y3 = y[3] + y4 = y[4] + + λr = Ksr .- 2μr/3 + βr = λr + 2μr + + # Compute strain tensor + ϵ[:,:,1] = (-2λr*y1 + n*(n+1)λr*y2 + rr*y3)/(βr*rr) * Y + ϵ[:,:,2] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y + 0.5y2*X) + ϵ[:,:,3] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y - 0.5y2*X) + ϵ[:,:,4] = 0.5/μr * y4 * dYdθ + ϵ[:,:,5] = 0.5/μr * y4 * dYdϕ .* 1.0 ./ sin.(clats) + ϵ[:,:,6] = 0.5 * y2/rr * Z + end + + + """ + function get_heating_profile(y, r, ρ, g, μ, κ, n, ω; lay=nothing) + + Get the radial volumetric heating for solid-body tides and eccentricity forcing, + assuming synchronous rotation. Heating rate is computed with numerical integration + using the solution `y` returned by [`compute_y`](@ref), using Eq. 2.39a/b integrated + over solid angle. The heating profile for a specific layer is specified with `lay`, + otherwise all layers will be caclulated. + + # Arguments + - `y::Array{ComplexF64,4}` : 4D array of the solution vector y across the interior, returned by `compute_y`. + - `r::Array{Float64,2}` : 2D array of layer boundaries. + - `ρ::Array{Float64,1}` : 1D array of layer densities. + - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. + - `μ::Array{Float64,1}` : 1D array of layer shear moduli. + - `κ::Array{Float64,1}` : 1D array of layer bulk moduli. + - `n::Int` : Tidal degree. + - `ω::Float64` : Tidal frequency in radians per second. + + # Keyword Arguments + - `lay::Int=nothing` : If specified, compute the heating profile for only the layer corresponding to this index. Otherwise, compute for all layers. + + # Returns + - `Eμ_tot::Array{Float64,1}` : 1D array of total power dissipated in each primary layer due to shear, in W. + - `Eμ_vol::Array{Float64,2}` : 2D array of angular averaged volumetric heating profiles in W/m^3 for dissipation due to shear, with dimensions corresponding to the secondary layer and primary layer indices. + - `Eκ_tot::Array{Float64,1}` : 1D array of total power dissipated in each primary layer due to compaction, in W. + - `Eκ_vol::Array{Float64,2}` : 2D array of angular averaged volumetric heating profiles in W/m^3 for dissipation due to compaction, with dimensions corresponding to the secondary layer and primary layer indices. + """ + function get_heating_profile(y, r, ρ, g, μ, κ, n, ω) + + dres = deg2rad(solid1d_relax.res) + + clats = solid1d_relax.clats + lons = solid1d_relax.lons + + Nr = length(r) + nlats = length(clats) + nlons = length(lons) + + # strain tensor per radius + ϵ = zeros(ComplexF64, nlats, nlons, 6) + + # outputs + Eμ_vol = zeros(Float64, Nr-1) + Eκ_vol = zeros(Float64, Nr-1) + + Eμ_tot = zeros(Float64, Nr-1) + Eκ_tot = zeros(Float64, Nr-1) + + for i in 1:Nr-1 + + rr = r[i] + dr = r[i+1] - r[i] + + dvol = 4π/3 * (r[i+1]^3 - r[i]^3) + + yrr = y[:, i] + + compute_strain_ten!(ϵ, yrr, n, rr, ρ[i], g[i], μ[i], κ[i]) + + # shear heating + Eμ_loc = sum(abs.(ϵ[:,:,1:3]).^2, dims=3) .+ + 2sum(abs.(ϵ[:,:,4:6]).^2, dims=3) .- + 1/3 .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + + Eμ_loc .*= ω * imag(μ[i]) + + # bulk heating + Eκ_loc = ω/2 * imag(κ[i]) .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + + # angular integration + weight = sin.(clats) + + Eμ_vol[i] = sum(weight .* Eμ_loc * dres^2) * rr^2 * dr / dvol + Eκ_vol[i] = sum(weight .* Eκ_loc * dres^2) * rr^2 * dr / dvol + + # accumulate totals + Eμ_tot[i] = Eμ_vol[i] + Eκ_tot[i] = Eκ_vol[i] + end + + return (Eμ_tot, Eμ_vol), (Eκ_tot, Eκ_vol) + end + +end \ No newline at end of file From 4a4ff2e6ab2027e1b1ce628dd43c8c12f296af05 Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 19 Apr 2026 16:53:12 +0000 Subject: [PATCH 05/36] Updated solid1d_relax. --- src/Obliqua.jl | 390 ++++++------------------------------- src/solid1d_relax.jl | 445 +++++++++++++++++++++---------------------- 2 files changed, 271 insertions(+), 564 deletions(-) diff --git a/src/Obliqua.jl b/src/Obliqua.jl index 4b42660..b90ccc1 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -323,6 +323,9 @@ module Obliqua module_solid = cfg["orbit"]["obliqua"]["module_solid"] ncalc = cfg["orbit"]["obliqua"]["solid"]["ncalc"] + dr_min = cfg["orbit"]["obliqua"]["solid"]["dr_min"] + dr_max = cfg["orbit"]["obliqua"]["solid"]["dr_max"] + core = cfg["orbit"]["obliqua"]["solid"]["core"] bulk_l = cfg["orbit"]["obliqua"]["solid"]["bulk_l"] permea = cfg["orbit"]["obliqua"]["solid"]["permea"] porosity_thresh = cfg["orbit"]["obliqua"]["solid"]["porosity_thresh"] @@ -347,8 +350,6 @@ module Obliqua module_fluid = nothing_if_none(module_fluid) module_mushy = nothing_if_none(module_mushy) - # shear .= ifelse.((shear .< 1e10), 3.2e9, shear) - # convert interior profiles to BigFloat ρ = convert(Vector{prec}, rho) r = convert(Vector{prec}, radius) @@ -372,6 +373,10 @@ module Obliqua throw("CMB radius not at bottom of mantle, did you properly order the interior arrays?") end + # get core mass from core density and radius + ρ_core = convert(prec, cfg["struct"]["core_density"]) + m_core = (4/3)*π*r[1]^3*ρ_core + # find planet radius (m) R = maximum(r) @@ -379,8 +384,8 @@ module Obliqua dv = 4/3 * π * (r[2:end].^3 .- r[1:end-1].^3) dm = dv .* ρ - # cumulative enclosed mass - M_enc = cumsum(dm) + # cumulative enclosed mass, including core mass + M_enc = cumsum(dm) .+ m_core # gravity at each layer radius g = G .* M_enc ./ r[2:end].^2 @@ -419,7 +424,6 @@ module Obliqua t_range = 10 .^ range(p_min, stop=p_max, length=N_σ) # periods [1e3 yr] σ_range = 2π ./ (t_range .* 1e3 .* 365.25 .* 24 .* 3600) # freq [s-1] σ_range = reshape(σ_range, :) - print("Using full spectrum with $N_σ frequencies from $(σ_range[1]) /s to $(σ_range[end]) /s.") end # get forcing frequency dependent complex shear modulus @@ -433,7 +437,7 @@ module Obliqua prf_total = zeros(prec, N_σ, N_layers) # core density for bottom boundary - ρ_mean_lower = convert(prec, cfg["struct"]["core_density"]) + ρ_mean_lower = ρ_core # Rayleigh drag efficiency at fluid core efficiency_seg = efficiency @@ -521,10 +525,10 @@ module Obliqua # calculate tides in solid region prf_seg[iss,:], kT, kL = run_solid1d_relax( σ, ρ_seg, r_seg, - g_seg, η_seg, - μc_seg[:, iss], - κ_seg, R; - ncalc=ncalc, n=n, m=m + η_seg, μc_seg[:, iss], + κ_seg, R, m_core, ρ_core; + dr_min=dr_min, dr_max=dr_max, + n=n, m=m, core=core ) # elseif 1D interior with mush interface and heating profile from strain tensor elseif module_solid=="solid1d-mush" @@ -582,16 +586,6 @@ module Obliqua # don't model mush tides if module_mushy===nothing kT, kL = 0., 0. - elseif module_solid=="solid1d" - # calculate tides in solid region - prf_seg[iss,:], kT, kL = run_solid1d( - σ, ρ_seg, - r_seg, η_seg, - μc_seg[:, iss], - κ_seg, R; - ncalc=ncalc, n=n, m=m - ) - # elseif heating profile from neighbouring segments elseif module_mushy=="interp" # turn on interpolation mode interp_active = true @@ -607,8 +601,9 @@ module Obliqua t_width=t_width, b_width=b_width ) end - # Then model next segment to get heating at the upper interface P_t + else + throw("No compatible mush tides module: $module_mushy.") end # if segment is ice @@ -660,7 +655,7 @@ module Obliqua interp_previous = false # step to next segment - end + end # plot segment k2 spectra plt = plotting.plot_imagk2_spectra(σ_range, abs.(.-imag.(knms_T)), segments; outpath="$OUT_DIR/all_layers_k2.png") @@ -669,6 +664,8 @@ module Obliqua knms_total = copy(knms_T[:, end]) # loop from top (surface) to just above CMB + # This implementation is currently only valid for CMB -> Solid -> Fluid -> Surface layering, + # and would need to be adapted for more complex layering (e.g., fluid under solid). for iseg in reverse(1:length(segments)-1) for i in 1:N_σ knms_total[i] = knms_T[i, iseg] + (1.0 + knms_L[i, iseg]) * knms_total[i] @@ -906,7 +903,7 @@ module Obliqua # masks for liquid and solid regions mask_l = η .< η_l - #mask_s = (η_s .< η .< 1e20) + # mask_s = (η_s .< η .< 1e13) mask_s = η_s .< η # total mantle thickness @@ -1169,8 +1166,6 @@ module Obliqua # Load tidal_solution_L = solid1d.compute_y(rr, g, M, R, y1_4, matrices_R, n; load=true) - plotting.plot_relaxation_solution(tidal_solution_T[1:6, end, :], r[1:end-1], filename="$OUT_DIR/shooting_solution.png") - # get k2 tidal Love Number (complex-valued) k2_T = tidal_solution_T[5, end, end] - 1 k2_L = tidal_solution_L[5, end, end] - 1 @@ -1189,145 +1184,61 @@ module Obliqua return power_prf, k2_T, k2_L end + + """ + run_solid1d_relax(omega, rho, radius, visc, shear, bulk, R, m_core, ρ_core; dr_min=300, dr_max=3000, n=2, m=2) - function resample_profiles(radius, rho, visc, shear, bulk, grav, ncalc) - - r_b = convert(Vector{prec}, radius) - - ρ_old = convert(Vector{prec}, rho) - η_old = convert(Vector{prec}, visc) - μ_old = convert(Vector{precc}, shear) - κ_old = convert(Vector{prec}, bulk) - g_old = convert(Vector{prec}, grav) - - N = length(ρ_old) - - # centers of original grid - r_c = 0.5 .* (r_b[1:N] .+ r_b[2:N+1]) - - rmin = first(r_b) - rmax = last(r_b) - - # detect steep viscosity transitions - logη = log.(η_old) - - dlogη = abs.(diff(logη)) # logarithmic gradient - - threshold = log(1e3) # threshold for significant viscosity jump - - jump_idx = findall(dlogη .> threshold) - - # base stretched grid (without refinement) using a power-law stretching function - s = range(0, 1, length=ncalc+1) - - γ = 3 - f_base(s) = 1 - (1 - s)^γ - - r_base = rmin .+ (rmax - rmin) .* f_base.(s) - - # build refinement weight function - refine = zeros(eltype(r_base), length(r_base)) - - σ = 0.12 * (rmax - rmin) # width of refinement region - - for idx in jump_idx - r_jump = r_c[idx] - - # exponential bump - for i in eachindex(r_base) - if r_base[i] > r_jump - refine[i] += exp(-(r_base[i] - r_jump) / σ) # only above transition - end - end - end - - # normalize refinement - if maximum(refine) > 0 - refine ./= maximum(refine) - end - - # distort grid - α = 0.5 # strength of refinement - - r_new_b = similar(r_base) - - r_new_b[1] = rmin - for i in 2:length(r_base) - dr = r_base[i] - r_base[i-1] - - # shrink spacing near jumps - dr_mod = dr * (1 - α * refine[i]) - - r_new_b[i] = r_new_b[i-1] + dr_mod - end - - # rescale to enforce exact endpoints - r_new_b .= rmin .+ (rmax - rmin) .* (r_new_b .- r_new_b[1]) ./ (r_new_b[end] - r_new_b[1]) - - r_new_c = 0.5 .* (r_new_b[1:end-1] .+ r_new_b[2:end]) - - # interpolate profiles onto new grid using linear interpolation (log–log for viscosity and shear modulus) - itp_ρ = linear_interpolation(r_c, ρ_old, extrapolation_bc=Line()) - itp_g = linear_interpolation(r_c, g_old, extrapolation_bc=Line()) - - itp_η = linear_interpolation(r_c, log.(η_old), extrapolation_bc=Line()) - itp_κ = linear_interpolation(r_c, log.(κ_old), extrapolation_bc=Line()) - - μ_mag = abs.(μ_old) - μ_phase = angle.(μ_old) - - itp_μ_mag = linear_interpolation(r_c, log.(μ_mag), extrapolation_bc=Line()) - itp_μ_phase = linear_interpolation(r_c, μ_phase, extrapolation_bc=Line()) - - # evaluate interpolants on new grid - ρ = itp_ρ.(r_new_c) - g = itp_g.(r_new_c) - - η = exp.(itp_η.(r_new_c)) - κ = exp.(itp_κ.(r_new_c)) - - μ = exp.(itp_μ_mag.(r_new_c)) .* cis.(itp_μ_phase.(r_new_c)) + Use 1D solid tides model with relaxation method to calculate k2 Lovenumbers, and compute 1D heating profile from strain tensor. - return r_new_b, ρ, η, μ, κ, g - end + # Arguments + - `omega::prec` : Forcing frequency range. + - `rho::Array{prec,1}` : Density profile of the planet. + - `radius::Array{prec,1}` : Radial positions of layers, from core to surface. + - `visc::Array{prec,1}` : Viscosity profile of the planet. + - `shear::Array{precc,1}` : Complex shear modulus profile of the planet. + - `bulk::Array{prec,1}` : Bulk modulus profile of the planet. + - `R::prec` : Planet radius. + - `m_core::prec` : Core mass. + - `ρ_core::prec` : Core density. + # Keyword Arguments + - `dr_min::Int=300` : Minimum layer thickness in km. + - `dr_max::Int=3000` : Maximum layer thickness in km. + - `n::Int=2` : Power of the radial factor (goes with (r/a)^{n}, since r< 1e-7 - Nr = trunc(Int, (length(r) - 1) * 100) - elseif omega > 1e-10 - Nr = trunc(Int, (length(r) - 1) * 20) - else - Nr = trunc(Int, (length(r) - 1) * 10) - end # resample profiles onto new grid - r_grid, ρ, η, μ, κ, g = resample_profiles(r, ρ, η, μ, κ, g, Nr) + r_grid, ρ, η, μ, κ, g = solid1d_relax.resample_profiles(r, ρ, η, μ, κ, m_core, dr_min, dr_max) # use cell centers r_centers = 0.5 .* (r_grid[1:end-1] .+ r_grid[2:end]) @@ -1336,35 +1247,17 @@ module Obliqua solid1d_relax.define_spherical_grid(res, n, m) # solve y functions across grid - y = solid1d_relax.compute_y_relaxation(r_centers, ρ, g, μ, κ, omega, n, R) - - # for debugging: plot y-function relaxation solution - # in particular, observe the oscillating behavior near transition zones - # and also near the surface for high frequencies - plotting.plot_relaxation_solution(y, r_centers, - filename="$OUT_DIR/relaxation_solution.png") + y = solid1d_relax.compute_y_relaxation(r_centers, ρ, g, μ, κ, omega, n, R, ρ_core; core=core) # Love numbers k2_T = y[5, end] - 1 k2_L = 0.0 + 0im - # enforce sign consistency with omega - # this should not be necessary, but due to numerical issues k2_T can - # end up in the wrong quadrant of the complex plane - if omega < 0 - k2_T = real(k2_T) + 1im * abs(imag(k2_T)) - else - k2_T = real(k2_T) - 1im * abs(imag(k2_T)) - end - # heating profile - (Eμ, Eκ) = solid1d_relax.get_heating_profile( + (Eμ_tot, Eκ_tot) = solid1d_relax.get_heating_profile( y, r_grid, ρ, g, μ, κ, n, omega ) - Eμ_tot, _ = Eμ - Eκ_tot, _ = Eκ - power_prf = abs.(Eμ_tot .+ Eκ_tot) .* (R ./ maximum(r)).^(2) # interpolate from grid back to original radius points @@ -1377,72 +1270,6 @@ module Obliqua return power_prf, k2_T, k2_L end - - - # """ - # run_solid1d(rho, radius, visc, shear, bulk; ncalc=2000, n=2) - - # Use 1D solid tides model to calculate k2 Lovenumbers. - - # # Arguments - # - `rho::Array{prec,1}` : Density profile of the planet. - # - `radius::Array{prec,1}` : Radial positions of layers, from core to surface. - # - `visc::Array{prec,1}` : Viscosity profile of the planet. - # - `μ_profile::Array{precc,1}` : Complex shear modulus profile of the planet. - # - `bulk::Array{prec,1}` : Bulk modulus profile of the planet. - # - `R::prec` : Planet radius. - - # # Keyword Arguments - # - `ncalc::Int=1000` : Number of sublayers. - # - `n::Int=2` : Power of the radial factor (goes with (r/a)^{n}, since r< !isfinite(real(x)) || !isfinite(imag(x)), A) - end - - try - B1 .= get_core_bc!(ω, r[1], ρ[1], g[1], μ[1], K[1], core, n; G0=1) - - if isbad(B1) - println("NaN/Inf detected in core BC → switching to liquid core") - B1 .= get_core_bc!(ω, r[1], ρ[1], g[1], μ[1], K[1], "liquid", n; G0=1) - end - - catch e - println("Error in core BC → switching to liquid core") - B1 .= get_core_bc!(ω, r[1], ρ[1], g[1], μ[1], K[1], "liquid", n; G0=1) - end - + B1 = get_core_bc!(ω, r[1], ρ_core, g[1], μ[1], K[1], core, n; G0=1) BN, b = get_surface_bc!(R_planet, n) - # BN, b = get_surface_bc!(R_planet, n) # storage R = Vector{Matrix{precc}}(undef, Nr) @@ -540,7 +584,6 @@ module solid1d_relax # final layer (n = N) dr = rs[Nr] - rs[Nr-1] - A_Nm = S * get_A(ωs, rs[end-1], ρs[end-1], gs[end-1], μs[end-1], Ks[end-1], n; G0=G0) * Sinv A_N = S * get_A(ωs, rs[end], ρs[end], gs[end], μs[end], Ks[end], n; G0=G0) * Sinv @@ -558,117 +601,31 @@ module solid1d_relax return XN, R, b end - # function solve_radial_system(r, ρ, g, μ, K, ω, n, R_planet; core="inertial") - - # Nr = length(r) - - # # scaling - # # R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(1, 1, 1) - # R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(9.8e6, 4.3e22, 4.3e3) - - # ωs = ω * s0 - # rs = r ./ R0 - # ρs = ρ ./ ρ0 - # gs = g ./ g0 - # μs = μ ./ μ0 - # Ks = K ./ μ0 - - # println(g0) - - # # boundary conditions - # B1 = zeros(precc, 3, 6) - - # # function isbad(A) - # # any(x -> !isfinite(real(x)) || !isfinite(imag(x)), A) - # # end - - # # try - # # B1 .= get_core_bc!(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], core, n) - - # # if isbad(B1) - # # println("NaN/Inf detected in core BC → switching to liquid core") - # # B1 .= get_core_bc!(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], "liquid", n) - # # end - - # # catch e - # # println("Error in core BC → switching to liquid core") - # # B1 .= get_core_bc!(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], "liquid", n) - # # end - # B1 .= get_core_bc!(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], core, n; G0=G0) - # BN, b = get_surface_bc!(R_planet/R0, n) - - # # storage - # R = Vector{Matrix{precc}}(undef, Nr) - - # # first layer (n = 1) - # dr = rs[2] - rs[1] - - # A1 = get_A(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], n; G0=G0) - # A2 = get_A(ωs, rs[2], ρs[2], gs[2], μs[2], Ks[2], n; G0=G0) - - # I6 = Matrix{precc}(I, 6, 6) - - # C1 = I6 + 0.5 * dr * A1 - # D2 = -I6 + 0.5 * dr * A2 - - # # split matrices - # C1u, C1l = C1[1:3, :], C1[4:6, :] - # D2u, D2l = D2[1:3, :], D2[4:6, :] - - # # build S1 and Q1 - # S1 = [B1; C1u] # 6×6 - # Q1 = [zeros(3,6); D2u] # 6×6 - - # # initial recursion - # R[1] = -pinv(S1) * Q1 - - # # forward recursion - # for i in 2:Nr-1 - - # dr = rs[i+1] - rs[i] - - # A_n = get_A(ωs, rs[i], ρs[i], gs[i], μs[i], Ks[i], n; G0=G0) - # A_np = get_A(ωs, rs[i+1], ρs[i+1], gs[i+1], μs[i+1], Ks[i+1], n; G0=G0) - - # Cn = I6 + 0.5 * dr * A_n - # Dnp = -I6 + 0.5 * dr * A_np - - # # split - # Cn_u, Cn_l = Cn[1:3, :], Cn[4:6, :] - # Dnp_u, Dnp_l = Dnp[1:3, :], Dnp[4:6, :] - - # # build blocks - # Pn = [Cn_l; zeros(3,6)] - # Sn = [Dnp_l; Cn_u] - # Qn = [zeros(3,6); Dnp_u] - - # # recursion - # Xn = Pn * R[i-1] + Sn - # R[i] = -pinv(Xn) * Qn - # end - # # final layer (n = N) - # dr = rs[Nr] - rs[Nr-1] - - - # A_Nm = get_A(ωs, rs[end-1], ρs[end-1], gs[end-1], μs[end-1], Ks[end-1], n; G0=G0) - # A_N = get_A(ωs, rs[end], ρs[end], gs[end], μs[end], Ks[end], n; G0=G0) - - # CNm = I6 + 0.5 * dr * A_Nm - # DN = -I6 + 0.5 * dr * A_N - - # CNm_l = CNm[4:6, :] - # DN_l = DN[4:6, :] - - # PN = [CNm_l; zeros(3,6)] - # SN = [DN_l; BN] + """ + get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1) - # XN = PN * R[Nr-1] + SN + Get the core boundary condition matrix `B` and vector `b` for the solid-body problem. The core + boundary conditions are derived from the requirement that the radial stress at the core-mantle + boundary must balance the tidal potential, and that the tangential stresses must vanish. - # return XN, R, b - # end + # Arguments + - `ω::Float64` : Forcing frequency. + - `r::prec` : Radial position of the core-mantle boundary. + - `ρ::prec` : Density at the core-mantle boundary. + - `g::prec` : Gravity at the core-mantle boundary. + - `μ::precc` : Complex shear modulus at the core-mantle boundary. + - `K::prec` : Bulk modulus at the core-mantle boundary. + - `type::String` : Type of core boundary condition to apply. Options are "liquid" for a fluid core, "solid" for a solid core, and "inertial" for a core with inertial response. + - `n::Int` : Tidal degree. + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + # Returns + - `B::Array{precc,2}` : 3x6 matrix representing the linear relationship between the state variables at the core and the boundary conditions. + - `b::Vector{precc}` : Vector of length 6 representing the inhomogeneous part of the core boundary conditions. + """ function get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1) Ic = get_Ic(ω, r, ρ, g, μ, K, type, n; G0=G0, M=6, N=3) @@ -700,8 +657,25 @@ module solid1d_relax end + """ + get_surface_bc!(R, n) + + Get the surface boundary condition vector `b` and matrix `BN` for the solid-body problem. The surface + boundary conditions are derived from the requirement that the radial stress at the surface must balance + the tidal potential, and that the tangential stresses must vanish. + + # Arguments + - `R::prec` : Planetary radius, used for surface boundary conditions. + - `n::Int` : Tidal degree. + + # Returns + - `BN::Array{precc,2}` : 3x6 matrix representing the linear relationship between the state variables at the surface and the boundary conditions. + - `b::Vector{precc}` : Vector of length 6 representing the inhomogeneous part of the surface boundary conditions. + """ function get_surface_bc!(R, n) + # first three elements of b are zero, last three + # elements are given in the documentation. b = zeros(precc, 6) b[6] = (2n+1)/R @@ -719,17 +693,35 @@ module solid1d_relax end - function compute_y_relaxation(r, ρ, g, μ, K, ω, n, R) + """ + compute_y_relaxation(r, ρ, g, μ, K, ω, n, R, ρ_core; core="liquid") - if ω < 1e-6 - core = "liquid" - else - core = "inertial" - end + Compute the solution `y` to the solid-body problem using a relaxation method. This function performs the + forward-backward relaxation scheme described in the main text of N. Kobayashi (2006), where we first solve + the radial system of ODEs to obtain the solution at the surface, and then perform back-substitution to + compute the solution at all interior points. + + # Arguments + - `r::Vector{prec}` : Vector of radial grid points (layer boundaries). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `n::Int` : Tidal degree. + - `R::prec` : Planetary radius, used for surface boundary conditions. + - `ρ_core::prec` : Density of the core, used for core boundary conditions. - # core = "liquid" + # Keyword Arguments + - `core::String="liquid" : Type of core boundary condition to apply. Options are "liquid" for a fluid core, "solid" for a solid core, and "inertial" for a core with inertial response. + + # Returns + - `y::Matrix{precc}` : 6xN matrix of the solution at all radial grid points, where N is the number of radial layers. Each column corresponds to a radial grid point, and each row corresponds to a state variable (displacements, stresses, potential). + """ + function compute_y_relaxation(r, ρ, g, μ, K, ω, n, R, ρ_core; core="liquid") - XN, R, b = solve_radial_system(r, ρ, g, μ, K, ω, n, R; core=core) + # solve radial system to get surface solution and recursion matrices + XN, R, b = solve_radial_system(r, ρ, g, μ, K, ω, n, R, ρ_core; core=core) Nr = length(r) T = eltype(XN) @@ -816,9 +808,7 @@ module solid1d_relax # Returns - `Eμ_tot::Array{Float64,1}` : 1D array of total power dissipated in each primary layer due to shear, in W. - - `Eμ_vol::Array{Float64,2}` : 2D array of angular averaged volumetric heating profiles in W/m^3 for dissipation due to shear, with dimensions corresponding to the secondary layer and primary layer indices. - `Eκ_tot::Array{Float64,1}` : 1D array of total power dissipated in each primary layer due to compaction, in W. - - `Eκ_vol::Array{Float64,2}` : 2D array of angular averaged volumetric heating profiles in W/m^3 for dissipation due to compaction, with dimensions corresponding to the secondary layer and primary layer indices. """ function get_heating_profile(y, r, ρ, g, μ, κ, n, ω) @@ -865,15 +855,12 @@ module solid1d_relax # angular integration weight = sin.(clats) - Eμ_vol[i] = sum(weight .* Eμ_loc * dres^2) * rr^2 * dr / dvol - Eκ_vol[i] = sum(weight .* Eκ_loc * dres^2) * rr^2 * dr / dvol + Eμ_tot[i] = sum(weight .* Eμ_loc * dres^2) * rr^2 * dr / dvol + Eκ_tot[i] = sum(weight .* Eκ_loc * dres^2) * rr^2 * dr / dvol - # accumulate totals - Eμ_tot[i] = Eμ_vol[i] - Eκ_tot[i] = Eκ_vol[i] end - return (Eμ_tot, Eμ_vol), (Eκ_tot, Eκ_vol) + return Eμ_tot, Eκ_tot end end \ No newline at end of file From 9b76f07f86742c04b72700d6d24eeb3d75e1f465 Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 19 Apr 2026 20:03:53 +0000 Subject: [PATCH 06/36] Added k_range table. Added notebook to generate k_range table. Updated Obliqua to access k_range table. --- examples/hansen_k_table.ipynb | 276 ++++++++++++++++++++++++++++++++++ res/hansen_k_table.nc | Bin 0 -> 32934 bytes src/Hansen.jl | 65 ++++++-- src/Obliqua.jl | 10 +- 4 files changed, 331 insertions(+), 20 deletions(-) create mode 100644 examples/hansen_k_table.ipynb create mode 100644 res/hansen_k_table.nc diff --git a/examples/hansen_k_table.ipynb b/examples/hansen_k_table.ipynb new file mode 100644 index 0000000..894bc07 --- /dev/null +++ b/examples/hansen_k_table.ipynb @@ -0,0 +1,276 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "568eac8b", + "metadata": {}, + "source": [ + "### This notebook can be used to generate the desired k range lookup table for Obliqua. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "8ce50609", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m\u001b[1m Updating\u001b[22m\u001b[39m registry at `~/.julia/registries/General.toml`\n", + "\u001b[32m\u001b[1m Resolving\u001b[22m\u001b[39m package versions...\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m aws_c_cal_jll ───────── v0.9.13+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m aws_c_sdkutils_jll ──── v0.2.4+1\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m aws_c_auth_jll ──────── v0.9.6+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m MPIPreferences ──────── v0.1.12\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m Hwloc_jll ───────────── v2.13.0+1\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m MPIABI_jll ──────────── v0.1.4+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m NetCDF_jll ──────────── v401.1000.0+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m CFTime ──────────────── v0.2.8\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m aws_c_common_jll ────── v0.12.6+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m aws_c_io_jll ────────── v0.26.3+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m s2n_tls_jll ─────────── v1.7.2+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m OpenMPI_jll ─────────── v5.0.11+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m MPICH_jll ───────────── v5.0.1+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m aws_c_http_jll ──────── v0.10.13+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m NCDatasets ──────────── v0.14.15\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m HDF5_jll ────────────── v2.1.2+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m aws_checksums_jll ───── v0.2.10+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m aws_c_compression_jll ─ v0.3.2+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m libaec_jll ──────────── v1.1.6+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m aws_c_s3_jll ────────── v0.11.5+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m mpif_jll ────────────── v0.1.7+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m dlfcn_win32_jll ─────── v1.4.2+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m MPItrampoline_jll ───── v5.5.6+0\n", + "\u001b[32m\u001b[1m Installed\u001b[22m\u001b[39m CommonDataModel ─────── v0.4.3\n", + "\u001b[32m\u001b[1m Compat\u001b[22m\u001b[39m entries added for NCDatasets\n", + "\u001b[32m\u001b[1m Updating\u001b[22m\u001b[39m `~/LovePy/fwlLove.jl/Project.toml`\n", + " \u001b[90m[85f8d34a] \u001b[39m\u001b[92m+ NCDatasets v0.14.15\u001b[39m\n", + "\u001b[32m\u001b[1m Updating\u001b[22m\u001b[39m `~/LovePy/fwlLove.jl/Manifest.toml`\n", + " \u001b[90m[179af706] \u001b[39m\u001b[92m+ CFTime v0.2.8\u001b[39m\n", + " \u001b[90m[1fbeeb36] \u001b[39m\u001b[92m+ CommonDataModel v0.4.3\u001b[39m\n", + " \u001b[90m[3c3547ce] \u001b[39m\u001b[92m+ DiskArrays v0.4.19\u001b[39m\n", + " \u001b[90m[8ac3fa9e] \u001b[39m\u001b[92m+ LRUCache v1.6.2\u001b[39m\n", + " \u001b[90m[3da0fdf6] \u001b[39m\u001b[92m+ MPIPreferences v0.1.12\u001b[39m\n", + " \u001b[90m[85f8d34a] \u001b[39m\u001b[92m+ NCDatasets v0.14.15\u001b[39m\n", + " \u001b[90m[0b7ba130] \u001b[39m\u001b[92m+ Blosc_jll v1.21.7+0\u001b[39m\n", + " \u001b[90m[0234f1f7] \u001b[39m\u001b[92m+ HDF5_jll v2.1.2+0\u001b[39m\n", + " \u001b[90m[e33a78d0] \u001b[39m\u001b[92m+ Hwloc_jll v2.13.0+1\u001b[39m\n", + " \u001b[90m[5ced341a] \u001b[39m\u001b[92m+ Lz4_jll v1.10.1+0\u001b[39m\n", + " \u001b[90m[b5ada748] \u001b[39m\u001b[92m+ MPIABI_jll v0.1.4+0\u001b[39m\n", + " \u001b[90m[7cb0a576] \u001b[39m\u001b[92m+ MPICH_jll v5.0.1+0\u001b[39m\n", + " \u001b[90m[f1f71cc9] \u001b[39m\u001b[92m+ MPItrampoline_jll v5.5.6+0\u001b[39m\n", + " \u001b[90m[9237b28f] \u001b[39m\u001b[92m+ MicrosoftMPI_jll v10.1.4+3\u001b[39m\n", + " \u001b[90m[7243133f] \u001b[39m\u001b[92m+ NetCDF_jll v401.1000.0+0\u001b[39m\n", + " \u001b[90m[fe0851c0] \u001b[39m\u001b[92m+ OpenMPI_jll v5.0.11+0\u001b[39m\n", + "\u001b[33m⌅\u001b[39m \u001b[90m[02c8fc9c] \u001b[39m\u001b[92m+ XML2_jll v2.13.9+0\u001b[39m\n", + " \u001b[90m[a65dc6b1] \u001b[39m\u001b[92m+ Xorg_libpciaccess_jll v0.18.1+0\u001b[39m\n", + "\u001b[33m⌅\u001b[39m \u001b[90m[2b3700d1] \u001b[39m\u001b[92m+ aws_c_auth_jll v0.9.6+0\u001b[39m\n", + " \u001b[90m[70f11efc] \u001b[39m\u001b[92m+ aws_c_cal_jll v0.9.13+0\u001b[39m\n", + " \u001b[90m[73048d1d] \u001b[39m\u001b[92m+ aws_c_common_jll v0.12.6+0\u001b[39m\n", + " \u001b[90m[73a04cd5] \u001b[39m\u001b[92m+ aws_c_compression_jll v0.3.2+0\u001b[39m\n", + " \u001b[90m[3254fc65] \u001b[39m\u001b[92m+ aws_c_http_jll v0.10.13+0\u001b[39m\n", + " \u001b[90m[13c41daa] \u001b[39m\u001b[92m+ aws_c_io_jll v0.26.3+0\u001b[39m\n", + "\u001b[33m⌅\u001b[39m \u001b[90m[bd1f34fb] \u001b[39m\u001b[92m+ aws_c_s3_jll v0.11.5+0\u001b[39m\n", + " \u001b[90m[1282aa60] \u001b[39m\u001b[92m+ aws_c_sdkutils_jll v0.2.4+1\u001b[39m\n", + " \u001b[90m[b2a88e68] \u001b[39m\u001b[92m+ aws_checksums_jll v0.2.10+0\u001b[39m\n", + " \u001b[90m[c4b69c83] \u001b[39m\u001b[92m+ dlfcn_win32_jll v1.4.2+0\u001b[39m\n", + " \u001b[90m[477f73a3] \u001b[39m\u001b[92m+ libaec_jll v1.1.6+0\u001b[39m\n", + " \u001b[90m[337d8026] \u001b[39m\u001b[92m+ libzip_jll v1.11.3+0\u001b[39m\n", + " \u001b[90m[9aeb927a] \u001b[39m\u001b[92m+ mpif_jll v0.1.7+0\u001b[39m\n", + " \u001b[90m[cddc5d3d] \u001b[39m\u001b[92m+ s2n_tls_jll v1.7.2+0\u001b[39m\n", + "\u001b[36m\u001b[1m Info\u001b[22m\u001b[39m Packages marked with \u001b[33m⌅\u001b[39m have new versions available but compatibility constraints restrict them from upgrading. To see why use `status --outdated -m`\n", + "\u001b[92m\u001b[1mPrecompiling\u001b[22m\u001b[39m project...\n", + " 527.5 ms\u001b[32m ✓ \u001b[39m\u001b[90ms2n_tls_jll\u001b[39m\n", + " 541.5 ms\u001b[32m ✓ \u001b[39m\u001b[90mLz4_jll\u001b[39m\n", + " 568.9 ms\u001b[32m ✓ \u001b[39m\u001b[90mdlfcn_win32_jll\u001b[39m\n", + " 588.1 ms\u001b[32m ✓ \u001b[39m\u001b[90mlibaec_jll\u001b[39m\n", + " 625.9 ms\u001b[32m ✓ \u001b[39m\u001b[90mMPIPreferences\u001b[39m\n", + " 623.0 ms\u001b[32m ✓ \u001b[39m\u001b[90mMicrosoftMPI_jll\u001b[39m\n", + " 678.4 ms\u001b[32m ✓ \u001b[39m\u001b[90mXorg_libpciaccess_jll\u001b[39m\n", + " 689.5 ms\u001b[32m ✓ \u001b[39m\u001b[90maws_c_common_jll\u001b[39m\n", + " 1051.0 ms\u001b[32m ✓ \u001b[39m\u001b[90mCFTime\u001b[39m\n", + " 555.0 ms\u001b[32m ✓ \u001b[39m\u001b[90mlibzip_jll\u001b[39m\n", + " 535.0 ms\u001b[32m ✓ \u001b[39m\u001b[90mBlosc_jll\u001b[39m\n", + " 618.9 ms\u001b[32m ✓ \u001b[39m\u001b[90mXML2_jll\u001b[39m\n", + " 479.3 ms\u001b[32m ✓ \u001b[39m\u001b[90maws_c_compression_jll\u001b[39m\n", + " 575.5 ms\u001b[32m ✓ \u001b[39m\u001b[90maws_c_sdkutils_jll\u001b[39m\n", + " 622.8 ms\u001b[32m ✓ \u001b[39m\u001b[90maws_c_cal_jll\u001b[39m\n", + " 628.9 ms\u001b[32m ✓ \u001b[39m\u001b[90maws_checksums_jll\u001b[39m\n", + " 865.2 ms\u001b[32m ✓ \u001b[39m\u001b[90mMPItrampoline_jll\u001b[39m\n", + " 445.2 ms\u001b[32m ✓ \u001b[39m\u001b[90mHwloc_jll\u001b[39m\n", + " 415.0 ms\u001b[32m ✓ \u001b[39m\u001b[90maws_c_io_jll\u001b[39m\n", + " 998.9 ms\u001b[32m ✓ \u001b[39m\u001b[90mCommonDataModel\u001b[39m\n", + " 506.4 ms\u001b[32m ✓ \u001b[39m\u001b[90mMPICH_jll\u001b[39m\n", + " 457.4 ms\u001b[32m ✓ \u001b[39m\u001b[90maws_c_http_jll\u001b[39m\n", + " 796.3 ms\u001b[32m ✓ \u001b[39m\u001b[90mMPIABI_jll\u001b[39m\n", + " 812.3 ms\u001b[32m ✓ \u001b[39m\u001b[90mOpenMPI_jll\u001b[39m\n", + " 415.2 ms\u001b[32m ✓ \u001b[39m\u001b[90maws_c_auth_jll\u001b[39m\n", + " 417.7 ms\u001b[32m ✓ \u001b[39m\u001b[90maws_c_s3_jll\u001b[39m\n", + " 717.0 ms\u001b[32m ✓ \u001b[39m\u001b[90mmpif_jll\u001b[39m\n", + " 631.3 ms\u001b[32m ✓ \u001b[39m\u001b[90mHDF5_jll\u001b[39m\n", + " 809.3 ms\u001b[32m ✓ \u001b[39m\u001b[90mNetCDF_jll\u001b[39m\n", + " 5298.2 ms\u001b[32m ✓ \u001b[39mNCDatasets\n", + " 3708.3 ms\u001b[32m ✓ \u001b[39mObliqua\n", + " 31 dependencies successfully precompiled in 14 seconds. 249 already precompiled.\n" + ] + } + ], + "source": [ + "import Pkg; Pkg.add(\"NCDatasets\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "414bcbc4", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: replacing module Hansen.\n" + ] + } + ], + "source": [ + "using NCDatasets\n", + "\n", + "# Get Obliqua root directory\n", + "ROOT_DIR = abspath(joinpath(dirname(abspath(@__FILE__)),\"../\"))\n", + "RES_DIR = joinpath(ROOT_DIR,\"res/\")\n", + "\n", + "include(joinpath(ROOT_DIR, \"src/Hansen.jl\"))\n", + "import .Hansen" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db636cef", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NetCDF file 'hansen_k_table.nc' created successfully.\n" + ] + } + ], + "source": [ + "# Define parameters\n", + "n_list = [2, 3, 4] # List of tidal degrees 'n' to consider\n", + "ecc_list = range(0.0, stop=0.89, step=0.01) |> collect\n", + "\n", + "# One may wish to adjust the threshold value, which represents the minimum absolute value \n", + "# of Hansen coefficients to be considered significant. A lower threshold will include more \n", + "# k values, while a higher threshold will be more selective. The choice of threshold may depend \n", + "# on the specific application and the desired balance between accuracy and computational efficiency.\n", + "threshold = 1e-3 # Threshold for significant Hansen coefficients\n", + "k_min_global = -500 # Minimum k to consider globally\n", + "k_max_global = 500 # Maximum k to consider globally\n", + "\n", + "# Prepare arrays (use Julia vectors)\n", + "n_vals = Int32[] # Tidal degree 'n'\n", + "m_vals = Int32[] # Tidal order 'm'\n", + "ecc_vals = Float64[] # Eccentricity\n", + "kmin_vals = Int32[] # Minimum k values\n", + "kmax_vals = Int32[] # Maximum k values\n", + "\n", + "# Loop over n, m, and ecc\n", + "for n in n_list\n", + " for m in 0:n\n", + " for e in ecc_list\n", + " k, X = Hansen.get_hansen(e, n, m, k_min_global, k_max_global)\n", + "\n", + " mask = abs.(X) .>= threshold\n", + " k_sig = k[mask]\n", + "\n", + " if isempty(k_sig)\n", + " kmin, kmax = 0, 0\n", + " else\n", + " kmin, kmax = minimum(k_sig), maximum(k_sig)\n", + " end\n", + "\n", + " push!(n_vals, Int32(n))\n", + " push!(m_vals, Int32(m))\n", + " push!(ecc_vals, Float64(e))\n", + " push!(kmin_vals, Int32(kmin))\n", + " push!(kmax_vals, Int32(kmax))\n", + " end\n", + " end\n", + "end\n", + "\n", + "# Create NetCDF file\n", + "ds = NCDataset(joinpath(RES_DIR, \"hansen_k_table.nc\"), \"c\")\n", + "\n", + "# Dimension\n", + "defDim(ds, \"entry\", length(n_vals))\n", + "\n", + "# Variables\n", + "n_var = defVar(ds, \"n\", Int32, (\"entry\",))\n", + "m_var = defVar(ds, \"m\", Int32, (\"entry\",))\n", + "ecc_var = defVar(ds, \"ecc\", Float64, (\"entry\",))\n", + "kmin_var = defVar(ds, \"k_min\", Int32, (\"entry\",))\n", + "kmax_var = defVar(ds, \"k_max\", Int32, (\"entry\",))\n", + "\n", + "# Assign data\n", + "n_var[:] = n_vals\n", + "m_var[:] = m_vals\n", + "ecc_var[:] = ecc_vals\n", + "kmin_var[:] = kmin_vals\n", + "kmax_var[:] = kmax_vals\n", + "\n", + "close(ds)\n", + "\n", + "println(\"NetCDF file '$(joinpath(RES_DIR, \"hansen_k_table.nc\"))' created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "id": "8c92da4b", + "metadata": {}, + "source": [ + "### Obliqua can access this table to determine the k range for Hansen coefficients based on eccentricity and tidal degree/order, through the `get_k_range` function defined in `Hansen.jl`." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "59dcf3ce", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-5, 27)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "Hansen.get_k_range(0.5, 2, 2) # Example usage" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.11.2", + "language": "julia", + "name": "julia-1.11" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/res/hansen_k_table.nc b/res/hansen_k_table.nc new file mode 100644 index 0000000000000000000000000000000000000000..16fbdc43957a6f752d19c9ee06759c0de887425b GIT binary patch literal 32934 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 UuCHfr2}i*|P)?o>rm@QZ1D1CBF8}}l literal 0 HcmV?d00001 diff --git a/src/Hansen.jl b/src/Hansen.jl index c9987e6..d83e015 100644 --- a/src/Hansen.jl +++ b/src/Hansen.jl @@ -5,41 +5,76 @@ module Hansen using FFTW using LinearAlgebra + using NCDatasets export get_hansen + # Get Obliqua root directory + ROOT_DIR = abspath(joinpath(dirname(abspath(@__FILE__)),"../")) + RES_DIR = joinpath(ROOT_DIR,"res/") + # Precision types prec = BigFloat precc = Complex{BigFloat} """ - get_k_range(ecc, n; tol=0.01) + get_k_range(e, n, m) - Compute k-range for Hansen coefficients given eccentricity e and tidal degree n. + Compute k-range for Hansen coefficients given eccentricity e and tidal degree n and order m. # Arguments - `e::Float64` : Eccentricity of the orbit. - `n::Int` : Tidal degree l. - `m::Int` : Tidal order m. - # Keyword Arguments - - `tol::Float64=0.01` : Desired fractional contribution threshold for e^p. - # Returns - `k_min::Int` : Minimum k-index for Hansen coefficients. - `k_max::Int` : Maximum k-index for Hansen coefficients. """ - function get_k_range(e::prec, n::Int, m::Int; tol::Float64=0.01) - # estimate maximum eccentricity power p_max - # power of e where contribution ~ tol - p_max = ceil(Int, log(tol)/log(e+eps())) - - k_min = -(n + p_max) - k_max = n + p_max - - if m == 0 - k_min = 0 + function get_k_range(e, n::Int, m::Int)::Tuple{Int, Int} + + path = joinpath(RES_DIR, "hansen_k_table.nc") + + ds = NCDataset(path, "r") + + n_vals = ds["n"][:] + m_vals = ds["m"][:] + ecc_vals = ds["ecc"][:] + kmin_vals = ds["k_min"][:] + kmax_vals = ds["k_max"][:] + + close(ds) + + # filter matching (n, m) + mask_nm = (n_vals .== n) .& (m_vals .== m) + + if !any(mask_nm) + error("No entries found for n=$n, m=$m in Hansen table.") + end + + ecc_sub = ecc_vals[mask_nm] + kmin_sub = kmin_vals[mask_nm] + kmax_sub = kmax_vals[mask_nm] + + # find smallest ecc >= e + # convert e to Float64 for comparison (table is Float64) + e_val = Float64(e) + + valid = ecc_sub .>= e_val + + if any(valid) + idx_local = findfirst(valid) # assumes ecc_list was sorted ascending + else + # if e is larger than any tabulated value → fallback to largest available + idx_local = argmax(ecc_sub) + end + + k_max = kmax_sub[idx_local] + k_min = kmin_sub[idx_local] + + if m == 0 + k_min = 0 end return k_min, k_max diff --git a/src/Obliqua.jl b/src/Obliqua.jl index b90ccc1..a0c9cbe 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -20,8 +20,8 @@ module Obliqua # Include local jl files include("solid0d.jl") include("solid1d.jl") - include("solid1d_relax.jl") include("solid1d_mush.jl") + include("solid1d_relax.jl") include("fluid0d.jl") include("Hansen.jl") include("load.jl") @@ -30,8 +30,8 @@ module Obliqua # Import submodules import .solid0d import .solid1d - import .solid1d_relax import .solid1d_mush + import .solid1d_relax import .fluid0d import .Hansen import .load @@ -40,8 +40,8 @@ module Obliqua # Export submodules (mostly for autodoc purposes) export solid0d export solid1d - export solid1d_relax export solid1d_mush + export solid1d_relax export fluid0d export Hansen export load @@ -393,7 +393,7 @@ module Obliqua # orbital and axial frequencies if spectrum == "adaptive" # get s range for proper convergence for given eccentricity - s_min_ecc, s_max_ecc = Hansen.get_k_range(ecc, n, m; tol=1e-3) + s_min_ecc, s_max_ecc = Hansen.get_k_range(ecc, n, m) if s_min === nothing s_min = s_min_ecc @@ -521,7 +521,7 @@ module Obliqua ncalc=ncalc, n=n, m=m ) # elseif 1D interior and heating profile from strain tensor - elseif module_solid=="solid1d_relax" + elseif module_solid=="solid1d-relax" # calculate tides in solid region prf_seg[iss,:], kT, kL = run_solid1d_relax( σ, ρ_seg, r_seg, From 348e813c1da220835d792653823c9c46501d6f95 Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 19 Apr 2026 20:05:30 +0000 Subject: [PATCH 07/36] Updated docs, added eqs for relaxation method. --- docs/src/development.md | 1 + docs/src/index.md | 5 +- docs/src/reference/forcing-frequency.md | 2 +- docs/src/reference/solid-phase.md | 380 +++++++++++++++++++++++- 4 files changed, 381 insertions(+), 7 deletions(-) diff --git a/docs/src/development.md b/docs/src/development.md index 5eb9794..cd0321e 100644 --- a/docs/src/development.md +++ b/docs/src/development.md @@ -23,6 +23,7 @@ Modules = [ Obliqua.solid0d, Obliqua.solid1d, Obliqua.solid1d_mush, + Obliqua.solid1d_relax, Obliqua.fluid0d, Obliqua.Hansen ] diff --git a/docs/src/index.md b/docs/src/index.md index c7f80a7..02ca8c0 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,9 +2,6 @@ - - - ``` # Obliqua (Tidal heating model) @@ -14,7 +11,7 @@ A Julia package to calculate the tidal deformation (i.e., tidal Love numbers) of Forked from the [original repository](https://github.com/hamishHay/Love.jl) of Hamish Hay. Distributed under the MIT License. ### Documentation -https://fwl-proteus.readthedocs.io +PROTEUS: [https://fwl-proteus.readthedocs.io](https://fwl-proteus.readthedocs.io) ### Repository structure * `README.md` - This file diff --git a/docs/src/reference/forcing-frequency.md b/docs/src/reference/forcing-frequency.md index b530e6d..f3ff7ac 100644 --- a/docs/src/reference/forcing-frequency.md +++ b/docs/src/reference/forcing-frequency.md @@ -3,7 +3,7 @@ # Forcing Frequency -Given the fact that the tidal forcing magnitude decreases exponentially with harmonnic order, we limit the calculation to only the lowest harmonnic frequency ``n = 2``. Generally, in the limit of ``R_p = a``, it suffices to only consider the quadrupolar harmonic (``n = 2``). Moreover, as we are considering a coplanar geometry, terms with ``\ell = 1`` associated with obliquity tides vanish. The Fourier modes of the second harmonnic have frequencies given by +Given the fact that the tidal forcing magnitude decreases exponentially with harmonnic order, we may limit the calculation to only the lowest harmonnic degree ``n = 2``. Generally, in the limit of ``R_p \ll a``, it suffices to only consider the quadrupolar harmonic (``n = 2``). Nevertheless, Obliqua can also determine higher degree contributions. As of now, we are considering a coplanar geometry. Hence, terms with ``\ell = 1`` associated with obliquity tides vanish. The Fourier modes of the second harmonnic have frequencies given by ```math \sigma = 2\Omega - k n_{\mathrm{orb}}, diff --git a/docs/src/reference/solid-phase.md b/docs/src/reference/solid-phase.md index c267136..23803f5 100644 --- a/docs/src/reference/solid-phase.md +++ b/docs/src/reference/solid-phase.md @@ -127,7 +127,7 @@ q_\ell(r_C) & 0 & 4\pi G \rho_0(r_C^-) \end{pmatrix}. ``` -The assumed normalization implies +Note: the density ``\rho_0`` used here corresponds to the core density. The assumed normalization implies ```math \mathbf{\tilde{I}}_C = \mathbf{S}^{-1} \mathbf{I}_C. @@ -139,7 +139,11 @@ Once the constants ``\mathbf{C}`` are determined, the full perturbed state of th ### (Visco)elastic Solution -We propagate the solution using the so-called propagator matrix (``\pmb{\Pi}_\ell``). The propagator matrix solves the homogeneous differential system +We provide two methods to solve the coupled system of ODEs, the shooting method and the relaxation method. + +#### Shooting method + +propagate the solution using the so-called propagator matrix (``\pmb{\Pi}_\ell``). The propagator matrix solves the homogeneous differential system ```math \frac{d\pmb{\Pi}_\ell(r, r')}{dr} = \pmb{A}_\ell(r)\,\pmb{\Pi}_\ell(r, r'), @@ -200,6 +204,313 @@ or in the normalized form This equation can be solved iteratively, up till the surface to yield the general responds of the interior to any form of tidal- or load induced deformation. +#### Relaxation method + +Alternatively, to solve the above equation numerically, we approximate it using a second-order finite-difference scheme: + +```math +\mathbf{y}_{n+1} - \mathbf{y}_n = \frac{\Delta r}{2} \left( \mathbf{A}_{n+1} \mathbf{y}_{n+1} + \mathbf{A}_n \mathbf{y}_n \right) +``` + +Here, ``\mathbf{y}_n`` and ``\mathbf{A}_n`` are evaluated at radius ``r = r_n`` for ``n = 1, \dots, N``, and ``\Delta r = r_{n+1} - r_n`` is the grid spacing. + +The grid is non-uniform and follows an exponential distribution, with the largest spacing ``\Delta r_{\max}`` located just above the core–mantle boundary (CMB), and the smallest spacing ``\Delta r_{\min}`` near the surface. + +The total number of nodes is given by: + +```math +N = \mathrm{round} \left( +\frac{R_E - R_c}{\Delta r_{\min}} \cdot \frac{\alpha}{\exp(\alpha) - 1} +\right) +``` + +where ``R_c`` is the core radius and ``\alpha = \ln\left(\frac{\Delta r_{\max}}{\Delta r_{\min}}\right)``. + +The nodal positions are then defined as: + +```math +r_i = R_E + (R_c - R_E) +\left( +\frac{ +\exp\left( \alpha \frac{N - i}{N - 1} \right) - 1 +}{ +\exp(\alpha) - 1 +} +\right), \quad i = 1, \ldots, N +``` + +Rearranging the finite-difference equation yields a linear relation between adjacent nodes: + +```math +\mathbf{C}_n \mathbf{y}_n + \mathbf{D}_{n+1} \mathbf{y}_{n+1} = \mathbf{0} +``` + +where the matrices ``\mathbf{C}_n`` and ``\mathbf{D}_{n+1}`` are defined as: + +```math +\mathbf{C}_n = \mathbf{I} + \frac{\Delta r}{2} \mathbf{A}_n +``` + +```math +\mathbf{D}_{n+1} = -\mathbf{I} + \frac{\Delta r}{2} \mathbf{A}_{n+1} +``` + +The columns of ``\mathbf{I}_C`` represent the three elementary solutions of an isotropic, homogeneous elastic–gravitational sphere. The vector ``\mathbf{C}`` contains the integration constants: + +```math +\mathbf{C} = (C_1, C_2, C_3)^T +``` + +These constants are determined from the boundary conditions at the planetary surface, applied to the stress components of the spheroidal solution. The general solution can be written as a linear combination of the elementary solutions ``\mathbf{y}^{(i)}``, which are regular at the centre: + +```math +\mathbf{y}_l(r_C^+) = C_1 \mathbf{y}^{(1)} + C_2 \mathbf{y}^{(2)} + C_3 \mathbf{y}^{(3)} +``` + +Eliminating the coefficients ``C_i`` leads to: + +```math +\begin{pmatrix} +U \\ +V \\ +\phi +\end{pmatrix} +- +\begin{pmatrix} +U^{(1)} & U^{(2)} & U^{(3)} \\ +V^{(1)} & V^{(2)} & V^{(3)} \\ +\phi^{(1)} & \phi^{(2)} & \phi^{(3)} +\end{pmatrix} +\begin{pmatrix} +X^{(1)} & X^{(2)} & X^{(3)} \\ +Y^{(1)} & Y^{(2)} & Y^{(3)} \\ +\psi^{(1)} & \psi^{(2)} & \psi^{(3)} +\end{pmatrix}^{-1} +\begin{pmatrix} +X \\ +Y \\ +\psi +\end{pmatrix} += 0 +``` + +Here, ``U^{(i)}, V^{(i)}, \phi^{(i)}, X^{(i)}, Y^{(i)}, \psi^{(i)}`` are the components of the (i)-th elementary solution. + +This can be written compactly as a boundary condition: + +```math +\mathbf{B} \, \mathbf{y} = 0 +``` + +where ``\mathbf{B}`` is a ``3 \times 6`` matrix: + +```math +\mathbf{B} = +\begin{pmatrix} +1 & 0 & b_{11} & b_{12} & 0 & b_{13} \\ +0 & 1 & b_{21} & b_{22} & 0 & b_{23} \\ +0 & 0 & b_{31} & b_{32} & 1 & b_{33} +\end{pmatrix} +``` + +The coefficients ``b_{ij}`` are defined as: + +```math +\begin{pmatrix} +b_{11} & b_{12} & b_{13} \\ +b_{21} & b_{22} & b_{23} \\ +b_{31} & b_{32} & b_{33} +\end{pmatrix} += +- +\begin{pmatrix} +U^{(1)} & U^{(2)} & U^{(3)} \\ +V^{(1)} & V^{(2)} & V^{(3)} \\ +\phi^{(1)} & \phi^{(2)} & \phi^{(3)} +\end{pmatrix} +\begin{pmatrix} +X^{(1)} & X^{(2)} & X^{(3)} \\ +Y^{(1)} & Y^{(2)} & Y^{(3)} \\ +\psi^{(1)} & \psi^{(2)} & \psi^{(3)} +\end{pmatrix}^{-1} +``` + +Notably, the second and third columns are swapped compared to Kobayashi (2007) for consistency with ``\mathbf{A}``. The difference equation together with the boundary conditions (given in the next sections) define the complete system of oscillations: + +```math +\begin{pmatrix} +B_1 \\ +C_1 & D_2 \\ +& C_2 & D_3 \\ +& & \ddots & \ddots \\ +& & & C_{N-1} & D_N \\ +& & & & B_N +\end{pmatrix} +\begin{pmatrix} +y_1 \\ +y_2 \\ +y_3 \\ +\vdots \\ +y_{N-1} \\ +y_N +\end{pmatrix} += +\begin{pmatrix} +0 \\ +0 \\ +0 \\ +\vdots \\ +0 \\ +b +\end{pmatrix} +``` + +Here, ``B_1`` and ``B_N`` represent the inner and outer boundary conditions, respectively, while ``C_n`` and ``D_n`` correspond to the discretised equations of motion, which depend on the forcing frequency ``\omega`` and the interior structure of the mantle. All unspecified entries in the global matrix are zero. + +The structure of the system can be exploited to reduce computational cost. Following a Henyey-type relaxation method (e.g. Unno et al. 1989), the system can be rewritten in block form: + +```math +\begin{pmatrix} +S_1 & Q_1 \\ +P_2 & S_2 & Q_2 \\ +& \ddots & \ddots & \ddots \\ +& & P_{N-1} & S_{N-1} & Q_{N-1} \\ +& & & P_N & S_N +\end{pmatrix} +\begin{pmatrix} +y_1 \\ +y_2 \\ +\vdots \\ +y_{N-1} \\ +y_N +\end{pmatrix} += +\begin{pmatrix} +0 \\ +0 \\ +\vdots \\ +0 \\ +b +\end{pmatrix} +``` + +In this formulation, ``S_1`` contains contributions from ``B_1`` and the upper block of ``C_1``. The upper and lower parts of ``Q_1`` consist of a zero block and the upper part of ``D_2``, respectively, while ``P_2`` contains the lower part of ``C_1`` combined with a zero block. + +The submatrices are defined as: + +```math +P_n \equiv +\begin{bmatrix} +C^{l}_{n-1} \\ +0 +\end{bmatrix}, +\quad n = 2, \ldots, N +``` + +```math +S_n \equiv +\begin{bmatrix} +D^{l}_n \\ +C^{u}_n +\end{bmatrix}, +\quad n = 2, \ldots, N-1 +``` + +```math +Q_n \equiv +\begin{bmatrix} +0 \\ +D^{u}_{n+1} +\end{bmatrix}, +\quad n = 1, \ldots, N-1 +``` + +Here, superscripts ``u`` and ``l`` denote the upper and lower blocks of the corresponding matrices. The matrix ``S_N`` consists of the lower part of ``D_N`` combined with ``B_N``. Each of the block matrices ``P_n``, ``S_n``, and ``Q_n`` has size ``6 \times 6``. Both formulations are equivalent, and the full system corresponds to a banded matrix of size ``6N \times 6N`` for spheroidal oscillations in a solid sphere. Because coupling is restricted to nearest neighbours, the system can be solved efficiently using a recursive scheme. + +The first block row reads: + +```math +S_1 y_1 + Q_1 y_2 = 0 +``` + +which gives: + +```math +y_1 = -S_1^{-1} Q_1 y_2 +``` + +Substituting into the next block row yields: + +```math +\left(-P_2 S_1^{-1} Q_1 + S_2 \right) y_2 + Q_2 y_3 = 0 +``` + +so that: + +```math +y_2 = -\left(-P_2 S_1^{-1} Q_1 + S_2 \right)^{-1} Q_2 y_3 +``` + +Proceeding recursively, we obtain: + +```math +y_n = R_n y_{n+1} +``` + +with + +```math +R_n = -X_n^{-1} Q_n +``` + +and + +```math +X_n = P_n R_{n-1} + S_n +``` + +The initial condition is: + +```math +R_1 = -S_1^{-1} Q_1 +``` + +Finally, the last block equation becomes: + +```math +X_N y_N = b = +\begin{pmatrix} +0 \\ +0 \\ +\frac{2n+1}{R} +\end{pmatrix} +``` + +with + +```math +X_N = P_N R_{N-1} + S_N +``` + +This recursive formulation reduces the computational cost from solving a full ``6N \times 6N`` system to a linear sequence of ``N`` matrix operations. + +Two different propagation operators appear in the formulation. From the differential equations, one obtains the local transfer relation: + +```math +y_n = -C_n^{-1} D_{n+1} y_{n+1} \equiv \tilde{R}_n y_{n+1} +``` + +Although this resembles the recursive relation above, the operators are fundamentally different. The matrix ``\tilde{R}_n`` is constructed directly from the differential operator. Since both ``C_n`` and ``D_n`` are invertible, ``\tilde{R}_n`` is also invertible, allowing propagation in both directions along the grid. + +In contrast, the relaxation operator is: + +```math +y_n = R_n y_{n+1}, \qquad R_n = -X_n^{-1} Q_n +``` + +Here, ``Q_n`` is singular by construction, so ``R_n`` is not invertible. This enforces a one-way recursion from ``n+1`` to ``n``. Rather than being a limitation, this structure ensures numerical stability by enforcing simultaneous compatibility with both the differential equations and boundary conditions. + + --- ### Surface Boundary Conditions @@ -267,3 +578,68 @@ To solve this system we thus need only provide ``\pmb{P}_1\,\pmb{y}(a^-)``, we w --- +Similarly, for the relaxation method, we can impose a free-surface outer boundary. The boundary conditions can be written in the same form as the inner system with + +```math +\mathbf{B} = +\begin{pmatrix} +0 & 0 & 1 & 0 & 0 & 0 \\ +0 & 0 & 0 & 1 & 0 & 0 \\ +0 & 0 & 0 & 0 & 0 & 1 +\end{pmatrix} +``` + +This corresponds to free-surface conditions applied to ``y_1``, ``y_2``, and ``y_5``, while the remaining components are constrained accordingly. In the general case, let ``P`` denote the external atmospheric pressure, ``\zeta`` the surface mass load, ``U`` an external potential, and ``\tau`` the tangential traction component. Then the boundary conditions for a spherical harmonic degree ``n`` can be written as: + +```math +\begin{cases} +y_{3n}(a) = -g_e \zeta_n - P_n \\ +y_{4n}(a) = \tau_n \\ +y_{6n}(a) + \frac{n+1}{a} y_{5n}(a) += \frac{2n+1}{a} U_n + 4\pi G \zeta_n +\end{cases} +``` + +where ``g_e`` is the surface gravity magnitude. + +Farrell (1972) and Longman (1962) showed that the surface mass load can alternatively be expressed as an equivalent external potential ``U'``, such that: + +```math +\zeta_n = \frac{2n+1}{4\pi G a} U'_n +``` + +We distinguish between Tidal Love numbers (response to an external potential perturbation ``U``) and Load Love numbers (response to a surface mass load ``\zeta`` (or equivalently ``U'``)). These can be obtained by solving the system with the following choices: + +* TLN: ``(U, U', \tau, P) = (1, 0, 0, 0)`` +* LLN: ``(U, U', \tau, P) = (0, 1, 0, 0)`` + +When the outer boundary is driven by tides from an external perturber, the system can be written as: + +```math +\mathbf{B} \mathbf{y} = \mathbf{b} +``` + +with + +```math +\mathbf{b} = +\begin{pmatrix} +0 \\ +0 \\ +\frac{2n+1}{R} +\end{pmatrix} +``` + +This expression follows from a simplified form of the general boundary conditions, assuming the outer radius ``a = R`` (planetary radius) and neglecting higher-order corrections. + +This corresponds to the free-surface conditions: + +```math +X(R) = 0, \qquad Y(R) = 0, \qquad \psi(R) = \frac{2n+1}{R} +``` + +applied at ``r = R`` the planetary radius. + + +--- + From 6a9d91f9a42dd8be307d0bd7867f233c70776231 Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 19 Apr 2026 20:20:40 +0000 Subject: [PATCH 08/36] Added core mass to other soli1d modules. --- src/Obliqua.jl | 14 ++++++++------ src/solid1d.jl | 12 ++++++------ src/solid1d_mush.jl | 7 +++++-- src/solid1d_relax.jl | 8 ++++---- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/Obliqua.jl b/src/Obliqua.jl index a0c9cbe..ea77e99 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -517,7 +517,7 @@ module Obliqua σ, ρ_seg, r_seg, η_seg, μc_seg[:, iss], - κ_seg, R; + κ_seg, R, m_core; ncalc=ncalc, n=n, m=m ) # elseif 1D interior and heating profile from strain tensor @@ -535,7 +535,7 @@ module Obliqua prf_seg[iss,:], kT, kL = run_solid1d_mush( σ, ρ_seg, r_seg, η_seg, μc_seg[:, iss], - κ_seg, ϕ_seg, R; + κ_seg, ϕ_seg, R, m_core; ncalc, n, m, visc_l, bulk_l, permea, porosity_thresh ) @@ -1135,7 +1135,8 @@ module Obliqua visc::Array{prec,1}, shear::Array{precc,1}, bulk::Array{prec,1}, - R::prec; + R::prec, + m_core::prec; ncalc::Int=2000, n::Int=2, m::Int=2 @@ -1153,7 +1154,7 @@ module Obliqua rr = solid1d.expand_layers(r, nr=convert(Int,div(ncalc,length(η)))) # get gravity at each layer - g = solid1d.get_g(rr, ρ); + g = solid1d.get_g(rr, ρ, m_core) # create grid solid1d.define_spherical_grid(res, n, m) @@ -1308,7 +1309,8 @@ module Obliqua shear::Array{precc,1}, bulk::Array{prec,1}, phi::Array{prec,1}, - R::prec; + R::prec, + m_core::prec; ncalc::Int=2000, n::Int=2, m::Int=2, @@ -1361,7 +1363,7 @@ module Obliqua rr = solid1d_mush.expand_layers(r, nr=convert(Int,div(ncalc,length(η)))) # get gravity at each layer - g = solid1d_mush.get_g(rr, ρ); + g = solid1d_mush.get_g(rr, ρ, m_core) # create grid solid1d_mush.define_spherical_grid(res, n, m) diff --git a/src/solid1d.jl b/src/solid1d.jl index fafc09e..c97d5c1 100644 --- a/src/solid1d.jl +++ b/src/solid1d.jl @@ -50,13 +50,14 @@ module solid1d """ - get_g(r, ρ) + get_g(r, ρ, m_core) Compute the radial gravity structure associated with a density profile `r` at intervals given by `r`. # Arguments - `r::Array{Float64,2}` : 2D array of layer boundaries. - `ρ::Array{Float64,1}` : 1D array of layer densities. The length of `ρ` must be equal to the number of columns in `r`. + - `m_core::Float64` : Mass of the planetary core. # Returns - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. The dimensions of `g` are the same as `r`. @@ -65,7 +66,7 @@ module solid1d `r` must be be a 2D array, with index 1 representing the top radius of secondary layers, and index 2 representing the top radius of primary layers. """ - function get_g(r, ρ) + function get_g(r, ρ, m_core) g = zeros(Float64, size(r)) M = zeros(Float64, size(r)) @@ -73,6 +74,8 @@ module solid1d M[2:end,i] = 4.0/3.0 * π .* diff(r[:,i].^3) .* ρ[i] end + M[2,1] += m_core + g[2:end,:] .= G*accumulate(+,M[2:end,:]) ./ r[2:end,:].^2 g[1,2:end] = g[end,1:end-1] @@ -581,10 +584,7 @@ module solid1d M[1, :] .= y1_4[3,:,end,end] # Row 1 - Radial Stress M[2, :] .= y1_4[4,:,end,end] # Row 2 - Tangential Stress M[3, :] .= y1_4[6,:,end,end] # Row 3 - Potential Stress - - println("Forcing frequency ω = ", ω) - println("cond(M) = ", cond(M)) - + return M, y1_4, matrices_R_sublayer end diff --git a/src/solid1d_mush.jl b/src/solid1d_mush.jl index f949dd4..0435e6c 100644 --- a/src/solid1d_mush.jl +++ b/src/solid1d_mush.jl @@ -51,13 +51,14 @@ module solid1d_mush """ - get_g(r, ρ) + get_g(r, ρ, m_core) Compute the radial gravity structure associated with a density profile `r` at intervals given by `r`. # Arguments - `r::Array{Float64,2}` : 2D array of layer boundaries. - `ρ::Array{Float64,1}` : 1D array of layer densities. The length of `ρ` must be equal to the number of columns in `r`. + - `m_core::Float64` : Mass of the planetary core. # Returns - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. The dimensions of `g` are the same as `r`. @@ -66,13 +67,15 @@ module solid1d_mush `r` must be be a 2D array, with index 1 representing the top radius of secondary layers, and index 2 representing the top radius of primary layers. """ - function get_g(r, ρ) + function get_g(r, ρ, m_core) g = zeros(Float64, size(r)) M = zeros(Float64, size(r)) for i in 1:size(r)[2] M[2:end,i] = 4.0/3.0 * π .* diff(r[:,i].^3) .* ρ[i] end + + M[2,1] += m_core g[2:end,:] .= G*accumulate(+,M[2:end,:]) ./ r[2:end,:].^2 g[1,2:end] = g[end,1:end-1] diff --git a/src/solid1d_relax.jl b/src/solid1d_relax.jl index f599b5d..d80da55 100644 --- a/src/solid1d_relax.jl +++ b/src/solid1d_relax.jl @@ -154,7 +154,7 @@ module solid1d_relax - `m::Int` : Tidal order. # Notes - The grid is internal to solid1d, but can be accessed with + The grid is internal to solid1d_relax, but can be accessed with solid1d_relax.clats[:] # colatitude grid solid1d_relax.lons[:] # longitude grid @@ -789,9 +789,9 @@ module solid1d_relax Get the radial volumetric heating for solid-body tides and eccentricity forcing, assuming synchronous rotation. Heating rate is computed with numerical integration - using the solution `y` returned by [`compute_y`](@ref), using Eq. 2.39a/b integrated - over solid angle. The heating profile for a specific layer is specified with `lay`, - otherwise all layers will be caclulated. + using the solution `y`, using Eq. 2.39a/b integrated over solid angle. The heating + profile for a specific layer is specified with `lay`, otherwise all layers will be + caclulated. # Arguments - `y::Array{ComplexF64,4}` : 4D array of the solution vector y across the interior, returned by `compute_y`. From 84b4cfadff8435afed62f17e578943dc9b98f618 Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 19 Apr 2026 20:21:37 +0000 Subject: [PATCH 09/36] Updated config files. --- res/config/all_options.toml | 24 +++++++++++++++--------- test/test.toml | 8 +++++++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/res/config/all_options.toml b/res/config/all_options.toml index 1568d23..981a1af 100644 --- a/res/config/all_options.toml +++ b/res/config/all_options.toml @@ -41,15 +41,15 @@ version = "1.0" # Version of this configuration file visc_l = 1e2 # Pure liquid viscosity [Pa s] visc_lus = 5e5 # Liquidus viscosity to use [Pa s] visc_s = 1e22 # Pure solid visosity [Pa s] - visc_sus = 5e13 # Solidus viscosity to use [Pa s] + visc_sus = 5e5 # Solidus viscosity to use [Pa s] n = 2 # Power of the radial factor (goes with (r/a)^{n}, since r<) or region of interest (<"adaptive">) - N_sigma = 50 # Number probe frequencies to evaluate k2 at - p_min = -20 # Minimum period for orbital and axial frequencies [log(kyr)] i.e. forcing occurion over minimal 1e-17 yr interval - p_max = 6 # Maximum period for orbital and axial frequencies [log(kyr)] i.e. forcing occurion over maximal 1e9 yr interval - s_min = "none" # -30 Minimum tidal mode range (k is the Fourier index in mean anomaly), set to obtain tidal mode range from eccentricity relation - s_max = "none" # 40 Maximum tidal mode range (k is the Fourier index in mean anomaly), set to obtain tidal mode range from eccentricity relation + N_sigma = 10 # Number probe frequencies to evaluate k2 at + p_min = -6.7 # Minimum period for orbital and axial frequencies [log(kyr)] i.e. forcing occurion over minimal 1e-17 yr interval + p_max = 4 # Maximum period for orbital and axial frequencies [log(kyr)] i.e. forcing occurion over maximal 1e9 yr interval + s_min = "none" # Minimum tidal mode range (k is the Fourier index in mean anomaly), set to obtain tidal mode range from eccentricity relation + s_max = "none" # Maximum tidal mode range (k is the Fourier index in mean anomaly), set to obtain tidal mode range from eccentricity relation material = "andrade" # Material type for complex shear modulus ("andrade", "maxwell") alpha = 0.3 # Power-law exponent, only for Andrade rheology (free parameter) @@ -63,16 +63,19 @@ version = "1.0" # Version of this configuration file # - "solid1d" : Radially resolved solid model. Solves the tidal response through a # layered solid interior. Allows realistic rigidity, density, and # rheology profiles. + # - "solid1d-relax" : Same as solid1d, but uses the relaxation method to obtain a solution + # (instead of the shooting method) which is generally more stable. + # However, this method is generally slower. # - "solid1d-mush" : Same as solid1d but includes a partially molten/porous ("mushy") region # with evolving rheology between solid and liquid behavior. - module_solid = "solid1d" + module_solid = "solid1d-relax" # Mushy layer model # Controls how partially molten regions are treated. # - "none" : No explicit mush layer treatment. # - "interp" : Rheological properties transition smoothly between solid and fluid # regimes using interpolation across the melt fraction range. - module_mushy = "interp" + module_mushy = "none" # Fluid layer model # Controls how the fluid (e.g., magma ocean or atmosphere) responds to tidal forcing. @@ -84,7 +87,10 @@ version = "1.0" # Version of this configuration file module_fluid = "fluid1d" [orbit.obliqua.solid] - ncalc = 1000 # Number of sublayers to use for dim=1 solid tides + ncalc = 100 # Number of sublayers to use for dim=1 solid tides (for shooting method) + dr_min = 300 # Minimum spacing between grid points (for relaxation method) [m] + dr_max = 3000 # Maximum spacing between grid points (for relaxation method) [m] + core = "liquid" # Core solution to use as CMB boundary condition, options: "liquid", "solid", "inertial" bulk_l = 1e9 # Liquid bulk modulus [Pa] permea = 1e-7 # Permeability [m^2] porosity_thresh = 1e-5 # Porosity threshold, below this value no mush. [dimensionless] diff --git a/test/test.toml b/test/test.toml index dbf3e70..567b0a0 100644 --- a/test/test.toml +++ b/test/test.toml @@ -60,6 +60,9 @@ version = "1.0" # Version of this configuration file # - "solid1d" : Radially resolved solid model. Solves the tidal response through a # layered solid interior. Allows realistic rigidity, density, and # rheology profiles. + # - "solid1d-relax" : Same as solid1d, but uses the relaxation method to obtain a solution + # (instead of the shooting method) which is generally more stable. + # However, this method is generally slower. # - "solid1d-mush" : Same as solid1d but includes a partially molten/porous ("mushy") region # with evolving rheology between solid and liquid behavior. module_solid = "solid1d" @@ -81,7 +84,10 @@ version = "1.0" # Version of this configuration file module_fluid = "fluid1d" [orbit.obliqua.solid] - ncalc = 1000 # Number of sublayers to use for dim=1 solid tides + ncalc = 100 # Number of sublayers to use for dim=1 solid tides (for shooting method) + dr_min = 300 # Minimum spacing between grid points (for relaxation method) [m] + dr_max = 3000 # Maximum spacing between grid points (for relaxation method) [m] + core = "liquid" # Core solution to use as CMB boundary condition, options: "liquid", "solid", "inertial" bulk_l = 1e9 # Liquid bulk modulus [Pa] permea = 1e-7 # Permeability [m^2] porosity_thresh = 1e-5 # Porosity threshold, below this value no mush. [dimensionless] From 89f65fdfbd51489958ee8a768c1da7c40d69d2c8 Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 27 Apr 2026 19:43:26 +0000 Subject: [PATCH 10/36] Updated all solid1d models: new general surface boundary, fixed somewhat random scaling of heating. Added new solid1d_mush_relax, still wip. --- src/Obliqua.jl | 266 +++++- src/solid1d.jl | 193 ++-- src/solid1d_mush.jl | 34 +- src/solid1d_mush_relax.jl | 1831 +++++++++++++++++++++++++++++++++++++ src/solid1d_relax.jl | 383 +++++--- 5 files changed, 2411 insertions(+), 296 deletions(-) create mode 100644 src/solid1d_mush_relax.jl diff --git a/src/Obliqua.jl b/src/Obliqua.jl index ea77e99..ffbd0e9 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -20,8 +20,10 @@ module Obliqua # Include local jl files include("solid0d.jl") include("solid1d.jl") + include("solid1d_qr.jl") include("solid1d_mush.jl") include("solid1d_relax.jl") + include("solid1d_mush_relax.jl") include("fluid0d.jl") include("Hansen.jl") include("load.jl") @@ -30,8 +32,10 @@ module Obliqua # Import submodules import .solid0d import .solid1d + import .solid1d_qr import .solid1d_mush import .solid1d_relax + import .solid1d_mush_relax import .fluid0d import .Hansen import .load @@ -517,7 +521,16 @@ module Obliqua σ, ρ_seg, r_seg, η_seg, μc_seg[:, iss], - κ_seg, R, m_core; + κ_seg, R, m_core, ρ_core; + ncalc=ncalc, n=n, m=m + ) + elseif module_solid=="solid1d-qr" + # calculate tides in solid region + prf_seg[iss,:], kT, kL = run_solid1d_qr( + σ, ρ_seg, + r_seg, η_seg, + μc_seg[:, iss], + κ_seg, R, m_core, ρ_core; ncalc=ncalc, n=n, m=m ) # elseif 1D interior and heating profile from strain tensor @@ -535,9 +548,18 @@ module Obliqua prf_seg[iss,:], kT, kL = run_solid1d_mush( σ, ρ_seg, r_seg, η_seg, μc_seg[:, iss], - κ_seg, ϕ_seg, R, m_core; - ncalc, n, m, visc_l, bulk_l, - permea, porosity_thresh + κ_seg, ϕ_seg, R, m_core, ρ_core; + ncalc=ncalc, n=n, m=m, core=core, visc_l=visc_l, bulk_l=bulk_l, + permea=permea, porosity_thresh=porosity_thresh + ) + elseif module_solid=="solid1d-mush-relax" + prf_seg[iss,:], kT, kL = run_solid1d_mush_relax( + σ, ρ_seg, r_seg, + η_seg, μc_seg[:, iss], + κ_seg, ϕ_seg, R, m_core, ρ_core; + dr_min=dr_min, dr_max=dr_max, + n=n, m=m, core=core, visc_l=visc_l, bulk_l=bulk_l, + permea=permea, porosity_thresh=porosity_thresh ) else throw("No compatible solid tides module: $module_solid.") @@ -1106,9 +1128,10 @@ module Obliqua """ - run_solid1d(omega, rho, radius, visc, shear, bulk; ncalc=2000, n=2, m=2) + run_solid1d(omega, rho, radius, visc, shear, bulk, R, m_core, ρ_core; ncalc=2000, n=2, m=2, core="liquid") Use 1D solid tides model to calculate k2 Lovenumbers, and compute 1D heating profile from strain tensor. + This method ignores inertia effects, since they break the numerical stability. # Arguments - `omega::prec` : Forcing frequency range. @@ -1118,11 +1141,14 @@ module Obliqua - `μ_profile::Array{precc,1}` : Complex shear modulus profile of the planet. - `bulk::Array{prec,1}` : Bulk modulus profile of the planet. - `R::prec` : Planet radius. + - `m_core::prec` : Core mass. + - `ρ_core::prec` : Core density. # Keyword Arguments - `ncalc::Int=2000` : Number of sublayers. - `n::Int=2` : Power of the radial factor (goes with (r/a)^{n}, since r< porosity_thresh) + + # 2. Find the start of the continuous region that reaches the surface + # We look for the first index such that all subsequent indices are also > threshold + if isempty(ii_all) + ii = nothing # No mushy layer found + else + # Logic: If it's a single continuous layer at the top, + # the start index will be the first one in a contiguous sequence ending at Nr. + if ii_all[end] == length(ϕ) + # Work backwards from the end to find the first break in continuity + continuous_from_top = findall(diff(ii_all) .!= 1) + if isempty(continuous_from_top) + ii = ii_all[1] # The whole thing is mushy + else + # The start is the index right after the last "break" in the sequence + ii = ii_all[continuous_from_top[end] + 1] + end + else + ii = nothing # Porosity > threshold, but it doesn't reach the surface + end + end + + # If the porosity = 0, throw error (because the matrix cannot be resolved, instead use 1 phase model) + if ϕ[ii] <= prec(porosity_thresh) + throw("No mush region identified in viscosity profile.") + end + + # Get top layer where porosity exceeds mush threshold + ii_top = maximum(findall(ϕ .< 0.60)) + + # update the liquid arrays + κl .= prec(bulk_l) # liquid bulk modulus + ηl .= prec(visc_l) # liquid viscosity + k[ii:ii_top] .= prec(permea) # permeability + + # set porosity to zero outside mush region (otherwise code cannot solve system) + ϕ[1:ii-1] .= 0.0 # zero below ii + + # resample profiles onto new grid + r_grid, ρ, η, μc, κs, κl, κd, α, ηl, ϕ, k, g, M_tot = solid1d_mush_relax.resample_profiles(r, ρ, η, μc, κs, κl, κd, α, ηl, ϕ, k, m_core, dr_min, dr_max) + + ρs = ρ.*(1.0.-ϕ) # solid density + ρl = ρ.*ϕ # liquid density + + # use cell centers + r_centers = 0.5 .* (r_grid[1:end-1] .+ r_grid[2:end]) + + # define angular grid + solid1d_mush_relax.define_spherical_grid(res, n, m) + + # solve y functions across grid + y_t, y_l = solid1d_mush_relax.compute_y(r_centers, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, ρ_core, M_tot; core=core) + + # for debugging: plot y-function relaxation solution + # in particular, observe the oscillating behavior near transition zones + # and also near the surface for high frequencies + # plotting.plot_relaxation_solution(y_t, r_centers, + # filename="$OUT_DIR/relaxation_solution.png") + + # Love numbers + k2_T = (y_t[3, end] - 1) .* (maximum(r) ./ R) + k2_L = (y_l[3, 1] - 1) .* (maximum(r) ./ R) + + # heating profile + (Eμ_tot, Eκ_tot) = solid1d_mush_relax.get_heating_profile( + y_t, r_grid, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n + ) + + power_prf = abs.(Eμ_tot .+ Eκ_tot) + + # interpolate from grid back to original radius points + itp = linear_interpolation(r_centers, power_prf, extrapolation_bc=Line()) + + # original centers + r_orig_centers = 0.5 .* (r[1:end-1] .+ r[2:end]) + + power_prf = itp.(r_orig_centers) return power_prf, k2_T, k2_L end diff --git a/src/solid1d.jl b/src/solid1d.jl index c97d5c1..5baea65 100644 --- a/src/solid1d.jl +++ b/src/solid1d.jl @@ -7,6 +7,7 @@ module solid1d using AssociatedLegendrePolynomials using StaticArrays + prec = BigFloat precc = Complex{BigFloat} @@ -57,7 +58,7 @@ module solid1d # Arguments - `r::Array{Float64,2}` : 2D array of layer boundaries. - `ρ::Array{Float64,1}` : 1D array of layer densities. The length of `ρ` must be equal to the number of columns in `r`. - - `m_core::Float64` : Mass of the planetary core. + - `m_core::Float64` : Mass of the core, which is used to compute the gravity at the core boundary. # Returns - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. The dimensions of `g` are the same as `r`. @@ -73,9 +74,9 @@ module solid1d for i in 1:size(r)[2] M[2:end,i] = 4.0/3.0 * π .* diff(r[:,i].^3) .* ρ[i] end - - M[2,1] += m_core + M[2,1] += m_core + g[2:end,:] .= G*accumulate(+,M[2:end,:]) ./ r[2:end,:].^2 g[1,2:end] = g[end,1:end-1] @@ -207,28 +208,6 @@ module solid1d end - function get_scales(r, ρ, R0, M0, s0) - - ρ0 = M0 / R0^3 # kg/m^3 - μ0 = M0 / (R0 * s0^2) # Pa - g0 = R0 / s0^2 # m/s^2 - - G0 = R0^3 / (M0 * s0^2) # Gravity constant scaling - - S = Diagonal(precc[ - R0, # y1: radial displacement (m) - R0, # y2: tangential displacement (m) - μ0, # y3: radial stress (Pa) - μ0, # y4: tangential stress (Pa) - g0*R0, # y5: potential (m^2/s^2) - g0 # y6: potential gradient/gravity (m/s^2) - ]) - - Sinv = inv(S) - return R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv - end - - """ get_Ic(r, ρ, g, μ, type, n; M=6, N=3) @@ -305,9 +284,9 @@ module solid1d # Notes See also [`get_A!`](@ref) """ - function get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing) + function get_A(r, ρ, g, μ, K, n) A = zeros(precc, 6, 6) - get_A!(A, ω, r, ρ, g, μ, K, n; G0=G0, λ=λ) + get_A!(A, r, ρ, g, μ, K, n) return A end @@ -334,39 +313,27 @@ module solid1d # Notes See also [`get_A`](@ref) """ - function get_A!(A::Matrix, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing) + function get_A!(A::Matrix, r, ρ, g, μ, K, n; λ=nothing) if isnothing(λ) λ = K - 2μ/3 end - # if abs(μ) < 1e-9 - # # Scale it down to the max allowed magnitude, preserving phase - # μ = μ / abs(μ) * 1e-9 - # end - - # if abs(K) < 1e-5 - # # Scale it down to the max allowed magnitude, preserving phase - # K = K / abs(K) * 1e-5 - # end - - G_norm = G / G0 - r_inv = 1.0/r β_inv = 1.0/(2μ + λ) rβ_inv = r_inv * β_inv A[1,1] = -2λ * r_inv*β_inv A[2,1] = -r_inv - A[3,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) - ω^2 * ρ + A[3,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) A[4,1] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[5,1] = 4π * G_norm * ρ - A[6,1] = 4π*(n+1)*G_norm*ρ*r_inv + A[5,1] = 4π * G * ρ + A[6,1] = 4π*(n+1)*G*ρ*r_inv A[1,2] = n*(n+1) * λ * r_inv*β_inv A[2,2] = r_inv A[3,2] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[4,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) - ω^2 * ρ - A[6,2] = -4π*n*(n+1)*G_norm*ρ*r_inv + A[4,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) + A[6,2] = -4π*n*(n+1)*G*ρ*r_inv A[1,3] = β_inv A[3,3] = r_inv*β_inv * (-4μ ) @@ -407,22 +374,21 @@ module solid1d # Notes See 'get_B!' for definition. """ - function get_B(ω, r1, r2, g1, g2, ρ, μ, K, n; G0=1) + function get_B(r1, r2, g1, g2, ρ, μ, K, n) B = zeros(precc, 6, 6) - get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n; G0=G0) + get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) return B end """ - get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K) + get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) Compute the 6x6 numerical integrator matrix, which integrates dy/dr from `r1` to `r2` for the solid-body problem. `B` here represnts the RK4 integrator, given by Eq. S5.5 in Hay et al., (2025). # Arguments - `B::Array{precc,2}` : 6x6 numerical integrator matrix for integrating dy/dr from r1 to r2 for the solid-body problem. - - `ω::prec` : Tidal frequency. - `r1::prec` : Starting radius for integration. - `r2::prec` : Ending radius for integration. - `g1::prec` : Gravity at radius r1. @@ -435,15 +401,15 @@ module solid1d # Notes See also [`get_B`](@ref) """ - function get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n; G0=1) + function get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) dr = r2 - r1 rhalf = r1 + 0.5dr ghalf = g1 + 0.5*(g2 - g1) - A1 = get_A(ω, r1, ρ, g1, μ, K, n; G0=G0) - Ahalf = get_A(ω, rhalf, ρ, ghalf, μ, K, n; G0=G0) - A2 = get_A(ω, r2, ρ, g2, μ, K, n; G0=G0) + A1 = get_A(r1, ρ, g1, μ, K, n) + Ahalf = get_A(rhalf, ρ, ghalf, μ, K, n) + A2 = get_A(r2, ρ, g2, μ, K, n) k16 = zeros(precc, 6, 6) k26 = zeros(precc, 6, 6) @@ -478,7 +444,7 @@ module solid1d - `K::Array{prec,1}` : 1D array of layer bulk moduli. - `n::Int` : Tidal degree. """ - function get_B_product!(Bprod2, ω, r, ρ, g, μ, K, n; G0=1) + function get_B_product!(Bprod2, r, ρ, g, μ, K, n) Bstart = Matrix{precc}(I, 6, 6) B = zeros(precc, 6, 6) @@ -490,7 +456,7 @@ module solid1d g1 = g[j] g2 = g[j+1] - get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n; G0=G0) + get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) Bprod2[:,:,j] .= B * (j==1 ? Bstart : Bprod2[:,:,j-1]) r1 = r2 @@ -499,7 +465,7 @@ module solid1d """ - compute_M(r, ρ, g, μ, K, n; core="liquid") + compute_M(r, ρ, g, μ, K, n, ρ_core; core="liquid") Compute the M matrix, which is used to propagate the solution across the entire interior. This is used in the `compute_y` function. @@ -510,6 +476,7 @@ module solid1d - `μ::Array{prec,1}` : 1D array of layer shear moduli. - `K::Array{prec,1}` : 1D array of layer bulk moduli. - `n::Int` : Tidal degree. + - `ρ_core::prec` : Density of the core, which is used to compute the starting vector for the numerical integration across the interior. # Keyword Arguments - `core::String="liquid"` : Type of core, either "liquid" or "solid". This is used to compute the starting vector for the numerical integration across the interior. @@ -517,80 +484,40 @@ module solid1d # Returns - `M::Array{precc,2}` : 3x3 M matrix, which is used to propagate the solution across the entire interior. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. - - `matrices_R_sublayer::Array{Matrix{precc},2}` : 2D array of the R matrices from the QR decomposition at each sublayer, which is used for back-propagation in the `compute_y` function to compute the solution vector across the interior. """ - function compute_M(ω, r, ρ, g, μ, K, n; core="liquid") + function compute_M(r, ρ, g, μ, K, n, ρ_core; core="liquid") r, ρ, g, μ, K = convert_params_to_prec(r, ρ, g, μ, K) nlayers = size(r)[2] nsublayers = size(r)[1] - R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(r, ρ, 4.6e6, 1.6e22, 3.6e5) - - # scale inputs - ω_scaled = ω * s0 - r_scaled = r ./ R0 - ρ_scaled = ρ ./ ρ0 - g_scaled = g ./ g0 - μ_scaled = μ ./ μ0 - K_scaled = K ./ μ0 - - y_start = get_Ic(r[end,1], ρ[1], g[end,1], μ[1], core, n; M=6, N=3) - y_start .= Sinv * y_start # Scale starting vector - - # To store QR factors for back-propagation - matrices_R_sublayer = Array{Matrix{precc}}(undef, nlayers, nsublayers-1) + y_start = get_Ic(r[end,1], ρ_core, g[end,1], μ[1], core, n; M=6, N=3) y1_4 = zeros(precc, 6, 3, nsublayers-1, nlayers) # Three linearly independent y solutions - - current_basis = y_start - + for i in 2:nlayers Bprod = zeros(precc, 6, 6, nsublayers-1) - @views get_B_product!(Bprod, ω_scaled, r_scaled[:, i], ρ_scaled[i], g_scaled[:, i], μ_scaled[i], K_scaled[i], n; G0=G0) + @views get_B_product!(Bprod, r[:, i], ρ[1], g[:, i], μ[i], K[i], n) for j in 1:nsublayers-1 - # Multiply by the current basis - y1_4[:,:,j,i] = @view(Bprod[:,:,j]) * current_basis - - # QR decomposition at the sublayer - F = qr(y1_4[:,:,j,i]) - - Qthin = Matrix(F.Q)[:, 1:3] # extract 6x3 - Rj = Matrix(F.R) - - y1_4[:,:,j,i] = Qthin # orthonormalized - current_basis = Qthin # update the basis for next sublayer - - # store the R matrix if you need it for back-propagation - matrices_R_sublayer[i,j] = Rj + y1_4[:,:,j,i] = @view(Bprod[:,:,j]) * y_start end - current_basis[:,:] .= y1_4[:,:,end,i] # Update the current basis to the last sublayer of the current layer - - if cond(current_basis) > 1e12 - @warn "Current basis at layer $i is ill-conditioned: cond = $(cond(current_basis))" - end - - end - - # Convert y1_4 back to physical units - for i in axes(y1_4,4), j in axes(y1_4,3) - y1_4[:,:,j,i] .= S * y1_4[:,:,j,i] # Scale back to physical units + y_start[:,:] .= @view(y1_4[:,:,end,i]) # Set starting vector for next layer end M = zeros(precc, 3,3) M[1, :] .= y1_4[3,:,end,end] # Row 1 - Radial Stress M[2, :] .= y1_4[4,:,end,end] # Row 2 - Tangential Stress - M[3, :] .= y1_4[6,:,end,end] # Row 3 - Potential Stress - - return M, y1_4, matrices_R_sublayer + M[3, :] .= y1_4[6,:,end,end] .+ (n+1)/r[end:end] .* y1_4[5,:,end,end] # Row 3 - Potential Stress + + return M, y1_4 end """ - compute_y(r, g, M, R, y1_4, matrices_R_sublayer, n; load=false) + compute_y(r, g, M, y1_4, n; load=false) Compute the solution vector `y` across the entire interior, given the M matrix and the y1_4 solutions across each layer. This is used to compute the strain tensor and heating profile. @@ -599,9 +526,7 @@ module solid1d - `r::Array{prec,2}` : 2D array of layer boundaries. - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. - `M::Array{precc,2}` : 3x3 M matrix, which is used to propagate the solution across the entire interior. - - `R::prec` : Surface radius of the body. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. - - `matrices_R_sublayer::Array{Matrix{precc},2}` : 2D array of the R matrices from the QR decomposition at each sublayer, which is used for back-propagation in the `compute_y` function to compute the solution vector across the interior. - `n::Int` : Tidal degree. # Keyword Arguments @@ -610,36 +535,37 @@ module solid1d # Returns - `y::Array{ComplexF64,3}` : 3D array of the solution vector y across the interior. """ - function compute_y(r, g, M, R, y1_4, matrices_R_sublayer, n; load=false) + function compute_y(r, g, M, y1_4, n; load=false) + + tau = 0.0 + P = 0.0 + U_prime = 0.0 + U = 0.0 + if load + U_prime = 1.0 + elseif !load + U = 1.0 + end + + # Define surface mass load (zeta) based on Farrell/Longman relation + zeta = ((2 * n + 1) / (4 * pi * G * r[end,end])) * U_prime nlayers = size(r)[2] nsublayers = size(r)[1] - # Boundary condition b = zeros(precc, 3) - if load - b[1] = -(2n+1)*g[end,end]/(4π*(R)^2) - b[3] = -(2n+1)*G/(R)^2 - else - b[3] = (2n+1)/R - end - # Solve surface coefficients - C_current = M \ b + b[1] = -g[end,end] * zeta * G / r[end,end] - P + b[2] = tau + b[3] = ((2 * n + 1) / r[end,end]) * U - 4 * pi * G * zeta - # Allocate - y = zeros(ComplexF64, 6, nsublayers-1, nlayers) + C = M \ b - # Backward propagation - for i in nlayers:-1:2 - for j in (nsublayers-1):-1:1 - - # Compute solution - y[:,j,i] .= @view(y1_4[:,:,j,i]) * C_current + y = zeros(ComplexF64, 6, nsublayers-1, nlayers) - # Update coefficients using local R - Rj = matrices_R_sublayer[i,j] - C_current = Rj \ C_current + for i in 2:nlayers + for j in 1:nsublayers-1 + y[:,j,i] = @view(y1_4[:,:,j,i])*C end end @@ -676,15 +602,6 @@ module solid1d y3 = y[3] y4 = y[4] - # if abs(μr) < 1e-9 - # # Scale it down to the max allowed magnitude, preserving phase - # μr = μr / abs(μr) * 1e-9 - # end - # if abs(Ksr) < 1e-5 - # # Scale it down to the max allowed magnitude, preserving phase - # Ksr = Ksr / abs(Ksr) * 1e-5 - # end - λr = Ksr .- 2μr/3 βr = λr + 2μr diff --git a/src/solid1d_mush.jl b/src/solid1d_mush.jl index 0435e6c..6625165 100644 --- a/src/solid1d_mush.jl +++ b/src/solid1d_mush.jl @@ -735,7 +735,7 @@ module solid1d_mush - `M::Array{precc,2}` : 4x4 M matrix, which is used to propagate the solution across the entire interior. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. """ - function compute_M(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; core="liquid") + function compute_M(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core; core="liquid") porous_layer = ϕ .> 0.0 ## Convert parameters to the precision of precc: @@ -747,7 +747,7 @@ module solid1d_mush nsublayers = size(r)[1] # Define starting vector as the core solution matrix, Y_r_C (Eq. S5.15) - y_start = get_Ic(r[end,1], ρ[1], g[end,1], μ[1], core, n; M=8, N=4) + y_start = get_Ic(r[end,1], ρ_core, g[end,1], μ[1], core, n; M=8, N=4) y1_4 = zeros(precc, 8, 4, nsublayers-1, nlayers) # Four linearly independent y solutions @@ -777,7 +777,7 @@ module solid1d_mush M[1, :] .= y1_4[3,:,end,end] # Row 1 - Radial Stress M[2, :] .= y1_4[4,:,end,end] # Row 2 - Tangential Stress - M[3, :] .= y1_4[6,:,end,end] # Row 3 - Potential Stress + M[3, :] .= y1_4[6,:,end,end] .+ (n+1)/r[end:end] .* y1_4[5,:,end,end] # Row 3 - Potential Stress for i in 2:nlayers if porous_layer[i] @@ -790,7 +790,7 @@ module solid1d_mush """ - compute_y(r, g, M, R, y1_4, n; load=false) + compute_y(r, g, M, y1_4, n; load=false) Compute the 8x1 solution vector at the surface and porous layer interface, which is used to compute the strain, Darcy flux, and pore pressure at a particular radial level. This is given by Eq. S5.20 in Hay et al., (2025). @@ -799,7 +799,6 @@ module solid1d_mush - `r::Array{prec,2}` : 2D array of layer boundaries. - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. - `M::Array{precc,2}` : 4x4 M matrix, which is used to propagate the solution across the entire interior. - - `R::prec` : Surface radius of the body. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. - `n::Int` : Tidal degree. @@ -809,18 +808,29 @@ module solid1d_mush # Returns - `y::Array{ComplexF64,4}` : 4D array of the solution vector y across the interior. """ - function compute_y(r, g, M, R, y1_4, n; load=false) + function compute_y(r, g, M, y1_4, n; load=false) + + tau = 0.0 + P = 0.0 + U_prime = 0.0 + U = 0.0 + if load + U_prime = 1.0 + elseif !load + U = 1.0 + end + + # Define surface mass load (zeta) based on Farrell/Longman relation + zeta = ((2 * n + 1) / (4 * pi * G * r[end,end])) * U_prime nlayers = size(r)[2] nsublayers = size(r)[1] b = zeros(precc, 4) - if load - b[1] = -(2n+1)*g[end,end]/(4π*(R)^2) - b[3] = -(2n+1)*G/(R)^2 - else - b[3] = (2n+1)/R - end + b[1] = -g[end,end] * zeta * G / r[end,end] - P + b[2] = tau + b[3] = ((2 * n + 1) / r[end,end]) * U - 4 * pi * G * zeta + b[4] = 0.0 C = M \ b diff --git a/src/solid1d_mush_relax.jl b/src/solid1d_mush_relax.jl new file mode 100644 index 0000000..dc460b7 --- /dev/null +++ b/src/solid1d_mush_relax.jl @@ -0,0 +1,1831 @@ + + +module solid1d_mush_relax + + using LinearAlgebra + import GenericLinearAlgebra + using DoubleFloats + using AssociatedLegendrePolynomials + using StaticArrays + using SpecialFunctions + using SparseArrays + using Printf + + prec = BigFloat + precc = Complex{BigFloat} + + const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 + + clats = 0.0 + lons = 0.0 + Y = 0.0 + dYdθ = 0.0 + dYdϕ = 0.0 + Z = 0.0 + X = 0.0 + res = 20.0 + + + """ + resample_profiles(radius, rho, visc, shear, bulk, phi, m_core, dr_min, dr_max) + + Resample the input profiles onto a new grid with `ncalc` points. The new grid is generated using a + stretched and refined scheme, which allows for better resolution in regions of interest (e.g., near + layer boundaries). + + # Arguments + - `radius::Vector{Float64}` : Original radius profile (layer boundaries). + - `rho::Vector{Float64}` : Original density profile (defined at layer centers). + - `visc::Vector{Float64}` : Original viscosity profile (defined at layer centers). + - `shear::Vector{Float64}` : Original shear modulus profile (defined at layer centers). + - `bulk::Vector{Float64}` : Original bulk modulus profile (defined at layer centers). + - `phi::Vector{Float64}` : Original phi profile (defined at layer centers). + - `m_core::Float64` : Mass of the core, used for gravity calculations. + - `Δr_min::Float64` : Minimum grid spacing for the new grid. + - `Δr_max::Float64` : Maximum grid spacing for the new grid. + + # Returns + Tuple of resampled profiles on the new grid: + - `r_new_b::Vector{prec}` : New radius profile at layer boundaries. + - `ρ_new::Vector{prec}` : New density profile at layer centers. + - `η_new::Vector{prec}` : New viscosity profile at layer centers. + - `μ_new::Vector{prec}` : New shear modulus profile at layer centers. + - `κ_new::Vector{prec}` : New bulk modulus profile at layer centers. + - `φ_new::Vector{prec}` : New phi profile at layer centers. + - `g_new::Vector{prec}` : New gravity profile at layer centers. + """ + function resample_profiles(radius, rho, visc, shear, bulk_s, bulk_l, bulk_d, alpha, visc_l, phi, k, m_core, dr_min, dr_max) + # setup grids + α = log(dr_max / dr_min) + + N = Int(round((radius[end] - radius[1]) / dr_min * α / (exp(α) - 1))) + + # indices i = 1:N + i = collect(1:N) + + # convert to BigFloat for consistency + i_bf = BigFloat.(i) + N_bf = BigFloat(N) + + # compute normalized coordinate (N - i)/(N - 1) + ξ = (N_bf .- i_bf) ./ (N_bf - 1) + + # compute r_i + r_new_b = radius[end] .+ (radius[1] - radius[end]) .* ( + (exp.(α .* ξ) .- 1) ./ (exp(α) - 1) + ) + + # cell centers + r_new_c = 0.5 .* (r_new_b[1:end-1] .+ r_new_b[2:end]) + + # obtain new profiles (Constant per original layer) + ρ_new = similar(rho, N-1) + η_new = similar(visc, N-1) + μ_new = similar(shear, N-1) + κs_new = similar(bulk_s, N-1) + κl_new = similar(bulk_l, N-1) + κd_new = similar(bulk_d, N-1) + α_new = similar(alpha, N-1) + ηl_new = similar(visc_l, N-1) + φ_new = similar(phi, N-1) + k_new = similar(k, N-1) + + for i in 1:N-1 + # find index such that r_b[idx] <= r_new_c[i] < r_b[idx+1] + idx = searchsortedfirst(radius, r_new_c[i]) - 1 + idx = clamp(idx, 1, length(rho)) # Safety clamp + + ρ_new[i] = rho[idx] + η_new[i] = visc[idx] + μ_new[i] = shear[idx] + κs_new[i] = bulk_s[idx] + κl_new[i] = bulk_l[idx] + κd_new[i] = bulk_d[idx] + α_new[i] = alpha[idx] + ηl_new[i] = visc_l[idx] + φ_new[i] = phi[idx] + k_new[i] = k[idx] + end + + g_new, M_tot = get_g(r_new_b, ρ_new, m_core) + + return r_new_b, ρ_new, η_new, μ_new, κs_new, κl_new, κd_new, α_new, ηl_new, φ_new, k_new, g_new, M_tot + end + + + """ + get_g(r, ρ, m_core) + + Compute the radial gravity structure associated with a density profile `r` at intervals given by `r`. + + # Arguments + - `r::Array{Float64,1}` : 1D array of layer boundaries. + - `ρ::Array{Float64,1}` : 1D array of layer densities. The length of `ρ` must be equal to the number of columns in `r`. + - `m_core::Float64` : Mass of the core. + + # Returns + - `g::Array{Float64,1}` : 1D array of gravity values at the layer boundaries. The dimensions of `g` are the same as `r`. + - `M_enc::Float64` : Total mass enclosed within the outermost layer boundary. + """ + function get_g(r, ρ, m_core) + + dm = 4.0/3.0 * π .* diff(r.^3) .* ρ + + M_enc = cumsum(dm) .+ m_core + + g = G .* M_enc ./ r[2:end].^2 + + return g, M_enc[end] + end + + + """ + Ynm(n, m, theta, phi) + + Compute the spherical harmonic Ynm for given n, m, theta, and phi. + + # Arguments + - `n::Int` : Tidal degree. + - `m::Int` : Tidal order. + - `theta::Array{Float64,1}` : Array of colatitudes in radians. + - `phi::Array{Float64,1}` : Array of longitudes in radians. + + # Returns + - `Ynm::Array{ComplexF64,2}` : 2D array of spherical harmonic values for each combination of theta and phi. + """ + function Ynm(n, m, theta, phi) + return Plm.(n, m, cos.(theta)) .* exp.(1im * m .* phi) + end + + + """ + define_spherical_grid(res) + + Create the spherical grid of angular resolution `res` in degrees. This is used for + numerical integrations over solid angle. A new grid can easily be defined by + recalling the function with a new `res`. + + # Arguments + - `res::Float64` : Desired angular resolution in degrees. + - `n::Int` : Tidal degree. + - `m::Int` : Tidal order. + + # Notes + The grid is internal to solid1d_relax, but can be accessed with + + solid1d_relax.clats[:] # colatitude grid + solid1d_relax.lons[:] # longitude grid + """ + function define_spherical_grid(res, n, m) + solid1d_mush_relax.res = res + + # θ and φ grids + lons = deg2rad.(collect(0:res:360-0.001))' + clats = deg2rad.(collect(0:res:180)) + clats[1] += 1e-6 + clats[end] -= 1e-6 + + # allocate arrays + solid1d_mush_relax.Y = zeros(ComplexF64, 1, length(clats), length(lons)) + solid1d_mush_relax.dYdθ = similar(solid1d_mush_relax.Y) + solid1d_mush_relax.dYdϕ = similar(solid1d_mush_relax.Y) + solid1d_mush_relax.Z = similar(solid1d_mush_relax.Y) + solid1d_mush_relax.X = similar(solid1d_mush_relax.Y) + + sinθ = sin.(clats) + cosθ = cos.(clats) + cotθ = cosθ ./ sinθ + cscθ = csc.(clats) + + # Normalization factor for spherical harmonics + norm = sqrt((2*n+1) * factorial(n-m) / (4π * factorial(n+m))) + + i = 1 + + # Y + solid1d_mush_relax.Y[i,:,:] = Ynm(n,m,clats,lons) + + # ∂Y/∂θ + Pn = Plm.(n, m, cosθ) + if n > m + Pn_1 = Plm.(n-1, m, cosθ) + dPdθ = (n .* cosθ .* Pn .- (n + m) .* Pn_1) ./ (sinθ) + else + # m == n -> P_{n-1}^m = 0 + dPdθ = (n .* cosθ .* Pn) ./ (sinθ) + end + solid1d_mush_relax.dYdθ[i,:,:] .= dPdθ .* exp.(1im .* m .* lons) + + # ∂Y/∂ϕ + solid1d_mush_relax.dYdϕ[i,:,:] .= 1im * m .* solid1d_mush_relax.Y[i,:,:] + + # Z = 2 ((1/sinθ) ∂²Y/∂θ∂ϕ - cotθ cscθ ∂Y/∂ϕ) + solid1d_mush_relax.Z[i,:,:] .= 2 .* (1im * m ./ sinθ .* solid1d_mush_relax.dYdθ[i,:,:] .- cotθ .* cscθ .* solid1d_mush_relax.dYdϕ[i,:,:]) + + # X = -2 (cotθ ∂Y/∂θ + csc²θ ∂²Y/∂ϕ²) - n(n+1)) Y + solid1d_mush_relax.X[i,:,:] .= -2 .* (cotθ .* solid1d_mush_relax.dYdθ[i,:,:] .- cscθ.^2 .* m^2 .* solid1d_mush_relax.Y[i,:,:]) .- n*(n+1) .* solid1d_mush_relax.Y[i,:,:] + + # Normalize + solid1d_mush_relax.Y[i,:,:] .*= norm + solid1d_mush_relax.dYdθ[i,:,:] .*= norm + solid1d_mush_relax.dYdϕ[i,:,:] .*= norm + solid1d_mush_relax.Z[i,:,:] .*= norm + solid1d_mush_relax.X[i,:,:] .*= norm + + # save grids + solid1d_mush_relax.clats = clats + solid1d_mush_relax.lons = lons + end + + + """ + get_scales(R0, M0, g0) + + Compute the characteristic scales for the problem based on a reference radius `R0`, mass `M0`, and gravity scale + `g0`. These scales are used to non-dimensionalize the equations and ensure numerical stability. + + # Arguments + - `R0::prec` : Reference radius scale (e.g., planetary radius). + - `M0::prec` : Reference mass scale (e.g., planetary mass). + - `g0::prec` : Reference gravity scale. + + # Returns + Tuple of characteristic scales: + - `R0::prec` : Length scale (m). + - `M0::prec` : Mass scale (kg). + - `s0::prec` : Time scale (s). + - `ρ0::prec` : Density scale (kg/m^3). + - `G0::prec` : Gravitational constant scale (m^3 kg^-1 s^-2). + - `g0::prec` : Gravity scale (m/s^2). + - `μ0::prec` : Shear modulus scale (Pa). + - `S::Diagonal{prec}` : Diagonal scaling matrix for state variables. + - `Sinv::Diagonal{prec}` : Inverse of the scaling matrix S. + """ + function get_scales(R0, M0, g0) + + ρ0 = M0 / R0^3 + P0 = g0 * R0 + μ0 = ρ0 * g0 * R0 + + s0 = sqrt(g0 / R0) + G0 = R0^3 / (M0 * s0^2) + + S = Diagonal(precc[ + R0, # y1: radial displacement (m) + R0, # y2: tangential displacement (m) + P0, # y5: potential (m^2/s^2) + μ0, # y7: pore pressure (Pa) + μ0, # y3: radial stress (Pa) + μ0, # y4: tangential stress (Pa) + g0, # y6: potential gradient/gravity (m/s^2) + R0, # y8: relative radial displacement (m) + ]) + + Sinv = inv(S) + return R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv + end + + + """ + doublefactorial(n) + + Compute the double factorial of an integer n, defined as n!! = n * (n-2) * (n-4) * ... until 1 or 0. + + # Arguments + - `n::Integer` : The integer for which to compute the double factorial. Must be non-negative. + + # Returns + - `result::Integer` : The double factorial of n. + """ + function doublefactorial(n::Integer) + n < 0 && error("doublefactorial not defined for negative n") + n == 0 && return one(n) + n == 1 && return one(n) + + result = one(n) + for k in n:-2:1 + result *= k + end + return result + end + + + """ + get_Ic(ω, r, ρ, g, μ, K, type, n; G0=1, M=8, N=4) + + Get the core solution vector. + https://academic.oup.com/gji/article/203/3/2150/2594863 + + # Arguments + - `ω::prec` : Angular frequency. + - `r::prec` : Radius of the core boundary. + - `ρ::prec` : Density of the core. + - `g::prec` : Gravity at the core boundary. + - `μ::prec` : Shear modulus of the core. + - `K::prec` : Bulk modulus of the core. + - `type::String` : Type of core, either "liquid" or "solid". + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `M::Int=6` : Number of rows in the Ic matrix. This should be 6 for the solid-body problem. + - `N::Int=3` : Number of linearly independent solutions to compute. This should be 3 for the solid-body problem. + + # Returns + - `Ic::Array{precc,2}` : MxN array of linearly independent solutions at the core boundary. These are used as starting vectors for the numerical integration across the interior. + """ + function get_Ic(ω, r, ρ, g, μ, K, type, n; G0=1, M=8, N=4) + Ic = zeros(precc, M, N) + + G_norm = G / G0 + + if type=="liquid" + if M == 6 + Ic[1,1] = -r^n / g + Ic[1,3] = 1.0 + Ic[2,2] = 1.0 + Ic[4,3] = g*ρ + Ic[3,1] = r^n + Ic[6,1] = 2(n-1)*r^(n-1) + Ic[6,3] = 4π * G_norm * ρ + elseif M == 8 + Ic[1,1] = -r^n / g + Ic[1,3] = 1.0 + Ic[2,2] = 1.0 + Ic[5,3] = g*ρ + Ic[3,1] = r^n + Ic[7,1] = 2(n-1)*r^(n-1) + Ic[7,3] = 4π * G_norm * ρ + end + elseif type == "inertial" + @warn "Inertial core boundary conditions have not been fully implemented. Use with caution." + + φ = 4π * G_norm * ρ / 3 + α = sqrt(K / ρ) + f = -ω^2 / φ + h = f - (n + 1) + k2 = (ω^2 + 4φ - n*(n+1)*φ^2 / ω^2) / α^2 + k = sqrt(Complex{BigFloat}(k2)) + x = k * r + + x64 = ComplexF64(x) + + jl_n = sphericalbesselj(n, x64) + jl_np1 = sphericalbesselj(n+1, x64) + + ϕl = doublefactorial(2n+1) / x^n * jl_n + ϕlp1 = doublefactorial(2n+3) / x^(n+1) * jl_np1 + ψl = 2*(2n+3)/x^2 * (1 - ϕl) + pref = -r^(n+1) / (2n + 3) + + if M == 6 + Ic[1,1] = n * r^(n-1) + Ic[2,1] = r^(n-1) + Ic[3,1] = 0.0 + Ic[4,1] = 0.0 + Ic[5,1] = -(n*φ - ω^2) * r^n + Ic[6,1] = -(2*(n-1)*n*φ - (2*n + 1)*ω^2) * r^(n-1) + + Ic[1,2] = pref * (0.5 * n * h * ψl + f * ϕlp1) + Ic[2,2] = pref * (0.5 * h * ψl - ϕlp1) + Ic[3,2] = -φ * r^n * f * ϕl + Ic[4,2] = 0.0 + Ic[5,2] = -r^(n+2) * ( + (α^2 * f)/r^2 - (3φ*f)/(2*(2n+3)) * ψl + ) + Ic[6,2] = -r^(n+1) * ( + (2n+1)*(α^2*f)/r^2 - + (3φ*((2n+1)*f - n*h))/(2*(2n+3)) * ψl + ) + + Ic[:,3] .= 0.0 + Ic[2,3] = 1.0 # tangential slip + elseif M == 8 + Ic[1,1] = n * r^(n-1) + Ic[2,1] = r^(n-1) + Ic[5,1] = 0.0 + Ic[6,1] = 0.0 + Ic[3,1] = -(n*φ - ω^2) * r^n + Ic[7,1] = -(2*(n-1)*n*φ - (2*n + 1)*ω^2) * r^(n-1) + + Ic[1,2] = pref * (0.5 * n * h * ψl + f * ϕlp1) + Ic[2,2] = pref * (0.5 * h * ψl - ϕlp1) + Ic[5,2] = -φ * r^n * f * ϕl + Ic[6,2] = 0.0 + Ic[3,2] = -r^(n+2) * ( + (α^2 * f)/r^2 - (3φ*f)/(2*(2n+3)) * ψl + ) + Ic[7,2] = -r^(n+1) * ( + (2n+1)*(α^2*f)/r^2 - + (3φ*((2n+1)*f - n*h))/(2*(2n+3)) * ψl + ) + + Ic[:,3] .= 0.0 + Ic[2,3] = 1.0 # tangential slip + end + elseif type == "solid" # incompressible solid core + if M == 6 + # First column + Ic[1, 1] = n*r^( n+1 ) / ( 2*( 2n + 3) ) + Ic[2, 1] = ( n+3 )*r^( n+1 ) / ( 2*( 2n+3 ) * ( n+1 ) ) + Ic[3, 1] = ( n*ρ*g*r + 2*( n^2 - n - 3)*μ ) * r^n / ( 2*( 2n + 3) ) + Ic[4, 1] = n *( n+2 ) * μ * r^n / ( ( 2n + 3 )*( n+1 ) ) + Ic[6, 1] = 2π*G_norm*ρ*n*r^( n+1 ) / ( 2n + 3 ) + + # Second column + Ic[1, 2] = r^( n-1 ) + Ic[2, 2] = r^( n-1 ) / n + Ic[3, 2] = ( ρ*g*r + 2*( n-1 )*μ ) * r^( n-2 ) + Ic[4, 2] = 2*( n-1 ) * μ * r^( n-2 ) / n + Ic[6, 2] = 4π*G_norm*ρ*r^( n-1 ) + + # Third column + Ic[3, 3] = -ρ * r^n + Ic[5, 3] = -r^n + Ic[6, 3] = -( 2n + 1) * r^( n-1 ) + elseif M == 8 + # First column + Ic[1, 1] = n*r^( n+1 ) / ( 2*( 2n + 3) ) + Ic[2, 1] = ( n+3 )*r^( n+1 ) / ( 2*( 2n+3 ) * ( n+1 ) ) + Ic[5, 1] = ( n*ρ*g*r + 2*( n^2 - n - 3)*μ ) * r^n / ( 2*( 2n + 3) ) + Ic[6, 1] = n *( n+2 ) * μ * r^n / ( ( 2n + 3 )*( n+1 ) ) + Ic[7, 1] = 2π*G_norm*ρ*n*r^( n+1 ) / ( 2n + 3 ) + + # Second column + Ic[1, 2] = r^( n-1 ) + Ic[2, 2] = r^( n-1 ) / n + Ic[5, 2] = ( ρ*g*r + 2*( n-1 )*μ ) * r^( n-2 ) + Ic[6, 2] = 2*( n-1 ) * μ * r^( n-2 ) / n + Ic[7, 2] = 4π*G_norm*ρ*r^( n-1 ) + + # Third column + Ic[5, 3] = -ρ * r^n + Ic[3, 3] = -r^n + Ic[7, 3] = -( 2n + 1) * r^( n-1 ) + end + else + error("Invalid core type: $type. Must be 'liquid', 'inertial', or 'solid'.") + end + + return Ic + end + + + """ + get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, M=8) + + Compute the 6x6 `A` matrix in the ODE for the solid-body problem. + + # Arguments + - `ω::prec` : Forcing frequency of the tidal forcing. + - `r::prec` : Radius at which to compute the A matrix. + - `ρ::prec` : Density at radius r. + - `g::prec` : Gravity at radius r. + - `μ::prec` : Shear modulus at radius r. + - `K::prec` : Bulk modulus at radius r. + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. + - `M::Int=8` : Number of rows in the A matrix. This should be 6 for the solid-body problem, but can be 8 for the two-phase problem. + + # Returns + - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. + """ + function get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, M=8) + A = zeros(precc, 6, 6) + get_A!(A, ω, r, ρ, g, μ, K, n; G0=G0, λ=λ, M=M) + return A + end + + + """ + get_A(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, λ=nothing) + + Compute the 8x8 `A` matrix in the ODE for the two-phase problem. These correspond to + the coefficients given in Equation S4.6 in Hay et al., (2025). + + # Arguments + - `ω::prec` : Forcing frequency. + - `r::prec` : Radius at which to compute the A matrix. + - `ρ::prec` : Density at radius r. + - `g::prec` : Gravity at radius r. + - `μ::prec` : Shear modulus at radius r. + - `K::prec` : Bulk modulus at radius r. + - `ρₗ::prec` : Liquid density at radius r. + - `Kl::prec` : Liquid bulk modulus at radius r. + - `Kd::prec` : Drained bulk modulus at radius r. + - `α::prec` : Biot coefficient at radius r. + - `ηₗ::prec` : Liquid viscosity at radius r. + - `ϕ::prec` : Porosity at radius r. + - `k::prec` : Permeability at radius r. + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. + + # Returns + - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. + + See also [`get_A!`](@ref) + """ + function get_A(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, λ=nothing) + A = zeros(precc, 8, 8) + get_A!(A, ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=G0, λ=λ) + return A + end + + + """ + get_A!(A, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, M=8) + + Compute the 6x6 `A` matrix in the ODE for the solid-body problem. These correspond to + the coefficients given in Equation S4.6 in Hay et al., (2025) when α=φ=0, as well as Sabadini and Vermeersen + (2016) Eq. 1.95. + + # Arguments + - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. + - `ω::prec` : Forcing frequency of the tidal forcing. + - `r::prec` : Radius at which to compute the A matrix. + - `ρ::prec` : Density at radius r. + - `g::prec` : Gravity at radius r. + - `μ::prec` : Shear modulus at radius r. + - `K::prec` : Bulk modulus at radius r. + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. + - `M::Int=8` : Number of rows in the A matrix. This should be 6 for the solid-body problem, but can be 8 for the two-phase problem. + """ + function get_A!(A::Matrix, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, M=8) + if isnothing(λ) + λ = K - 2μ/3 + end + + G_norm = G / G0 + + r_inv = 1.0/r + β_inv = 1.0/(2μ + λ) + rβ_inv = r_inv * β_inv + + if M == 8 + A[1,1] = -2λ * r_inv*β_inv + A[2,1] = -r_inv + A[5,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) - ω^2 * ρ + A[6,1] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) + A[3,1] = 4π * G_norm * ρ + A[7,1] = 4π*(n+1)*G_norm*ρ*r_inv + + A[1,2] = n*(n+1) * λ * r_inv*β_inv + A[2,2] = r_inv + A[5,2] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) + A[6,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) - ω^2 * ρ + A[7,2] = -4π*n*(n+1)*G_norm*ρ*r_inv + + A[1,5] = β_inv + A[5,5] = r_inv*β_inv * (-4μ ) + A[6,5] = -λ * r_inv*β_inv + + A[2,6] = 1.0 / μ + A[5,6] = n*(n+1)*r_inv + A[6,6] = -3r_inv + + A[5,3] = ρ * (n+1)*r_inv + A[6,3] = -ρ*r_inv + A[3,3] = -(n+1)r_inv + + A[5,7] = -ρ + A[3,7] = 1.0 + A[7,7] = (n-1)r_inv + + elseif M ==6 + A[1,1] = -2λ * r_inv*β_inv + A[2,1] = -r_inv + A[4,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) - ω^2 * ρ + A[5,1] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) + A[3,1] = 4π * G_norm * ρ + A[6,1] = 4π*(n+1)*G_norm*ρ*r_inv + + A[1,2] = n*(n+1) * λ * r_inv*β_inv + A[2,2] = r_inv + A[4,2] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) + A[5,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) - ω^2 * ρ + A[6,2] = -4π*n*(n+1)*G_norm*ρ*r_inv + + A[1,4] = β_inv + A[4,4] = r_inv*β_inv * (-4μ ) + A[5,4] = -λ * r_inv*β_inv + + A[2,5] = 1.0 / μ + A[4,5] = n*(n+1)*r_inv + A[5,5] = -3r_inv + + A[4,3] = ρ * (n+1)*r_inv + A[5,3] = -ρ*r_inv + A[3,3] = -(n+1)r_inv + + A[4,6] = -ρ + A[3,6] = 1.0 + A[6,6] = (n-1)r_inv + end + end + + + """ + get_A!(A, ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + + Compute the 8x8 `A` matrix in the ODE for the two-phase problem. These correspond to + the coefficients given in Equation S4.6 in Hay et al., (2025). + + # Arguments + - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. + - `ω::prec` : Forcing frequency. + - `r::prec` : Radius at which to compute the A matrix. + - `ρ::prec` : Density at radius r. + - `g::prec` : Gravity at radius r. + - `μ::prec` : Shear modulus at radius r. + - `K::prec` : Bulk modulus at radius r. + - `ρₗ::prec` : Liquid density at radius r. + - `Kl::prec` : Liquid bulk modulus at radius r. + - `Kd::prec` : Drained bulk modulus at radius r. + - `α::prec` : Biot coefficient at radius r. + - `ηₗ::prec` : Liquid viscosity at radius r. + - `ϕ::prec` : Porosity at radius r. + - `k::prec` : Permeability at radius r. + - `n::Int` : Tidal degree. + + # Notes + See also [`get_A`](@ref) + """ + function get_A!(A::Matrix, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, λ=nothing) + # λ = K - 2μ/3 # Lame's second param, which uses the drained compaction modulus + λ = Kd .- 2μ/3 # Lame's second param, which uses the drained compaction modulus + S = ϕ/Kl + (α - ϕ)/K # Storavity, which uses liquid and solid grain bulk moduli + + # First add the solid-body coefficients, but using drained moduli. + get_A!(A, ω, r, ρ, g, μ, Kd, n; λ=λ, G0=G0, M=8) # Note that here we replace the bulk modulus with the compaction modulus + + r_inv = 1.0/r + β_inv = 1.0/(2μ + λ) + + G_norm = G / G0 + + # ϕ = 0. + # If there is a porous layer, now add the two-phase components + if !iszero(ϕ) + + A[1,4] = α * β_inv + + A[5,1] += 1im * k*ρₗ^2 *g^2 * n*(n+1) / (ω*ηₗ) * r_inv^2 + A[5,3] += -(n+1)r_inv * 1im *(k*ρₗ^2*g*n)/(ω*ηₗ) * r_inv + A[5,4] = 1im * (k*ρₗ*g*n*(n+1))/(ω*ηₗ)*r_inv^2 - 4μ*α*β_inv*r_inv + A[5,8] = 1im * (k*ρₗ^2*g^2*n*(n+1))/(ω*ηₗ)*r_inv^2 - 4ϕ*ρₗ*g*r_inv + + A[6,4] = 2α*μ*r_inv * β_inv + A[6,8] = ϕ*ρₗ*g*r_inv + + A[3,8] = 4π*G_norm*ρₗ*ϕ + + A[7,1] += -1im * 4π*G_norm*n*(n+1)*r_inv * (k*ρₗ^2*g/(ω*ηₗ)*r_inv) + A[7,3] = 1im*4π*n*(n+1)G_norm*(ρₗ)^2*k*r_inv^2 / (ω*ηₗ) + A[7,4] = -1im *4π*n*(n+1)G_norm*ρₗ*k*r_inv^2 / ( ω*ηₗ) + A[7,8] = 4π*G_norm*(n+1)*r_inv * (ϕ*ρₗ - 1im * n*k*ρₗ^2*g/(ω*ηₗ)*r_inv) + + A[4,1] = ρₗ*g*r_inv * ( 4 - 1im *(k*ρₗ*g*n*(n+1)/(ω*ϕ*ηₗ))*r_inv) + A[4,2] = -ρₗ*n*(n+1)*r_inv*g + A[4,3] = -ρₗ*(n+1)r_inv * (1 - 1im*(k*ρₗ*g*n)/(ω*ϕ*ηₗ)*r_inv) + A[4,7] = ρₗ + A[4,4] = - 1im*(k*ρₗ*g*n*(n+1))/(ω*ϕ*ηₗ)*r_inv^2 + A[4,8] = -1im*ω*ϕ*ηₗ/k - 4π*G_norm*(ρ - ϕ*ρₗ)*ρₗ + ρₗ*g*r_inv*(4 - 1im*(k*ρₗ*g*n*(n+1))/(ω*ϕ*ηₗ)*r_inv) + + A[8,1] = r_inv*( 1im * k*ρₗ*g*n*(n+1)/(ω*ϕ*ηₗ)*r_inv - α/ϕ * 4μ*β_inv) + A[8,2] = α/ϕ * 2n*(n+1)*μ *β_inv * r_inv + A[8,5] = -α/ϕ * β_inv + A[8,3] = -1im * k *ρₗ *n*(n+1) / (ω*ϕ*ηₗ)*r_inv^2 + A[8,4] = 1im*k*n*(n+1)/(ω*ϕ*ηₗ)*r_inv^2 - 1/ϕ * (S + α^2 * β_inv) # If solid and liquid are compressible, keep the 1/M term + A[8,8] = 1im * k *ρₗ*g *n*(n+1) / (ω*ϕ*ηₗ)*r_inv^2 - 2r_inv + end + + end + + + """ + solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core; core="liquid") + + Solve the radial system of ODEs for the solid-body problem using a relaxation method. This function + implements the forward-backward relaxation scheme described in the main text of N. Kobayashi (2006). + + # Arguments + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `ρₗ::Vector{prec}` : Vector of liquid densities at the layer centers. + - `Kl::Vector{prec}` : Vector of liquid bulk moduli at the layer centers. + - `Kd::Vector{prec}` : Vector of drained bulk moduli at the layer centers. + - `α::Vector{prec}` : Vector of Biot coefficients at the layer centers. + - `ηₗ::Vector{prec}` : Vector of liquid viscosities at the layer centers. + - `ϕ::Vector{prec}` : Vector of porosities at the layer centers. + - `k::Vector{prec}` : Vector of permeabilities at the layer centers. + - `n::Int` : Tidal degree. + - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `M_tot::prec` : Total mass of the body, used for non-dimensionalization. + + # Keyword Arguments + - `core::String="liquid" : Type of core boundary condition to apply. Options are "liquid" for a fluid core, "solid" for a solid core, and "inertial" for a core with inertial response. + + # Returns + - `y_t::Vector{precc}` : Vector of length 8 representing the tidal solution at the top of the mantle. This includes the displacements, stresses, and potential at the surface. + - `y_l::Vector{precc}` : Vector of length 8 representing the load solution at the top of the mantle. This includes the displacements, stresses, and potential at the surface. + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. + - `S::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the normalization. + - `ifc::Int` : Index of the first interface layer (the one closer to the core). + - `ifd::Int` : Index of the second interface layer (the one closer to the surface). + """ + function solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, + ρ_core, M_tot; core="liquid") + + # 1. Find the original interface index + ifc_orig = findfirst(k .> 0) + ifd_orig = findlast(k .> 0) + + vars = (r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k) + new_vars = map(vars) do v + v_new = copy(v) + insert!(v_new, ifc_orig, v[ifc_orig]) + insert!(v_new, ifd_orig + 1, v[ifd_orig]) + v_new + end + new_r, new_ρ, new_g, new_μ, new_K, new_ρₗ, new_Kl, new_Kd, new_α, new_ηₗ, new_ϕ, new_k = new_vars + + Nr = length(new_r) + ifc = ifc_orig # The first of the two duplicate layers + ifd = ifd_orig + 1 # The second of the two duplicate layers + + # 3. Define the new segments for the relaxation scheme + # Segment 3 now covers the transition between the two identical radial points + ids = [ + (1, 2), # 1: Core Boundary + (2, ifc-1), # 2: Solid Propagation + (ifc-1, ifc+1), # 3: Interface Transition (duplicate layer) + (ifc+1, ifd-1), # 4: Mushy Propagation + (ifd-1, ifd+1), # 5: Interface Transition (duplicate layer) + (ifd+1, Nr-1), # 6: Solid Propagation + (Nr-1, Nr) # 7: Surface Boundary + ] + + # 4. Non-dimensional scaling (Not working atm, use 1.,1.,1. for now) + R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(1., 1., 1.) + # R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(r[end], M_tot, g[end]) + + ωs = ω * s0 + rs = new_r ./ R0 + ρs = new_ρ ./ ρ0 + gs = new_g ./ g0 + μs = new_μ ./ μ0 + Ks = new_K ./ μ0 + ρₗs = new_ρₗ ./ ρ0 + Kls = new_Kl ./ μ0 + Kds = new_Kd ./ μ0 + ηₗs = new_ηₗ ./ (μ0 * s0) + ks = new_k ./ R0^2 + + # 5. Initialize Matrices + R = [zeros(precc, 8, 8) for _ in 1:Nr] + B = [zeros(precc, 8, 1) for _ in 1:Nr] + + # Define the specific indices used by 6x6 + idx = [1, 2, 3, 5, 6, 7] + + # Create the view using these indices for both rows and columns + R6_view = [view(R[i], idx, idx) for i in 1:Nr] + R8_view = [view(R[i], 1:8, 1:8) for i in 1:Nr] + B6_view = [view(B[i], idx, 1) for i in 1:Nr] + B8_view = [view(B[i], 1:8, 1) for i in 1:Nr] + + # component 1: apply core boundary condition and get first solution (3x6) + C1l, D2l = core_boundary(R6_view, ids[1], rs, ρs, gs, μs, Ks, ωs, ρ_core/ρ0, core, n; G0=G0) + + # component 1: apply core boundary condition and get first solution (4x8) + # C1l, D2l = core_boundary_mush(R8_view, ids[1], rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, α, ηₗs, ϕ, ks, ρ_core/ρ0, core, n; G0=G0) + + # component 2: propagate the solution up to the surface (6x6) + C1l, D2l = propagate_solid(R6_view, B6_view, C1l, D2l, ids[2], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) + + # component 3: interface between 6x6 and 8x8 + C1l, D2l = interface_solid_mush(R8_view, B8_view, C1l, D2l, ids[3]) + + # component 4: propagate the solution up to the surface (8x8) + C1l, D2l = propagate_mush(R8_view, B8_view, C1l, D2l, ids[4], rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, new_α, ηₗs, new_ϕ, ks, n; G0=G0) + + # component 5: interface between 8x8 and 6x6 + C1l, D2l = interface_mush_solid(R8_view, B8_view, C1l, D2l, ids[5]) + + # component 6: propagate the solution up to the surface (6x6) + C1l, D2l = propagate_solid(R6_view, B6_view, C1l, D2l, ids[6], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) + + # component 7: apply surface boundary condition and solve for the final solution at the surface + y_t, y_l = surface_boundary(R6_view, B6_view, C1l, D2l, ids[7], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) + + # component 3: apply surface boundary condition and solve for the final solution at the surface + # y_t, y_l = surface_boundary_mush(R8_view, B8_view, C1l, D2l, ids[5], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) + + return y_t, y_l, R, B, S, ifc, ifd + end + + + """ + interface_mush_solid(R8, B8, Cn_l, Dnp_l, ids) + + Perform the forward-backward relaxation step at the interface between the mushy layer and the solid layer. This + function implements the recursion described in N. Kobayashi (2007) for the transition from the 8x8 system to the 6x6 system. + + # Arguments + - `R8::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `B8::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. + - `Cn_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Cn matrix from the previous step. + - `Dnp_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Dnp matrix from the previous step. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + + # Returns + - `Cn_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Cn matrix for the next iteration. + - `Dnp_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. + """ + function interface_mush_solid(R8, B8, Cn_l, Dnp_l, ids) + + start_id, end_id = ids + + # Impose continuity at the boundary + # Make sure bn[4, 1] = 0.0, i.e. zero darcy flux at boundary + bn = zeros(precc, 8, 1) + + I86 = zeros(precc, 8, 8) + I8 = Matrix{precc}(I, 8, 8) + + icc = [1.,1.,1.,0.,1.,1.,1.,1.] + idd = [1.,1.,1.,0.,1.,1.,1.,0.] + for i in 1:8 + I86[i, i] = icc[i] + I8[i, i] = idd[i] + end + + Cn = I86 + Dnp = -I8 + + target_cols = [1, 2, 3, 5, 6, 7] + # 1. Use the "stored" lower halves from the previous step + # to fill the upper blocks of P and S. + Pn_u = Cn_l + Sn_u = Dnp_l + Qn_u = zeros(precc, 4, 8) + + # 2. Get the upper halves of the NEWLY calculated Cn and Dnp + Cn_u = Cn[1:4, :] + Dnp_u = Dnp[1:4, :] + + # 3. Build the 8x8 blocks + Pn = [Pn_u; zeros(precc, 4, 8)] + Sn = [Sn_u; Cn_u] + Qn = [Qn_u; Dnp_u] + + # 4. Perform recursion + Xn = Pn * R8[start_id] + Sn + R_ifc = - pinv(Xn) * Qn + + R8[start_id+1] .= R_ifc + B8[start_id+1] .= pinv(Xn) * (bn - Pn * B8[start_id]) + + # 5. Update the "stored" lower halves for the next iteration + Cn_l = Cn[5:7, target_cols] + Dnp_l = Dnp[5:7, target_cols] + + return Cn_l, Dnp_l + end + + + """ + interface_solid_mush(R8, B8, Cn_l, Dnp_l, ids) + + Perform the forward-backward relaxation step at the interface between the solid layer and the mushy layer. This + function implements the recursion described in N. Kobayashi (2007) for the transition from the 6x6 system to the 8x8 system. + + # Arguments + - `R8::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `B8::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. + - `Cn_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Cn matrix from the previous step. + - `Dnp_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Dnp matrix from the previous step. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + + # Returns + - `Cn_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Cn matrix for the next iteration. + - `Dnp_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. + """ + function interface_solid_mush(R8, B8, Cn_l, Dnp_l, ids) + + start_id, end_id = ids + + # impose continuity at the boundary + bn = zeros(precc, 8, 1) + # Induce some pore pressure + bn[4, 1] = -1.0 + + I86 = zeros(precc, 8, 8) + iss = [1.,1.,1.,1.,1.,1.,1.,0.] + for i in 1:8 + I86[i, i] = iss[i] + end + + I8 = Matrix{precc}(I, 8, 8) + + Cn = I86 + Dnp = -I8 + + target_cols = [1, 2, 3, 5, 6, 7] + # 1. Use the "stored" lower halves from the previous step + # to fill the upper blocks of P and S. + Pn_u = zeros(precc, 4, 8) + Pn_u[1:3, target_cols] .= Cn_l + + Sn_u = zeros(precc, 4, 8) + Sn_u[1:3, target_cols] .= Dnp_l + + Qn_u = zeros(precc, 4, 8) + + # 2. Get the upper halves of the NEWLY calculated Cn and Dnp + Cn_u = Cn[1:4, :] + Dnp_u = Dnp[1:4, :] + + # 3. Build the 8x8 blocks + Pn = [Pn_u; zeros(precc, 4, 8)] + Sn = [Sn_u; Cn_u] + Qn = [Qn_u; Dnp_u] + + # 4. Perform recursion + Xn = Pn * R8[start_id] + Sn + R_ifc = - pinv(Xn) * Qn + + R8[start_id+1] .= R_ifc + B8[start_id+1] .= pinv(Xn) * (bn - Pn * B8[start_id]) + + # 5. Update the "stored" lower halves for the next iteration + Cn_l = Cn[5:8, :] + Dnp_l = Dnp[5:8, :] + + return Cn_l, Dnp_l + + end + + + """ + core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n) + + Perform the forward-backward relaxation step at the core boundary. This function implements the recursion described + in N. Kobayashi (2007) for the initial step of the relaxation scheme, where we apply the core boundary condition and + get the first solution for the first layer above the core. + + # Arguments + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `core::String` : Type of core boundary condition to apply. + - `n::Int` : Tidal degree. + + Keyword Arguments + - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + + # Returns + - `C1l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the C1 matrix for the next iteration. + - `D2l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the D2 matrix for the next iteration. + """ + function core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n; G0=1) + + start_id, end_id = ids + + # boundary conditions + B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ[start_id], K[start_id], core, n; G0=G0, M=6, N=3) + + # first layer (n = 1) + dr = r[end_id] - r[start_id] + + A1 = get_A(ω, r[start_id], ρ[start_id], g[start_id], μ[start_id], K[start_id], n; G0=G0, M=6) + A2 = get_A(ω, r[end_id], ρ[end_id], g[end_id], μ[end_id], K[end_id], n; G0=G0, M=6) + + I6 = Matrix{precc}(I, 6, 6) + + C1 = I6 + 0.5 * dr * A1 + D2 = -I6 + 0.5 * dr * A2 + + # split matrices + C1u, C1l = C1[1:3, :], C1[4:6, :] + D2u, D2l = D2[1:3, :], D2[4:6, :] + + # build S1 and Q1 + S1 = [B1; C1u] # 6×6 + Q1 = [zeros(3,6); D2u] # 6×6 + + # initial recursion + R[start_id] .= -S1 \ Q1 + + return C1l, D2l + end + + + """ + core_boundary_mush(R, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, ρ_core, core, n) + + Perform the forward-backward relaxation step at the core boundary for the two-phase problem. This function implements + the recursion described in N. Kobayashi (2007) for the initial step of the relaxation scheme, where we apply the core + boundary condition and get the first solution for the first layer above the core, but now using the full 8x8 system + that includes the porous layer components. + + # Arguments + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `ρₗ::Vector{prec}` : Vector of liquid densities at the layer centers. + - `Kl::Vector{prec}` : Vector of liquid bulk moduli at the layer centers. + - `Kd::Vector{prec}` : Vector of drained bulk moduli at the layer centers. + - `α::Vector{prec}` : Vector of Biot coefficients at the layer centers. + - `ηₗ::Vector{prec}` : Vector of liquid viscosities at the layer centers. + - `ϕ::Vector{prec}` : Vector of porosities at the layer centers. + - `k::Vector{prec}` : Vector of permeabilities at the layer centers. + - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `core::String` : Type of core boundary condition to apply. + - `n::Int` : Tidal degree. + + Keyword Arguments + - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + + # Returns + - `C1l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the C1 matrix for the next iteration. + - `D2l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the D2 matrix for the next iteration. + """ + function core_boundary_mush(R, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, ρ_core, core, n; G0=1) + + start_id, end_id = ids + + # boundary conditions + B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ[start_id], K[start_id], core, n; G0=G0, M=8, N=4) + + # first layer (n = 1) + dr = r[end_id] - r[start_id] + + A1 = get_A(ω, r[start_id], ρ[start_id], g[start_id], μ[start_id], K[start_id], + ρₗ[start_id], Kl[start_id], Kd[start_id], α[start_id], ηₗ[start_id], ϕ[start_id], k[start_id], n; G0=G0) + + A2 = get_A(ω, r[end_id], ρ[end_id], g[end_id], μ[end_id], K[end_id], + ρₗ[end_id], Kl[end_id], Kd[end_id], α[end_id], ηₗ[end_id], ϕ[end_id], k[end_id], n; G0=G0) + + I8 = Matrix{precc}(I, 8, 8) + + C1 = I8 + 0.5 * dr * A1 + D2 = -I8 + 0.5 * dr * A2 + + # split matrices + C1u, C1l = C1[1:4, :], C1[5:8, :] + D2u, D2l = D2[1:4, :], D2[5:8, :] + + # build S1 and Q1 + S1 = [B1; C1u] # 8×8 + Q1 = [zeros(4,8); D2u] # 8×8 + + # initial recursion + R[start_id] .= -pinv(S1) * Q1 + + return C1l, D2l + end + + + """ + propagate_solid(R, B, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, n) + + Perform the forward-backward relaxation step for the solid propagation segments. This function implements the + recursion described in N. Kobayashi (2007) for the segments of the radial grid that correspond to the solid + layers, where we propagate the solution up to the surface using the 6x6 system of equations. + + # Arguments + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. + - `Cn_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Cn matrix from the previous step. + - `Dnp_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Dnp matrix from the previous step. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `n::Int` : Tidal degree. + + Keyword Arguments + - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + + # Returns + - `Cn_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Cn matrix for the next iteration. + - `Dnp_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. + """ + function propagate_solid(R, B, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, n; G0=1) + + start_id, end_id = ids + + I6 = Matrix{precc}(I, 6, 6) + + Cn_u = zeros(3,6) + Dnp_u = zeros(3,6) + + # forward recursion + for i in start_id:end_id + + dr = r[i+1] - r[i] + + # Calculate A at current and next step + A_n = get_A(ω, r[i], ρ[i], g[i], μ[i], K[i], n; G0=G0, M=6) + A_np = get_A(ω, r[i+1], ρ[i+1], g[i+1], μ[i+1], K[i+1], n; G0=G0, M=6) + + Cn = I6 + 0.5 * dr * A_n + Dnp = -I6 + 0.5 * dr * A_np + + # 1. Use the "stored" lower halves from the previous step + # to fill the upper blocks of P and S. + Pn_u = Cn_l + Sn_u = Dnp_l + Qn_u = zeros(precc, 3, 6) + + # 2. Get the upper halves of the NEWLY calculated Cn and Dnp + Cn_u = Cn[1:3, :] + Dnp_u = Dnp[1:3, :] + + # 3. Build the 6x6 blocks + Pn = [Pn_u; zeros(precc, 3, 6)] + Sn = [Sn_u; Cn_u] + Qn = [Qn_u; Dnp_u] + + # 4. Perform recursion + Xn = Pn * R[i-1] + Sn + R[i] .= -Xn \ Qn + B[i] .= Xn \ (-Pn * B[i-1]) + + # 5. Update the "stored" lower halves for the next iteration + Cn_l = Cn[4:6, :] + Dnp_l = Dnp[4:6, :] + end + + return Cn_l, Dnp_l + end + + + """ + propagate_mush(R, B, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + + Perform the forward-backward relaxation step for the mushy layer propagation segment. This function implements the + recursion described in N. Kobayashi (2007) for the segment of the radial grid that corresponds to the mushy layer, + where we propagate the solution up to the surface using the full 8x8 system of equations that includes the porous + layer components. + + # Arguments + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. + - `Cn_l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the Cn matrix from the previous step. + - `Dnp_l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the Dnp matrix from the previous step. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `ρₗ::Vector{prec}` : Vector of liquid densities at the layer centers. + - `Kl::Vector{prec}` : Vector of liquid bulk moduli at the layer centers. + - `Kd::Vector{prec}` : Vector of drained bulk moduli at the layer centers. + - `α::Vector{prec}` : Vector of Biot coefficients at the layer centers. + - `ηₗ::Vector{prec}` : Vector of liquid viscosities at the layer centers. + - `ϕ::Vector{prec}` : Vector of porosities at the layer centers. + - `k::Vector{prec}` : Vector of permeabilities at the layer centers. + - `n::Int` : Tidal degree. + + Keyword Arguments + - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + + # Returns + - `Cn_l::Matrix{precc}` : Updated 4x8 matrix representing the "stored" lower half of the Cn matrix for the next iteration. + - `Dnp_l::Matrix{precc}` : Updated 4x8 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. + """ + function propagate_mush(R, B, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1) + + start_id, end_id = ids + + I8 = Matrix{precc}(I, 8, 8) + + Cn_u = zeros(4,8) + Dnp_u = zeros(4,8) + + # forward recursion + for i in start_id:end_id + + dr = r[i+1] - r[i] + + # Calculate A at current and next step + A_n = get_A(ω, r[i], ρ[i], g[i], μ[i], K[i], + ρₗ[i], Kl[i], Kd[i], α[i], ηₗ[i], ϕ[i], k[i], n; G0=G0) + + A_np = get_A(ω, r[i+1], ρ[i+1], g[i+1], μ[i+1], K[i+1], + ρₗ[i+1], Kl[i+1], Kd[i+1], α[i+1], ηₗ[i+1], ϕ[i+1], k[i+1], n; G0=G0) + + Cn = I8 + 0.5 * dr * A_n + Dnp = -I8 + 0.5 * dr * A_np + + # 1. Use the "stored" lower halves from the previous step + # to fill the upper blocks of P and S. + Pn_u = Cn_l + Sn_u = Dnp_l + Qn_u = zeros(precc, 4, 8) + + # 2. Get the upper halves of the NEWLY calculated Cn and Dnp + Cn_u = Cn[1:4, :] + Dnp_u = Dnp[1:4, :] + + # 3. Build the 6x6 blocks + Pn = [Pn_u; zeros(precc, 4, 8)] + Sn = [Sn_u; Cn_u] + Qn = [Qn_u; Dnp_u] + + # 4. Perform recursion + Xn = Pn * R[i-1] + Sn + + R[i] .= -Xn \ Qn + B[i] .= Xn \ (-Pn * B[i-1]) + + # 5. Update the "stored" lower halves for the next iteration + Cn_l = Cn[5:8, :] + Dnp_l = Dnp[5:8, :] + end + + return Cn_l, Dnp_l + end + + + """ + surface_boundary(R, B, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n) + + Perform the forward-backward relaxation step at the surface boundary. This function implements the recursion described + in N. Kobayashi (2007) for the final step of the relaxation scheme, where we apply the surface boundary condition and + solve for the final solution at the surface, using the 6x6 system of equations. + + # Arguments + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. + - `CNm_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the CNm matrix from the previous step. + - `DN_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the DN matrix from the previous step. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `n::Int` : Tidal degree. + + Keyword Arguments + - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + + # Returns + - `y_t::Matrix{precc}` : 6x1 matrix representing the solution at the surface for the tidal problem. + - `y_l::Matrix{precc}` : 6x1 matrix representing the solution at the surface for the load problem. + """ + function surface_boundary(R, B, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n; G0=1) + + start_id, end_id = ids + + # tidal surface boundary condition + BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0; M=6, N=3) + # load surface boundary condition + BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0; M=6, N=3) + + PN = [CNm_l; zeros(3,6)] + SN_t = [DN_l; BN_t] + SN_l = [DN_l; BN_l] + + XN_t = PN * R[start_id] + SN_t + XN_l = PN * R[start_id] + SN_l + + BN = - XN_t \ PN * B[start_id] + + # solve outer (tides) + y_t = XN_t \ b_t + BN + # solve outer (load) + y_l = XN_l \ b_l + BN + + return y_t, y_l + + end + + + """ + surface_boundary_mush(R, B, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n) + + Perform the forward-backward relaxation step at the surface boundary for the two-phase problem. This function implements + the recursion described in N. Kobayashi (2007) for the final step of the relaxation scheme, where we apply the surface + boundary condition and solve for the final solution at the surface, using the full 8x8 system of equations that includes + the porous layer components. + + # Arguments + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. + - `CNm_l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the CNm matrix from the previous step. + - `DN_l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the DN matrix from the previous step. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `n::Int` : Tidal degree. + + Keyword Arguments + - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + + # Returns + - `y_t::Matrix{precc}` : 8x1 matrix representing the solution at the surface for the tidal problem. + - `y_l::Matrix{precc}` : 8x1 matrix representing the solution at the surface for the load problem. + """ + function surface_boundary_mush(R, B, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n; G0=1) + + start_id, end_id = ids + + # tidal surface boundary condition + BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0; M=8, N=4) + # load surface boundary condition + BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0; M=8, N=4) + + PN = [CNm_l; zeros(4,8)] + SN_t = [DN_l; BN_t] + SN_l = [DN_l; BN_l] + + XN_t = PN * R[start_id] + SN_t + XN_l = PN * R[start_id] + SN_l + + BN = - XN_t \ PN * B[start_id] + + # solve outer (tides) + y_t = pinv(XN_t) * b_t + BN + # solve outer (load) + y_l = pinv(XN_l) * b_l + BN + + return y_t, y_l + + end + + + """ + get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1, M=6, N=3) + + Get the core boundary condition matrix `B` for the solid-body problem. The core boundary + conditions are derived from the requirement that the radial stress at the core-mantle + boundary must balance the tidal potential, and that the tangential stresses must vanish. + + # Arguments + - `ω::Float64` : Forcing frequency. + - `r::prec` : Radial position of the core-mantle boundary. + - `ρ::prec` : Density at the core-mantle boundary. + - `g::prec` : Gravity at the core-mantle boundary. + - `μ::precc` : Complex shear modulus at the core-mantle boundary. + - `K::prec` : Bulk modulus at the core-mantle boundary. + - `type::String` : Type of core boundary condition to apply. Options are "liquid" for a fluid core, "solid" for a solid core, and "inertial" for a core with inertial response. + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `M::Int=6` : Dimensionality of the system (6 for standard, 8 for mushy layer). + - `N::Int=3` : Number of boundary conditions to apply (3 for standard, 4 for mushy layer). + + # Returns + - `B::Array{precc,2}` : 3x6 matrix representing the linear relationship between the state variables at the core and the boundary conditions. + """ + function get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1, M=6, N=3) + # 1. Get the Initial Conditions matrix + Ic = get_Ic(ω, r, ρ, g, μ, K, type, n; G0=G0, M=M, N=N) + + # 2. Define indices based on dimensionality + # If M=8 (Mushing/Hay 2025): U=1, V=2, phi=5, P=7 | X=3, Y=4, psi=6, R=8 + # If M=6 (Standard/Takeuchi): U=1, V=2, phi=5 | X=3, Y=4, psi=6 + if M == 8 + idx_u = [1, 2, 3, 4] + idx_s = [5, 6, 7, 8] + else + idx_u = [1, 2, 5] + idx_s = [3, 4, 6] + end + + # 3. Partition and calculate the boundary condition coefficients + Mu = Ic[idx_u, :] + Ms = Ic[idx_s, :] + + # Equation 91: b = -Mu * Ms⁻¹ + b = -Mu * pinv(Ms) + + # 4. Construct the NxM B matrix + T = eltype(b) + B = zeros(T, N, M) + + for i in 1:N + B[i, idx_u[i]] = 1.0 + for j in 1:length(idx_s) + B[i, idx_s[j]] = b[i, j] + end + end + + return B + end + + + """ + get_surface_bc!(R, g, n, U, U_prime, tau, P; G0=1, M=6, N=3) + + Get the surface boundary condition vector `b` and matrix `BN` for the solid-body problem. The surface + boundary conditions are determined by setting, respectively (U, U', tau, P) to (1,0,0,0) for tidal Love + number and (0,1,0,0) for load Love number in system. + + https://hal.science/hal-03421553/document + + # Arguments + - `R::prec` : Planetary radius, used for surface boundary conditions. + - `g::prec` : Gravity at the surface, used for surface boundary conditions. + - `n::Int` : Tidal degree. + - `U::Int` : Tidal potential at the surface. + - `U_prime::Int` : Radial derivative of the tidal potential at the surface. + - `tau::Int` : Tangential tidal stress at the surface. + - `P::Int` : Surface mass load at the surface. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `M::Int=6` : Dimensionality of the system (6 for standard, 8 for mushy layer). + - `N::Int=3` : Number of boundary conditions to apply (3 for standard, 4 for mushy layer). + + # Returns + - `B::Array{precc,2}` : 3x6 matrix representing the linear relationship between the state variables at the surface and the boundary conditions. + - `b::Vector{precc}` : Vector of length 6 representing the inhomogeneous part of the surface boundary conditions. + """ + function get_surface_bc!(R, g, n, U, U_prime, tau, P; G0=1, M=8, N=4) + + # Define surface mass load (zeta) based on Farrell/Longman relation + zeta = ((2 * n + 1) / (4 * pi * G/G0 * R)) * U_prime + + # b vector (Right Hand Side of the B*y = b system) + b = zeros(precc, M) + + if M == 8 + # radial Stress y3 + b[5] = -g * zeta * (G/G0) / R - P + + # tangential Stress y4 + b[6] = tau + + # gravitational potential boundary + b[7] = ((2 * n + 1) / R) * U + 4 * pi * G/G0 * zeta + + # darcy flux boundary + b[8] = 0. + elseif M == 6 + # radial Stress y3 + b[4] = -g * zeta * (G/G0) / R - P + + # tangential Stress y4 + b[5] = tau + + # gravitational potential boundary + b[6] = ((2 * n + 1) / R) * U - 4 * pi * G/G0 * zeta + else + error("Unsupported M value. M should be either 6 or 8.") + end + + # construct the 4x8 B matrix + # this matrix extracts y3, y4, and the combination for y6 + B = zeros(precc, N, M) + + if M == 8 + # stress components + B[1, 5] = 1.0 # radial stress y3 + B[2, 6] = 1.0 # tangential stress y4 + + # potential component + B[3, 3] = (n + 1) / R + B[3, 7] = 1.0 + B[4, 8] = 1.0 + elseif M == 6 + # stress components + B[1, 4] = 1.0 # radial stress y3 + B[2, 5] = 1.0 # tangential stress y4 + # potential component + B[3, 3] = (n + 1) / R + B[3, 6] = 1.0 + end + + return B, b + end + + + """ + compute_y(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core; core="liquid") + + Compute the solution `y` to the solid-body problem using a relaxation method. This function performs the + forward-backward relaxation scheme described in the main text of N. Kobayashi (2006), where we first solve + the radial system of ODEs to obtain the solution at the surface, and then perform back-substitution to + compute the solution at all interior points. + + # Arguments + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `ρₗ::Vector{prec}` : Vector of liquid densities at the layer centers. + - `Kl::Vector{prec}` : Vector of liquid bulk moduli at the layer centers. + - `Kd::Vector{prec}` : Vector of drained bulk moduli at the layer centers. + - `α::Vector{prec}` : Vector of Biot coefficients at the layer centers. + - `ηₗ::Vector{prec}` : Vector of liquid viscosities at the layer centers. + - `ϕ::Vector{prec}` : Vector of porosities at the layer centers. + - `k::Vector{prec}` : Vector of permeabilities at the layer centers. + - `n::Int` : Tidal degree. + - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `M_tot::prec` : Total mass of the planet. + + # Keyword Arguments + - `core::String="liquid" : Type of core boundary condition to apply. Options are "liquid" for a fluid core, "solid" for a solid core, and "inertial" for a core with inertial response. + + # Returns + - `y::Matrix{precc}` : 6xN matrix of the solution at all radial grid points, where N is the number of radial layers. Each column corresponds to a radial grid point, and each row corresponds to a state variable (displacements, stresses, potential). + """ + function compute_y(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, M_tot; core="liquid") + + # solve radial system to get surface solution and recursion matrices + yN_t, yN_l, R, B, S, ifc, ifd = solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, M_tot; core=core) + + Nr = length(r) + 2 + T = eltype(yN_t) + M = length(yN_t) + + # allocate as 8 x N matrix + y_t = zeros(T, 8, Nr) + y_l = zeros(T, 8, 1) + + # solve outer (tides) + y_t[1:M, Nr] = yN_t + # solve outer (load) + y_l[1:M, 1] = yN_l + + # back-substitution + for i in Nr-1:-1:1 + if i > ifc && i < ifd + # in the mushy region, use the mushy recursion + y_t[:, i] = R[i] * y_t[:, i+1] + B[i] + else + # in the solid region, use the solid recursion + y_t[:, i] = R[i] * y_t[:, i+1] + B[i] + end + end + + # Create a mask for all columns except the two interface indices + mask = [i for i in 1:size(y_t, 2) if i != ifc && i != ifd] + + # Apply the mask to the columns + y_t = y_t[:, mask] + + # apply scaling to get dimensional solution + for i in 1:Nr-2 + y_t[:, i] = S * y_t[:, i] + end + + y_l .= S * y_l + + return y_t, y_l + end + + + """ + compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr) + + Calculate the strain tensor ϵ at a particular radial level. + + # Arguments + - `ϵ::Array{ComplexF64,4}` : 4D array to store the strain tensor at a particular radial level, with dimensions corresponding to latitude, longitude, and the 6 independent components of the strain tensor. + - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 6 components. + - `n::Int` : Tidal degree. + - `rr::prec` : Radius at which to compute the strain tensor. + - `ρr::prec` : Density at radius rr. + - `gr::prec` : Gravity at radius rr. + - `μr::prec` : Shear modulus at radius rr. + - `Ksr::prec` : Bulk modulus at radius rr. + - `ω::prec` : Forcing frequency. + - `ρlr::prec` : Liquid density at radius rr. + - `Klr::prec` : Liquid bulk modulus at radius rr. + - `Kdr::prec` : Drained bulk modulus at radius rr. + - `αr::prec` : Biot coefficient at radius rr. + - `ηlr::prec` : Liquid viscosity at radius rr. + - `ϕr::prec` : Porosity at radius rr. + - `kr::prec` : Permeability at radius rr. + """ + function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr) + i = 1 + + @views Y = solid1d_mush_relax.Y[i,:,:] + @views dYdθ = solid1d_mush_relax.dYdθ[i,:,:] + @views dYdϕ = solid1d_mush_relax.dYdϕ[i,:,:] + @views Z = solid1d_mush_relax.Z[i,:,:] + @views X = solid1d_mush_relax.X[i,:,:] + + y1 = y[1] + y2 = y[2] + y3 = y[5] + y4 = y[6] + y7 = y[4] + + λr = Kdr - 2μr/3 + βr = λr + 2μr + + # Compute strain tensor + ϵ[:,:,1] = (-2λr*y1 + n*(n+1)λr*y2 + rr*y3 + rr*αr*y7)/(βr*rr) * Y # e_rr + ϵ[:,:,2] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y + 0.5y2*X) # e_ + ϵ[:,:,3] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y - 0.5y2*X) # e_ + ϵ[:,:,4] = 0.5/μr * y4 * dYdθ # e_rθ + ϵ[:,:,5] = 0.5/μr * y4 * dYdϕ .* 1.0 ./ sin.(clats) # e_rϕ + ϵ[:,:,6] = 0.5 * y2/rr * Z # e_ + end + + + """ + compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl) + + Calculate the Darcy displacement vector at a particular radial level. + + # Arguments + - `dis_rel::Array{ComplexF64,4}` : 4D array to store the Darcy displacement vector at a particular radial level, with dimensions corresponding to latitude, longitude, and the 3 components of the Darcy displacement vector. + - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 8 components. + - `n::Int` : Tidal degree. + - `r::prec` : Radius at which to compute the Darcy displacement vector. + - `ω::prec` : Forcing frequency. + - `ϕ::prec` : Porosity at radius r. + - `ηl::prec` : Liquid viscosity at radius r. + - `k::prec` : Permeability at radius r. + - `g::prec` : Gravity at radius r. + - `ρl::prec` : Liquid density at radius r. + """ + function compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl) + i = 1 + + @views Y = solid1d_mush_relax.Y[i,:,:] + @views dYdθ = solid1d_mush_relax.dYdθ[i,:,:] + @views dYdϕ = solid1d_mush_relax.dYdϕ[i,:,:] + + y1 = y[1] + y5 = y[3] + y7 = y[4] + y8 = y[8] + y9 = 1im * k / (ω*ϕ*ηl*r) * (ρl*g*y1 - ρl * y5 + ρl*g*y8 + y7) + + dis_rel[:,:,1] = Y * y8 + dis_rel[:,:,2] = dYdθ * y9 + dis_rel[:,:,3] = dYdϕ * y9 ./ sin.(clats) + end + + + """ + compute_pore_pressure!(p, y, n) + + Calculate the fluid pore pressur at a particular radial level. + + # Arguments + - `p::Array{ComplexF64,4}` : 4D array to store the pore pressure at a particular radial level, with dimensions corresponding to latitude and longitude. + - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 8 components. + - `n::Int` : Tidal degree. + """ + function compute_pore_pressure!(p, y, n) + i = 1 + + @views Y = solid1d_mush_relax.Y[i,:,:] + @views dYdθ = solid1d_mush_relax.dYdθ[i,:,:] + @views dYdϕ = solid1d_mush_relax.dYdϕ[i,:,:] + + y7 = y[4] + + p[:,:] = Y * y7 + end + + + """ + get_heating_profile(y, r, ρ, g, μ, Ks, ω, ρl, Kl, Kd, α, ηl, ϕ, k, n; lay=nothing) + + Get the radial volumetric heating for two-phase tides and eccentricity forcing, + assuming synchronous rotation. Heating rate is computed with numerical integration + using the solution `y`, using Eq. 2.39a/b/c integrated over solid angle. + + # Arguments + - `y::Array{ComplexF64,2}` : 2D array [state_vector, radius] of the solution vector. + - `r::AbstractVector` : 1D vector of radial boundaries/shell coordinates. + - `ρ::AbstractVector` : 1D vector of layer densities. + - `g::AbstractVector` : 1D vector of gravity values. + - `μ::AbstractVector` : 1D vector of complex shear moduli. + - `Ks::AbstractVector` : 1D vector of bulk moduli for shear dissipation. + - `ω::Float64` : Tidal frequency in radians per second. + - `ρl::AbstractVector` : 1D vector of liquid densities. + - `Kl::AbstractVector` : 1D vector of liquid bulk moduli. + - `Kd::AbstractVector` : 1D vector of drained bulk moduli. + - `α::AbstractVector` : 1D vector of Biot coefficients. + - `ηl::AbstractVector` : 1D vector of liquid viscosities. + - `ϕ::AbstractVector` : 1D vector of porosities. + - `k::AbstractVector` : 1D vector of permeabilities. + - `n::Int` : Tidal degree. + + # Returns + - `Eμ_tot::Vector{Float64}` : Total power dissipated due to shear (W/m³). + - `Eκ_tot::Vector{Float64}` : Total power dissipated due to compaction (W/m³). + - `El_tot::Vector{Float64}` : Total power dissipated due to Darcy flow (W/m³). + """ + function get_heating_profile(y, r, ρ, g, μ, Ks, ω, ρl, Kl, Kd, α, ηl, ϕ, k, n) + + dres = deg2rad(solid1d_mush_relax.res) + clats = solid1d_mush_relax.clats + lons = solid1d_mush_relax.lons + + # Reference spherical harmonic Y if needed globally + # (Assuming the logic uses the same spatial resolution as the first example) + Y = solid1d_mush_relax.Y[1, :, :] + + Nr = length(r) + nlats = length(clats) + nlons = length(lons) + + # Pre-allocate spatial buffers + ϵ = zeros(ComplexF64, nlats, nlons, 6) + d_disp = zeros(ComplexF64, nlats, nlons, 3) + p = zeros(ComplexF64, nlats, nlons) + + # Output vectors (per radial shell) + Eμ_tot = zeros(Float64, Nr - 1) + Eκ_tot = zeros(Float64, Nr - 1) + El_tot = zeros(Float64, Nr - 1) + + for i in 1:Nr-1 + rr = r[i] + dr = r[i+1] - r[i] + dvol = 4π/3 * (r[i+1]^3 - r[i]^3) + yrr = y[:, i] + + # 1. Compute Tensors/Fields + compute_strain_ten!(ϵ, yrr, n, rr, ρ[i], g[i], μ[i], Ks[i], ω, ρl[i], Kl[i], Kd[i], α[i], ηl[i], ϕ[i], k[i]) + + if ϕ[i] > 0 + compute_darcy_displacement!(d_disp, yrr, n, rr, ω, ϕ[i], ηl[i], k[i], g[i], ρl[i]) + # In your snippet, p was being overwritten by y7 * Y immediately + p .= yrr[7] .* Y + else + p .= 0.0 + end + + # 2. Shear Heating (Solid) + Eμ_loc = sum(abs.(ϵ[:,:,1:3]).^2, dims=3) .+ + 2sum(abs.(ϵ[:,:,4:6]).^2, dims=3) .- + 1/3 .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + Eμ_loc .*= ω * imag(μ[i]) + + # 3. Compaction Heating (Bulk) + Eκ_loc = ω/2 * imag(Kd[i]) .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + if ϕ[i] > 0 + Eκ_loc .+= (ω/2 * imag(Kd[i]) .* (abs.(p) ./ Ks[i]).^2) + end + + # 4. Darcy Dissipation (Liquid) + El_loc = zeros(nlats, nlons) + if ϕ[i] > 0 + El_loc .= 0.5 * ϕ[i]^2 * ω^2 * (ηl[i] / k[i]) .* (abs.(d_disp[:,:,1]).^2 .+ abs.(d_disp[:,:,2]).^2 .+ abs.(d_disp[:,:,3]).^2) + end + + # 5. Angular Integration and Volume Averaging + weight = sin.(clats) + + Eμ_tot[i] = sum(weight .* Eμ_loc .* dres^2) * rr^2 * dr / dvol + Eκ_tot[i] = sum(weight .* Eκ_loc .* dres^2) * rr^2 * dr / dvol + El_tot[i] = sum(weight .* El_loc .* dres^2) * rr^2 * dr / dvol + end + + return Eμ_tot, Eκ_tot, El_tot + end + +end \ No newline at end of file diff --git a/src/solid1d_relax.jl b/src/solid1d_relax.jl index d80da55..9a75a54 100644 --- a/src/solid1d_relax.jl +++ b/src/solid1d_relax.jl @@ -25,7 +25,7 @@ module solid1d_relax """ - resample_profiles(radius, rho, visc, shear, bulk, grav, ncalc) + resample_profiles(radius, rho, visc, shear, bulk, m_core, dr_min, dr_max) Resample the input profiles onto a new grid with `ncalc` points. The new grid is generated using a stretched and refined scheme, which allows for better resolution in regions of interest (e.g., near @@ -49,6 +49,7 @@ module solid1d_relax - `μ_new::Vector{prec}` : New shear modulus profile at layer centers. - `κ_new::Vector{prec}` : New bulk modulus profile at layer centers. - `g_new::Vector{prec}` : New gravity profile at layer centers. + - `M_tot::Float64` : Total mass enclosed within the outermost layer boundary. """ function resample_profiles(radius, rho, visc, shear, bulk, m_core, dr_min, dr_max) # setup grids @@ -91,9 +92,9 @@ module solid1d_relax κ_new[i] = bulk[idx] end - g_new = get_g(r_new_b, ρ_new, m_core) + g_new, M_tot = get_g(r_new_b, ρ_new, m_core) - return r_new_b, ρ_new, η_new, μ_new, κ_new, g_new + return r_new_b, ρ_new, η_new, μ_new, κ_new, g_new, M_tot end @@ -109,6 +110,7 @@ module solid1d_relax # Returns - `g::Array{Float64,1}` : 1D array of gravity values at the layer boundaries. The dimensions of `g` are the same as `r`. + - `M_enc::Float64` : Total mass enclosed within the outermost layer boundary. """ function get_g(r, ρ, m_core) @@ -118,7 +120,7 @@ module solid1d_relax g = G .* M_enc ./ r[2:end].^2 - return g + return g, M_enc[end] end @@ -222,15 +224,15 @@ module solid1d_relax """ - get_scales(R0, M0, s0) + get_scales(R0, M0, g0) - Compute the characteristic scales for the problem based on a reference radius `R0`, mass `M0`, and time - scale `s0`. These scales are used to non-dimensionalize the equations and ensure numerical stability. + Compute the characteristic scales for the problem based on a reference radius `R0`, mass `M0`, and density + scale `g0`. These scales are used to non-dimensionalize the equations and ensure numerical stability. # Arguments - `R0::prec` : Reference radius scale (e.g., planetary radius). - `M0::prec` : Reference mass scale (e.g., planetary mass). - - `s0::prec` : Reference time scale (e.g., 1 day in seconds). + - `g0::prec` : Reference density scale. # Returns Tuple of characteristic scales: @@ -244,19 +246,21 @@ module solid1d_relax - `S::Diagonal{prec}` : Diagonal scaling matrix for state variables. - `Sinv::Diagonal{prec}` : Inverse of the scaling matrix S. """ - function get_scales(R0, M0, s0) + function get_scales(R0, M0, g0) - ρ0 = M0 / R0^3 # 1000 kg/m^3 - μ0 = M0 / (R0 * s0^2) # 1e9 Pa (1 GPa) - g0 = R0 / s0^2 # 0.1 m/s^2 (Note: check if you want 10 or 0.1) - G0 = R0^3 / (M0 * s0^2) # Gravity constant scaling + ρ0 = M0 / R0^3 + P0 = g0 * R0 + μ0 = ρ0 * g0 * R0 + + s0 = sqrt(g0 / R0) + G0 = R0^3 / (M0 * s0^2) S = Diagonal(precc[ R0, # y1: radial displacement (m) R0, # y2: tangential displacement (m) μ0, # y3: radial stress (Pa) μ0, # y4: tangential stress (Pa) - g0*R0, # y5: potential (m^2/s^2) + P0, # y5: potential (m^2/s^2) g0 # y6: potential gradient/gravity (m/s^2) ]) @@ -306,6 +310,7 @@ module solid1d_relax - `n::Int` : Tidal degree. # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. - `M::Int=6` : Number of rows in the Ic matrix. This should be 6 for the solid-body problem. - `N::Int=3` : Number of linearly independent solutions to compute. This should be 3 for the solid-body problem. @@ -494,31 +499,39 @@ module solid1d_relax implements the forward-backward relaxation scheme described in the main text of N. Kobayashi (2006). # Arguments - - `r::Vector{prec}` : Vector of radial grid points (layer boundaries). + - `r::Vector{prec}` : Vector of radial grid points (layer centers). - `ρ::Vector{prec}` : Vector of densities at the layer centers. - `g::Vector{prec}` : Vector of gravity values at the layer centers. - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. - `ω::prec` : Angular frequency of the tidal forcing. - `n::Int` : Tidal degree. - - `R_planet::prec` : Planetary radius, used for surface boundary conditions. - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `M_tot::prec` : Total mass of the planet, used for gravity calculations. # Keyword Arguments - `core::String="liquid" : Type of core boundary condition to apply. Options are "liquid" for a fluid core, "solid" for a solid core, and "inertial" for a core with inertial response. # Returns - - `XN::Matrix{precc}` : 6x6 matrix representing the solution at the surface (radius = R_planet). Each column corresponds to a linearly independent solution of the ODE system, and each row corresponds to a state variable (displacements, stresses, potential). - - `R::Vector{Matrix{precc}}` : Vector of 6x6 matrices representing the solution at each radial grid point. Each matrix contains the state variables (displacements, stresses, potential) for the 6-component system of ODEs. - - `b::Vector{precc}` : Vector of length 6 representing the inhomogeneous part of the surface boundary conditions. - """ - function solve_radial_system(r, ρ, g, μ, K, ω, n, R_planet, ρ_core; core="liquid") + - `y_t::Vector{precc}` : Vector of length 6 representing the tidal solution at the surface (radius = R_planet). This includes the displacements, stresses, and potential at the surface. + - `y_l::Vector{precc}` : Vector of length 6 representing the load solution at the surface (radius = R_planet). This includes the displacements, stresses, and potential at the surface. + - `R::Vector{Matrix{precc}}` : Vector of 6x6 matrices representing the coefficients of the ODE system at each radial layer. + - `S::Vector{Matrix{precc}}` : Vector of 6x6 matrices representing the normalization. + """ + function solve_radial_system(r, ρ, g, μ, K, ω, n, ρ_core, M_tot; core="liquid") Nr = length(r) + # indices for the three components of the relaxation scheme: + # 1: core boundary + # 2: forward propagation + # 3: surface boundary + ids = [(1,2), (2, Nr-1), (Nr-1, Nr)] + # non-dimensional scaling # this implementation needs to be double-checked for consistency. - R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(9.8e6, 4.3e22, 4.3e3) + R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(1., 1., 1.) + # R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(r[end], M_tot, g[end]) ωs = ω * s0 rs = r ./ R0 @@ -527,18 +540,61 @@ module solid1d_relax μs = μ ./ μ0 Ks = K ./ μ0 - # boundary conditions - B1 = get_core_bc!(ω, r[1], ρ_core, g[1], μ[1], K[1], core, n; G0=1) - BN, b = get_surface_bc!(R_planet, n) - - # storage R = Vector{Matrix{precc}}(undef, Nr) + # component 1: apply core boundary condition and get first solution + C1l, D2l = core_boundary(R, ids[1], rs, ρs, gs, μs, Ks, ωs, ρ_core/ρ0, core, n; G0=G0) + + # component 2: propagate the solution up to the surface (6x6) + C1l, D2l = propagate_solid(R, C1l, D2l, ids[2], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) + + # component 3: apply surface boundary condition and solve for the final solution at the surface + y_t, y_l = surface_boundary(R, C1l, D2l, ids[3], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) + + return y_t, y_l, R, S + + end + + + """ + core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n) + + Perform the forward-backward relaxation step at the core boundary. This function implements the recursion described + in N. Kobayashi (2007) for the initial step of the relaxation scheme, where we apply the core boundary condition and + get the first solution for the first layer above the core. + + # Arguments + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `core::String` : Type of core boundary condition to apply. + - `n::Int` : Tidal degree. + + Keyword Arguments + - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + + # Returns + - `C1l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the C1 matrix for the next iteration. + - `D2l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the D2 matrix for the next iteration. + """ + function core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n; G0=1) + + start_id, end_id = ids + + # boundary conditions + B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ[start_id], K[start_id], core, n; G0=G0) + # first layer (n = 1) - dr = rs[2] - rs[1] + dr = r[end_id] - r[start_id] - A1 = S * get_A(ωs, rs[1], ρs[1], gs[1], μs[1], Ks[1], n; G0=G0) * Sinv - A2 = S * get_A(ωs, rs[2], ρs[2], gs[2], μs[2], Ks[2], n; G0=G0) * Sinv + A1 = get_A(ω, r[start_id], ρ[start_id], g[start_id], μ[start_id], K[start_id], n; G0=G0) + A2 = get_A(ω, r[end_id], ρ[end_id], g[end_id], μ[end_id], K[end_id], n; G0=G0) I6 = Matrix{precc}(I, 6, 6) @@ -554,59 +610,147 @@ module solid1d_relax Q1 = [zeros(3,6); D2u] # 6×6 # initial recursion - R[1] = -pinv(S1) * Q1 + R[start_id] = -S1 \ Q1 + + return C1l, D2l + end + + + """ + propagate_solid(R, B, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, n) + + Perform the forward-backward relaxation step for the solid propagation segments. This function implements the + recursion described in N. Kobayashi (2007) for the segments of the radial grid that correspond to the solid + layers, where we propagate the solution up to the surface using the 6x6 system of equations. + + # Arguments + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. + - `Cn_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Cn matrix from the previous step. + - `Dnp_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Dnp matrix from the previous step. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `n::Int` : Tidal degree. + + Keyword Arguments + - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + + # Returns + - `Cn_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Cn matrix for the next iteration. + - `Dnp_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. + """ + function propagate_solid(R, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, n; G0=1) + + start_id, end_id = ids + + I6 = Matrix{precc}(I, 6, 6) + + Cn_u = zeros(3,6) + Dnp_u = zeros(3,6) # forward recursion - for i in 2:Nr-1 + for i in start_id:end_id - dr = rs[i+1] - rs[i] + dr = r[i+1] - r[i] - A_n = S * get_A(ωs, rs[i], ρs[i], gs[i], μs[i], Ks[i], n; G0=G0) * Sinv - A_np = S * get_A(ωs, rs[i+1], ρs[i+1], gs[i+1], μs[i+1], Ks[i+1], n; G0=G0) * Sinv + # Calculate A at current and next step + A_n = get_A(ω, r[i], ρ[i], g[i], μ[i], K[i], n; G0=G0) + A_np = get_A(ω, r[i+1], ρ[i+1], g[i+1], μ[i+1], K[i+1], n; G0=G0) Cn = I6 + 0.5 * dr * A_n Dnp = -I6 + 0.5 * dr * A_np - # split - Cn_u, Cn_l = Cn[1:3, :], Cn[4:6, :] - Dnp_u, Dnp_l = Dnp[1:3, :], Dnp[4:6, :] + # 1. Use the "stored" lower halves from the previous step + # to fill the upper blocks of P and S. + Pn_u = Cn_l + Sn_u = Dnp_l + Qn_u = zeros(precc, 3, 6) - # build blocks - Pn = [Cn_l; zeros(3,6)] - Sn = [Dnp_l; Cn_u] - Qn = [zeros(3,6); Dnp_u] + # 2. Get the upper halves of the NEWLY calculated Cn and Dnp + Cn_u = Cn[1:3, :] + Dnp_u = Dnp[1:3, :] - # recursion + # 3. Build the 6x6 blocks + Pn = [Pn_u; zeros(precc, 3, 6)] + Sn = [Sn_u; Cn_u] + Qn = [Qn_u; Dnp_u] + + # 4. Perform recursion Xn = Pn * R[i-1] + Sn - R[i] = -pinv(Xn) * Qn + R[i] = -Xn \ Qn + + # 5. Update the "stored" lower halves for the next iteration + Cn_l = Cn[4:6, :] + Dnp_l = Dnp[4:6, :] end - # final layer (n = N) - dr = rs[Nr] - rs[Nr-1] + return Cn_l, Dnp_l + end + + + """ + surface_boundary(R, B, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n, R_planet) - A_Nm = S * get_A(ωs, rs[end-1], ρs[end-1], gs[end-1], μs[end-1], Ks[end-1], n; G0=G0) * Sinv - A_N = S * get_A(ωs, rs[end], ρs[end], gs[end], μs[end], Ks[end], n; G0=G0) * Sinv + Perform the forward-backward relaxation step at the surface boundary. This function implements the recursion described + in N. Kobayashi (2007) for the final step of the relaxation scheme, where we apply the surface boundary condition and + solve for the final solution at the surface, using the 6x6 system of equations. + + # Arguments + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. + - `CNm_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the CNm matrix from the previous step. + - `DN_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the DN matrix from the previous step. + - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + - `r::Vector{prec}` : Vector of radial grid points (layer centers). + - `ρ::Vector{prec}` : Vector of densities at the layer centers. + - `g::Vector{prec}` : Vector of gravity values at the layer centers. + - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. + - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. + - `ω::prec` : Angular frequency of the tidal forcing. + - `n::Int` : Tidal degree. + + Keyword Arguments + - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + + # Returns + - `y_t::Matrix{precc}` : 6x1 matrix representing the solution at the surface for the tidal problem. + - `y_l::Matrix{precc}` : 6x1 matrix representing the solution at the surface for the load problem. + """ + function surface_boundary(R, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n; G0=1) - CNm = I6 + 0.5 * dr * A_Nm - DN = -I6 + 0.5 * dr * A_N + start_id, end_id = ids - CNm_l = CNm[4:6, :] - DN_l = DN[4:6, :] + # tidal surface boundary condition + BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0) + # load surface boundary condition + BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0) PN = [CNm_l; zeros(3,6)] - SN = [DN_l; BN] + SN_t = [DN_l; BN_t] + SN_l = [DN_l; BN_l] - XN = PN * R[Nr-1] + SN + XN_t = PN * R[start_id] + SN_t + XN_l = PN * R[start_id] + SN_l + + # solve outer (tides) + y_t = XN_t \ b_t + # solve outer (load) + y_l = XN_l \ b_l + + return y_t, y_l - return XN, R, b end """ - get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1) + get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1, M=6, N=3) - Get the core boundary condition matrix `B` and vector `b` for the solid-body problem. The core - boundary conditions are derived from the requirement that the radial stress at the core-mantle + Get the core boundary condition matrix `B` for the solid-body problem. The core boundary + conditions are derived from the requirement that the radial stress at the core-mantle boundary must balance the tidal potential, and that the tangential stresses must vanish. # Arguments @@ -621,14 +765,15 @@ module solid1d_relax # Keyword Arguments - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `M::Int=6` : Number of rows in the Ic matrix. This should be 6 for the solid-body problem. + - `N::Int=3` : Number of linearly independent solutions to compute. This should be 3 for the solid-body problem. # Returns - `B::Array{precc,2}` : 3x6 matrix representing the linear relationship between the state variables at the core and the boundary conditions. - - `b::Vector{precc}` : Vector of length 6 representing the inhomogeneous part of the core boundary conditions. """ - function get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1) + function get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1, M=6, N=3) - Ic = get_Ic(ω, r, ρ, g, μ, K, type, n; G0=G0, M=6, N=3) + Ic = get_Ic(ω, r, ρ, g, μ, K, type, n; G0=G0, M=M, N=N) # Define indices based on Takeuchi & Saito (1972) # u vectors (Displacements/Potential): U=1, V=2, phi=5 @@ -644,7 +789,7 @@ module solid1d_relax # Construct the 3x6 B matrix T = eltype(b) - B = zeros(T, 3, 6) + B = zeros(T, 3, M) for i in 1:3 B[i, idx_u[i]] = 1.0 # Identity for u components @@ -661,40 +806,62 @@ module solid1d_relax get_surface_bc!(R, n) Get the surface boundary condition vector `b` and matrix `BN` for the solid-body problem. The surface - boundary conditions are derived from the requirement that the radial stress at the surface must balance - the tidal potential, and that the tangential stresses must vanish. + boundary conditions are determined by setting, respectively (U, U', tau, P) to (1,0,0,0) for tidal Love + number and (0,1,0,0) for load Love number in system. + + https://hal.science/hal-03421553/document # Arguments - - `R::prec` : Planetary radius, used for surface boundary conditions. + - `R::prec` : Mantle radius, used for surface boundary conditions. + - `g::prec` : Gravity at the surface, used for surface boundary conditions. - `n::Int` : Tidal degree. + - `U::Int` : Tidal potential at the surface. + - `U_prime::Int` : Radial derivative of the tidal potential at the surface. + - `tau::Int` : Tangential tidal stress at the surface. + - `P::Int` : Surface mass load at the surface. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. # Returns - `BN::Array{precc,2}` : 3x6 matrix representing the linear relationship between the state variables at the surface and the boundary conditions. - `b::Vector{precc}` : Vector of length 6 representing the inhomogeneous part of the surface boundary conditions. """ - function get_surface_bc!(R, n) + function get_surface_bc!(R, g, n, U, U_prime, tau, P; G0=1) - # first three elements of b are zero, last three - # elements are given in the documentation. + # Define surface mass load (zeta) based on Farrell/Longman relation + zeta = ((2 * n + 1) / (4 * pi * G/G0 * R)) * U_prime + + # b vector (Right Hand Side of the B*y = b system) b = zeros(precc, 6) - b[6] = (2n+1)/R - # s vectors (Stresses/Potential Flux): X=3, Y=4, psi=6 - idx_s = [3, 4, 6] - - # Construct the 3x6 B matrix - B = zeros(3, 6) + # radial Stress y3 + b[4] = -g * zeta * (G/G0) / R - P + + # tangential Stress y4 + b[5] = tau + + # gravitational potential boundary + b[6] = ((2 * n + 1) / R) * U + 4 * pi * G/G0 * zeta + + # construct the 3x6 B matrix + # this matrix extracts y3, y4, and the combination for y6 + B = zeros(precc, 3, 6) - for i in 1:3 - B[i, idx_s[i]] = 1.0 # Identity for s components - end + # stress components + B[1, 3] = 1.0 # radial stress y3 + B[2, 4] = 1.0 # tangential stress y4 + + # potential component + B[3, 5] = (n + 1) / R + B[3, 6] = 1.0 return B, b end """ - compute_y_relaxation(r, ρ, g, μ, K, ω, n, R, ρ_core; core="liquid") + compute_y(r, ρ, g, μ, K, ω, n, R, ρ_core; core="liquid") Compute the solution `y` to the solid-body problem using a relaxation method. This function performs the forward-backward relaxation scheme described in the main text of N. Kobayashi (2006), where we first solve @@ -702,15 +869,15 @@ module solid1d_relax compute the solution at all interior points. # Arguments - - `r::Vector{prec}` : Vector of radial grid points (layer boundaries). + - `r::Vector{prec}` : Vector of radial grid points (layer centers). - `ρ::Vector{prec}` : Vector of densities at the layer centers. - `g::Vector{prec}` : Vector of gravity values at the layer centers. - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. - `ω::prec` : Angular frequency of the tidal forcing. - `n::Int` : Tidal degree. - - `R::prec` : Planetary radius, used for surface boundary conditions. - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `M_tot::prec` : Total mass of the planet, used for non-dimensionalization. # Keyword Arguments - `core::String="liquid" : Type of core boundary condition to apply. Options are "liquid" for a fluid core, "solid" for a solid core, and "inertial" for a core with inertial response. @@ -718,26 +885,34 @@ module solid1d_relax # Returns - `y::Matrix{precc}` : 6xN matrix of the solution at all radial grid points, where N is the number of radial layers. Each column corresponds to a radial grid point, and each row corresponds to a state variable (displacements, stresses, potential). """ - function compute_y_relaxation(r, ρ, g, μ, K, ω, n, R, ρ_core; core="liquid") + function compute_y(r, ρ, g, μ, K, ω, n, ρ_core, M_tot; core="liquid") # solve radial system to get surface solution and recursion matrices - XN, R, b = solve_radial_system(r, ρ, g, μ, K, ω, n, R, ρ_core; core=core) + yN_t, yN_l, R, S = solve_radial_system(r, ρ, g, μ, K, ω, n, ρ_core, M_tot; core=core) Nr = length(r) - T = eltype(XN) + T = eltype(yN_t) # allocate as 6 x N matrix - y = Matrix{T}(undef, 6, Nr) + y_t = Matrix{T}(undef, 6, Nr) + y_l = Matrix{T}(undef, 6, 1) - # solve outer boundary - y[:, Nr] = pinv(XN) * b + # solve outer (tides) + y_t[:, Nr] = yN_t + # solve outer (load) + y_l[:, 1] = yN_l # back-substitution for i in Nr-1:-1:1 - y[:, i] = R[i] * y[:, i+1] + y_t[:, i] = R[i] * y_t[:, i+1] end - return y + # # apply scaling to get dimensional solution + # for i in 1:Nr + # y_t[:, i] = S * y_t[:, i] + # end + + return y_t, y_l end @@ -785,30 +960,25 @@ module solid1d_relax """ - function get_heating_profile(y, r, ρ, g, μ, κ, n, ω; lay=nothing) + function get_heating_profile(y, r, ρ, g, μ, κ, n, ω) Get the radial volumetric heating for solid-body tides and eccentricity forcing, assuming synchronous rotation. Heating rate is computed with numerical integration - using the solution `y`, using Eq. 2.39a/b integrated over solid angle. The heating - profile for a specific layer is specified with `lay`, otherwise all layers will be - caclulated. + using the solution `y`, using Eq. 2.39a/b integrated over solid angle. # Arguments - - `y::Array{ComplexF64,4}` : 4D array of the solution vector y across the interior, returned by `compute_y`. - - `r::Array{Float64,2}` : 2D array of layer boundaries. - - `ρ::Array{Float64,1}` : 1D array of layer densities. - - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. - - `μ::Array{Float64,1}` : 1D array of layer shear moduli. - - `κ::Array{Float64,1}` : 1D array of layer bulk moduli. - - `n::Int` : Tidal degree. - - `ω::Float64` : Tidal frequency in radians per second. - - # Keyword Arguments - - `lay::Int=nothing` : If specified, compute the heating profile for only the layer corresponding to this index. Otherwise, compute for all layers. + - `y::Array{ComplexF64,6}` : 6D array of the solution vector y across the interior, returned by `compute_y`. + - `r::AbstractVector` : 1D vector of radial coordinates or shell boundaries. + - `ρ::AbstractVector` : 1D vector of densities at each radial shell. + - `g::AbstractVector` : 1D vector of gravitational acceleration values at each radial shell. + - `μ::AbstractVector` : 1D vector of complex shear moduli at each radial shell. + - `κ::AbstractVector` : 1D vector of complex bulk moduli at each radial shell. + - `n::Int` : Tidal degree. + - `ω` : Tidal frequency in radians per second. # Returns - - `Eμ_tot::Array{Float64,1}` : 1D array of total power dissipated in each primary layer due to shear, in W. - - `Eκ_tot::Array{Float64,1}` : 1D array of total power dissipated in each primary layer due to compaction, in W. + - `Eμ_tot::Array{Float64,1}` : 1D array of total power dissipated in each radial shell due to shear, in W. + - `Eκ_tot::Array{Float64,1}` : 1D array of total power dissipated in each radial shell due to compaction, in W. """ function get_heating_profile(y, r, ρ, g, μ, κ, n, ω) @@ -825,9 +995,6 @@ module solid1d_relax ϵ = zeros(ComplexF64, nlats, nlons, 6) # outputs - Eμ_vol = zeros(Float64, Nr-1) - Eκ_vol = zeros(Float64, Nr-1) - Eμ_tot = zeros(Float64, Nr-1) Eκ_tot = zeros(Float64, Nr-1) From 7b925db143fcd7838d5e0b725074197b1dd2cff4 Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 27 Apr 2026 19:44:20 +0000 Subject: [PATCH 11/36] Updated run tests, expected heating rates are lower now due to inclusion of core in gravity calculation. --- test/runtests.jl | 134 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 115 insertions(+), 19 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 8e3b959..a97f10f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -68,12 +68,24 @@ if suite >= 0 @warn "Fail" failed += 1 end + if isdefined(Obliqua, :run_solid1d_relax) + @info "Pass" + else + @warn "Fail" + failed += 1 + end if isdefined(Obliqua, :run_solid1d_mush) @info "Pass" else @warn "Fail" failed += 1 end + if isdefined(Obliqua, :run_solid1d_mush_relax) + @info "Pass" + else + @warn "Fail" + failed += 1 + end total += 3 @info "--------------------------" end @@ -140,9 +152,9 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 1.2930879671213278e-12, 1.2219771504808403e-12, 1.1736558859289374e-12, 1.1314931658945296e-12, 1.0916026417910732e-12, 1.0538251910982026e-12, 1.5883496284658382e-15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 1.731021692653176e7 - imag_k2_expt = [0.0237182181024913] + power_prf_expt = [0.0, 0.0, 3.4054467488462404e-15, 3.221172119001604e-15, 3.1295744034823437e-15, 3.0848071238085414e-15, 3.0748087696203486e-15, 3.097587698655655e-15, 6.812344431592964e-16, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 49253.31079112116 + imag_k2_expt = [6.748620035044579e-5] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -178,9 +190,93 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 3.3410413059922125e-16, 3.0141311725796533e-16, 2.871071975586501e-16, 2.7665275365727065e-16, 2.6681804901946297e-16, 2.5750291702649744e-16, 7.077614201094658e-25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 4280.179309796564 - imag_k2_expt = [5.8646420676523305e-6] + power_prf_expt = [0.0, 0.0, 8.439500899702699e-19, 7.620980552195005e-19, 7.343006660817232e-19, 7.233683944336647e-19, 7.207110036818358e-19, 7.257082672043938e-19, 2.791755081097162e-25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 11.206735073152927 + imag_k2_expt = [1.5355312288113298e-8] + + power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( + omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg + ) + test_pass = true + + test_pass &= all(isapprox.(power_prf, power_prf_expt; rtol=rtol, atol=atol)) + test_pass &= isapprox(power_blk, power_blk_expt; rtol=rtol) + test_pass &= all(isapprox.(imag_k2, imag_k2_expt; rtol=rtol)) + + @info "First 5 expected profile elements: $(power_prf_expt[1:5])" + @info "First 5 modelled profile elements: $(power_prf[1:5])" + @info "Expected total power = $(power_blk_expt) W" + @info "Modelled total power = $(power_blk) W" + @info "Expected imag(k2): $(imag_k2_expt)" + @info "Modelled imag(k2): $(imag_k2)" + + if test_pass + @info "Pass" + else + @warn "Fail" + failed += 1 + end + total += 1 + @info "--------------------------" + +end + +if suite > 2 + # test solid1d module with andrade rheology + @info " " + @info "Testing solid1d-relax module with andrade rheology" + + # update config to use only solid1d + cfg["orbit"]["obliqua"]["module_solid"] = "solid1d-relax" + cfg["orbit"]["obliqua"]["module_fluid"] = "none" + cfg["orbit"]["obliqua"]["module_mushy"] = "none" + + cfg["orbit"]["obliqua"]["material"] = "andrade" + + omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = + load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) + + power_prf_expt = [0.0, 1.2500871386903165e-14, 1.3490093179591017e-14, 1.379776907343251e-14, 1.4367432888221434e-14, 1.5036981510041056e-14, 1.576511111912771e-14, 1.6552497289716098e-14, 1.8304993632021006e-15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 248561.9899812635 + imag_k2_expt = [0.0003405761762193043] + + power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( + omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg + ) + test_pass = true + + test_pass &= all(isapprox.(power_prf, power_prf_expt; rtol=rtol, atol=atol)) + test_pass &= isapprox(power_blk, power_blk_expt; rtol=rtol) + test_pass &= all(isapprox.(imag_k2, imag_k2_expt; rtol=rtol)) + + @info "First 5 expected profile elements: $(power_prf_expt[1:5])" + @info "First 5 modelled profile elements: $(power_prf[1:5])" + @info "Expected total power = $(power_blk_expt) W" + @info "Modelled total power = $(power_blk) W" + @info "Expected imag(k2): $(imag_k2_expt)" + @info "Modelled imag(k2): $(imag_k2)" + + if test_pass + @info "Pass" + else + @warn "Fail" + failed += 1 + end + total += 1 + @info "--------------------------" + + @info " " + @info "Testing solid1d-relax module with maxwell rheology" + + # update config to use maxwell rheology + cfg["orbit"]["obliqua"]["material"] = "maxwell" + + omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = + load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) + + power_prf_expt = [0.0, 3.061317898445062e-18, 3.31954900407594e-18, 3.2426735038389572e-18, 3.3496980944013153e-18, 3.505053284163295e-18, 3.674843274104151e-18, 3.85852228062806e-18, 7.519018418066546e-25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 57.809566146923785 + imag_k2_expt = [7.92098622508629e-8] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -224,9 +320,9 @@ if suite > 4 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 3.9458586892328636e-15, 3.684375027447792e-15, 3.5332688538838223e-15, 3.4385147729212697e-15, 3.3857777200941584e-15, 3.3721836008236663e-15, 1.2678042929823814e-15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 61564.69369317412 - imag_k2_expt = [8.435508570604252e-5] + power_prf_expt = [0.0, 0.0, 4.950568223680489e-14, 4.5676008465905126e-14, 4.282121624939781e-14, 4.0271210724467955e-14, 3.787104918976475e-14, 3.560948831486367e-14, 1.2385369527824588e-11, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 4.805276752962792e8 + imag_k2_expt = [0.6584123269704052] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -254,7 +350,7 @@ if suite > 4 @info "--------------------------" @info " " - @info "Testing solid1d module with maxwell rheology" + @info "Testing solid1d-mush module with maxwell rheology" # update config to use maxwell rheology cfg["orbit"]["obliqua"]["material"] = "maxwell" @@ -262,9 +358,9 @@ if suite > 4 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 9.412009210814184e-19, 8.391526074045509e-19, 7.984510236608038e-19, 7.771471545020786e-19, 7.656366535345375e-19, 7.630908981500375e-19, 1.1998652842288277e-15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 3139.7068234228814 - imag_k2_expt = [4.3019825535337385e-6] + power_prf_expt = [0.0, 0.0, 1.2000536007632784e-17, 1.0577415996677013e-17, 9.837740538298375e-18, 9.249727566534158e-18, 8.698383380619814e-18, 8.179031833355627e-18, 1.2370422448018447e-11, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 4.8114259486262393e8 + imag_k2_expt = [0.6592548811944196] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -344,9 +440,9 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.9150692840613072e-13, 2.32176469195039e-13, 2.2984543955634825e-13, 2.275218082497258e-13, 1.8312731634158356e-13, 1.3840633350642642e-13, 1.0446190925830448e-13, 7.863674415674511e-14] - power_blk_expt = 1.2647997272570137e7 - imag_k2_expt = [0.017330109677062184] + power_prf_expt = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 9.038225140970252e-14, 9.038225140970252e-14, 9.038225140970252e-14, 9.038225140970252e-14, 7.352121265265731e-14, 5.620007993230152e-14, 4.295071065802233e-14, 3.282134829196775e-14] + power_blk_expt = 5.173115797760875e6 + imag_k2_expt = [0.007088131204911426] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -391,9 +487,9 @@ if suite > 10 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 5.348438930694959e-10, 5.054382700817955e-10, 4.854545590088724e-10, 4.6801736056944e-10, 4.5151996680864546e-10, 4.3589657868479087e-10, 6.567896224392372e-13, 6.567896224392372e-13, 2.2221337785775897e-13, 7.486017119771332e-14, 2.511263039415512e-14, 8.389146324289135e-15, 2.7909384049643753e-15, 9.247231915500194e-16, 3.0515727768368006e-16, 1.0030141378763135e-16, 3.283839902745504e-17, 1.0709474724406045e-17, 3.479242855318201e-18, 1.1260307470708736e-18, 3.6306359011179086e-19, 1.1662712622133663e-19, 3.7326550015850954e-20, 1.1902933304838137e-20, 3.7820216100559424e-21, 1.1974113738139303e-21, 3.777697398081894e-22, 1.187654086455705e-22, 3.720882354278839e-23, 1.1617396519825671e-23, 3.614863513517372e-24, 1.1210064802892951e-24, 3.464734689808167e-25, 1.0673076400893613e-25, 3.2770176513763044e-26, 1.0028797513345896e-26, 3.059221341787089e-27, 9.301983893760305e-28, 2.8193777248554025e-28, 8.518320338741817e-29, 2.5655908770677535e-29, 7.703054921027464e-30, 2.305633125385449e-30, 6.879897808388008e-31, 2.0468875703213448e-31, 6.079540588563313e-32, 1.82809769862545e-32, 6.424949729505258e-33, 5.415085000960695e-33, 1.3628910622963522e-32, 4.522144596189985e-32, 1.5481839048545105e-31, 5.328997698087551e-31, 1.8400365756743336e-30, 6.372000678813409e-30, 2.2129966861902714e-29, 7.707883477528396e-29, 2.692362472722205e-28, 9.431282592662013e-28, 3.313155806917663e-27, 1.167191340904073e-26, 4.1235072197865054e-26, 1.4608699612294494e-25, 5.190058185883491e-25, 1.849033188695096e-24, 6.605795306391037e-24, 2.3665175151102258e-23, 8.501488679062831e-23, 3.062509781173761e-22, 1.1062517926728058e-21, 4.007018930387838e-21, 1.4553811847848123e-20, 5.3005052437310766e-20, 1.935709883553861e-19, 7.088318551714252e-19, 2.602697852906662e-18, 9.582516775281375e-18, 3.5375989609502625e-17, 1.30950986866763e-16, 4.860469684006445e-16, 1.808902768201557e-15, 6.750222438409277e-15, 2.5257205107513347e-14, 9.475789973937655e-14, 3.5645702881982644e-13, 1.34449552742012e-12, 5.084764816251399e-12, 1.928149568428732e-11, 7.331081060740859e-11, 7.331081060740859e-11, 8.88794915973433e-11, 8.798711294233438e-11, 8.709756474695825e-11, 7.010282387306757e-11, 5.298315256901038e-11, 3.998889011221784e-11, 3.010277455399582e-11] - power_blk_expt = 1.2866082902183443e10 - imag_k2_expt = [0.046417892444006494, 0.045639480552623346, 0.04487993352593461, 0.0441413835094393, 0.04342634229137915, 0.04273779394964199, 0.04207931712777366, 0.041455248954265535, 0.040870908664398777] + power_prf_expt = [0.0, 0.0, 1.4096131769666246e-12, 1.3333546715265303e-12, 1.2954481329556693e-12, 1.2769257986342845e-12, 1.2727969752559767e-12, 1.2822371915505641e-12, 2.821229930633951e-13, 2.821229930633951e-13, 9.545142176139973e-14, 3.2156073783718976e-14, 1.0787092561209404e-14, 3.603545167880893e-15, 1.1988433881459088e-15, 3.972134541138589e-16, 1.3107984900166746e-16, 4.308432121866323e-17, 1.4105684841104198e-17, 4.600238737276539e-18, 1.4945035280724368e-18, 4.836848113787542e-19, 1.5595341828677605e-19, 5.009700640480185e-20, 1.60335633381245e-20, 5.11288707291592e-21, 1.6245616861252033e-21, 5.143462520828592e-22, 1.6227042273848713e-22, 5.101550239945426e-23, 1.5982994108939882e-23, 4.99023517698882e-24, 1.5527591775302929e-24, 4.815266451502689e-25, 1.488271567429775e-25, 4.5846038922348656e-26, 1.407637996310444e-26, 4.307854866500501e-27, 1.3140839195708988e-27, 3.995653170935623e-28, 1.2110594551788732e-28, 3.659031671526164e-29, 1.1020456979480327e-29, 3.3088356720414075e-30, 9.903813466608894e-31, 2.9552534406743966e-31, 8.79249909254516e-32, 2.611876840721646e-32, 7.866748453820105e-33, 2.807937807861988e-33, 2.4897343541112928e-33, 6.412894297136211e-33, 2.1336786059339796e-32, 7.306547572174945e-32, 2.515035677540206e-31, 8.684120259406213e-31, 3.007289583179626e-30, 1.0444320863283046e-29, 3.637764520833359e-29, 1.270670568627847e-28, 4.451129198392355e-28, 1.5636563114413197e-27, 5.5086033172775255e-27, 1.9461047005488956e-26, 6.894630582425686e-26, 2.4494674298625543e-25, 8.726581495295039e-25, 3.1176298746233385e-24, 1.116886818577161e-23, 4.0123094730131237e-23, 1.4453629793639127e-22, 5.220996833425348e-22, 1.891127615394336e-21, 6.868726096099303e-21, 2.501593333124611e-20, 9.13565540811195e-20, 3.3453585303030626e-19, 1.2283530149639029e-18, 4.5225047381945086e-18, 1.6695830999220718e-17, 6.180280947734556e-17, 2.29391690004367e-16, 8.537184470396599e-16, 3.1857928013568695e-15, 1.1920232695749766e-14, 4.4721346239449414e-14, 1.6823123189920694e-13, 6.345397076604935e-13, 2.3997738290864037e-12, 9.099974217275072e-12, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 2.8144721960568983e-11, 2.1514002847231398e-11, 1.64419993296216e-11, 1.2564368592020458e-11] + power_blk_expt = 1.5125953109300022e9 + imag_k2_expt = [0.006259307269295431, 0.005976347265592152, 0.005693513740336182, 0.005410806145990414, 0.005128224634243445, 0.004845770227394191, 0.004563445045070873, 0.004281252608909295, 0.003999198259291266] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg From efe255144b0f4f4d06439443733ab5d4d8c87750 Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 27 Apr 2026 19:54:01 +0000 Subject: [PATCH 12/36] Fixed small nomralization issue, still needs further work. --- src/solid1d_mush_relax.jl | 4 ++-- src/solid1d_relax.jl | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/solid1d_mush_relax.jl b/src/solid1d_mush_relax.jl index dc460b7..dd3de40 100644 --- a/src/solid1d_mush_relax.jl +++ b/src/solid1d_mush_relax.jl @@ -1313,9 +1313,9 @@ module solid1d_mush_relax start_id, end_id = ids # tidal surface boundary condition - BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0; M=6, N=3) + BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0; G0=G0, M=6, N=3) # load surface boundary condition - BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0; M=6, N=3) + BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0; G0=G0, M=6, N=3) PN = [CNm_l; zeros(3,6)] SN_t = [DN_l; BN_t] diff --git a/src/solid1d_relax.jl b/src/solid1d_relax.jl index 9a75a54..ed46a1d 100644 --- a/src/solid1d_relax.jl +++ b/src/solid1d_relax.jl @@ -725,9 +725,9 @@ module solid1d_relax start_id, end_id = ids # tidal surface boundary condition - BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0) + BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0; G0=G0) # load surface boundary condition - BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0) + BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0; G0=G0) PN = [CNm_l; zeros(3,6)] SN_t = [DN_l; BN_t] @@ -803,7 +803,7 @@ module solid1d_relax """ - get_surface_bc!(R, n) + get_surface_bc!(R, g, n, U, U_prime, tau, P; G0=1) Get the surface boundary condition vector `b` and matrix `BN` for the solid-body problem. The surface boundary conditions are determined by setting, respectively (U, U', tau, P) to (1,0,0,0) for tidal Love From 200669f86473c63c0688b8e035fe8d266a00f874 Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 27 Apr 2026 19:55:06 +0000 Subject: [PATCH 13/36] Updated deps. --- Project.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Project.toml b/Project.toml index 882855b..ad5abbd 100644 --- a/Project.toml +++ b/Project.toml @@ -7,22 +7,30 @@ version = "0.1.0" AssociatedLegendrePolynomials = "2119f1ac-fb78-50f5-8cc0-dda848ebdb19" DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +GenericLinearAlgebra = "14197337-ba66-59df-a3e3-ca00e7dcff7a" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" +NCDatasets = "85f8d34a-cbdd-5861-8df4-14fed0d494ab" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [compat] FFTW = "1.10.0" +GenericLinearAlgebra = "0.4.0" Interpolations = "0.16.2" JSON = "1.3.0" LoggingExtras = "1.2.0" +NCDatasets = "0.14.15" Plots = "1.41.2" Printf = "1.11.0" +SparseArrays = "1.11.0" +SpecialFunctions = "2.7.2" Statistics = "1.11.1" TOML = "1.0.3" From c51e10e11c5b0df3bd32b1469b40e217325a6a97 Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 27 Apr 2026 20:30:10 +0000 Subject: [PATCH 14/36] Updated docs for solid1d. --- Project.toml | 2 +- docs/src/development.md | 1 + docs/src/reference/forcing-frequency.md | 17 +++-- docs/src/reference/solid-phase.md | 95 +++---------------------- docs/src/reference/surface-loading.md | 5 +- docs/src/reference/tidal-potentials.md | 38 ---------- src/Obliqua.jl | 3 +- 7 files changed, 21 insertions(+), 140 deletions(-) diff --git a/Project.toml b/Project.toml index ad5abbd..90b3c91 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Obliqua" uuid = "70fbf7c6-2f8c-431c-b73b-6bd747db0edf" -authors = ["Marijn van Dijk ", "Tim Lichtenberg ", "Harrison Nicholls ", "Hamish Hay "] +authors = ["Marijn van Dijk ", "Tim Lichtenberg ", "Harrison Nicholls "] version = "0.1.0" [deps] diff --git a/docs/src/development.md b/docs/src/development.md index cd0321e..15f494b 100644 --- a/docs/src/development.md +++ b/docs/src/development.md @@ -24,6 +24,7 @@ Modules = [ Obliqua.solid1d, Obliqua.solid1d_mush, Obliqua.solid1d_relax, + Obliqua.solid1d_mush_relax, Obliqua.fluid0d, Obliqua.Hansen ] diff --git a/docs/src/reference/forcing-frequency.md b/docs/src/reference/forcing-frequency.md index f3ff7ac..85dce11 100644 --- a/docs/src/reference/forcing-frequency.md +++ b/docs/src/reference/forcing-frequency.md @@ -3,23 +3,22 @@ # Forcing Frequency -Given the fact that the tidal forcing magnitude decreases exponentially with harmonnic order, we may limit the calculation to only the lowest harmonnic degree ``n = 2``. Generally, in the limit of ``R_p \ll a``, it suffices to only consider the quadrupolar harmonic (``n = 2``). Nevertheless, Obliqua can also determine higher degree contributions. As of now, we are considering a coplanar geometry. Hence, terms with ``\ell = 1`` associated with obliquity tides vanish. The Fourier modes of the second harmonnic have frequencies given by +Given the fact that the tidal forcing magnitude decreases exponentially with harmonic order, we may limit the calculation to only the lowest harmonic degree ``n = 2``. Generally, in the limit of ``R_p \ll a``, it suffices to only consider the quadrupolar harmonic (``n = 2``). Nevertheless, Obliqua can also determine higher degree contributions. As of now, we are considering a coplanar geometry. Hence, terms with ``m = 1`` associated with obliquity tides vanish. The Fourier modes of the second harmonic have frequencies given by ```math -\sigma = 2\Omega - k n_{\mathrm{orb}}, +\sigma = m\Omega - k n_{\mathrm{orb}}, ``` -where ``\Omega`` is spin rate and ``n_{\mathrm{orb}}`` orbital mean motion, and for integer values of ``k``. In our formalism tides are occuring over a large time interval, a time step ``\Delta t``. As such, we must account for tidal excitations that occur over a wide range of frequencies. We calculate the imaginary part of the second harmonnic degree (``k_2``) Love number (``\Im[k_{2}(\sigma)]``) for a handful of frequencies. The resulting descrete profile can subsequently be interpolated across positive and negative frequencies to yield a continous profile. - -Note, since we are only interested in the the Fourier modes of the second harmonnic, we don't actually use the entire spectrum. We can determine which frequencies are relevant using the Hansen Coefficients. +where ``\Omega`` is spin rate and ``n_{\mathrm{orb}}`` orbital mean motion, and for integer values of order ``-2 \leq m \leq 2`` and harmonic ``\infty \leq k \leq \infty``. In our formalism tides are occuring over a large time interval, a time step ``\Delta t``. As such, we must account for tidal excitations that occur over a wide range of frequencies. We calculate the imaginary part of the ``n``th harmonic degree (``k_n``) Love number (``\Im[k_{n}(\sigma)]``) for all relevant harmonnic frequencies for which the Hansen coefficient ```math -X_{22k}(e) = +X_{nmk}(e) = \frac{1}{2\pi} \int_0^{2\pi} -\left(\frac{r}{a}\right)^2 -e^{2i v - ikM}\,dM, +\left(\frac{r}{a}\right)^n +e^{im\Omega - ikn_{\mathrm{orb}}}\,dn_{\mathrm{orb}} \geq 0.01, ``` -where ``v`` is the true anomoly. +(i.e. ~1% corrections). + --- \ No newline at end of file diff --git a/docs/src/reference/solid-phase.md b/docs/src/reference/solid-phase.md index 23803f5..bb39d61 100644 --- a/docs/src/reference/solid-phase.md +++ b/docs/src/reference/solid-phase.md @@ -148,17 +148,7 @@ propagate the solution using the so-called propagator matrix (``\pmb{\Pi}_\ell`` ```math \frac{d\pmb{\Pi}_\ell(r, r')}{dr} = \pmb{A}_\ell(r)\,\pmb{\Pi}_\ell(r, r'), ``` -at radius ``r`` w.r.t. the solution at the previous layer ``r'``, this is also know as the Cauchy data at radius (``r'``). Equivalently, we may write the normalized version of the propagator matrix as - -```math -\frac{d\tilde{\pmb{\Pi}}_\ell(r, r')}{dr} = \tilde{\pmb{A}}_\ell(r)\,\tilde{\pmb{\Pi}}_\ell(r, r'), -``` - -Given that ``\tilde{\pmb{\Pi}}_\ell(r, r')`` is constructed from the normalized matrix ``\tilde{\pmb{A}}_\ell``, the normailization in ``\mathbf{A}_\ell`` enters ``\tilde{\pmb{\Pi}}_\ell`` as - -```math -\tilde{\pmb{\Pi}}_\ell(r, r') = \left[\left[\left[ \mathbf{1} \times \mathbf{S}^{-1} \tilde{\pmb{\Pi}}_\ell(r_1, r_1') \mathbf{S}\right] \times \mathbf{S}^{-1} \tilde{\pmb{\Pi}}_\ell(r_2, r_2') \mathbf{S} \right] \times \dots \right] = \mathbf{S}^{-1} \pmb{\Pi}_\ell(r, r') \mathbf{S}, -``` the normalization does not alter the physical solution. If ``r = r'`` we have +at radius ``r`` w.r.t. the solution at the previous layer ``r'``, this is also know as the Cauchy data at radius (``r'``). If ``r = r'`` we have ```math \pmb{\Pi}_\ell(r', r') = \pmb{1}. @@ -170,13 +160,7 @@ Each column of the propagator matrix is one of the six linearly independent solu \frac{d\pmb{y}_{\ell m}}{dr} = \pmb{A}_\ell(r)\,\pmb{y}_{\ell m}. ``` -The six linearly independent solutions are multiplied by the propagator, forming a basis matrix. To prevent ill-conditioning due to widely differing units and growing/decaying solutions, we perform a QR decomposition at every sublayer: - -```math -\pmb{\Pi}_\ell(r, r') = \mathbf{Q}\,\mathbf{R}, -``` where ``\mathbf{Q}`` is an orthogonal matrix and ``\mathbf{R}`` is an upper triangular matrix. The orthogonal matrix ``\mathbf{Q}`` forms an orthonormalized basis used to propagate to the next sublayer, while the upper triangular matrix ``\mathbf{R}`` stores the mixing coefficients for back-propagation, ensuring accurate reconstruction of the physical solution. The process is repeated across all layers. - -We impose continuity: +The six linearly independent solutions are multiplied by the propagator, forming a basis matrix. We impose continuity: ```math \pmb{\Pi}_\ell(r_j^+, r') = \pmb{\Pi}_\ell(r_j^-, r'), @@ -195,13 +179,6 @@ Therefore, \pmb{\Pi}_\ell(r, r_C^+)\,\pmb{I}_C\,\pmb{C}. ``` -or in the normalized form - -```math -\pmb{y}_{\ell m}(r) = -\mathbf{S} \, \tilde{\pmb{\Pi}}_\ell(r, r_C^+)\,\tilde{\pmb{I}}_C\,{\pmb{C}}. -``` - This equation can be solved iteratively, up till the surface to yield the general responds of the interior to any form of tidal- or load induced deformation. #### Relaxation method @@ -475,7 +452,7 @@ The initial condition is: R_1 = -S_1^{-1} Q_1 ``` -Finally, the last block equation becomes: +Finally, the last block equation becomes (see below for the surface boundary conditions): ```math X_N y_N = b = @@ -515,48 +492,7 @@ Here, ``Q_n`` is singular by construction, so ``R_n`` is not invertible. This en ### Surface Boundary Conditions -Finally, to find the specific solution for the case where a planet is orbiting around a star-like object, we can specify the surface (``r=a^-``) boundary condition. In particular we need to constrain the 3rd, 4th, and 6th ``y``-values. To do this employ the projector on the 3rd, 4th, and 6th components given by - -```math -\pmb{P}_1\,\pmb{y}(a^-) = -\pmb{P}_1 \left[ -\pmb{\Pi}_\ell(a^-, r_C^+)\,\pmb{I}_C\,\pmb{C} -\right] -= \pmb{b}, -``` - -where - -```math -\pmb{b} = \sigma_{\ell m}^L\,\pmb{b}^L + -\left(\Phi_{\ell m}^T(a) + \Phi_{\ell m}^C(a)\right)\pmb{b}^T, -``` - -with - -```math -\pmb{b}^L = -\begin{pmatrix} --\dfrac{(2\ell+1)g(a)}{4\pi a^2} \\[1em] -0 \\[1em] --\dfrac{(2\ell+1)G}{a^2} -\end{pmatrix} -\qquad\text{(Load)}, -``` - -```math -\pmb{b}^T = -\begin{pmatrix} -0 \\[1em] -0 \\[1em] -\dfrac{2\ell+1}{a} -\end{pmatrix} -\qquad\text{(Tidal)}. -``` - -Incase the solid part of the mantle extends up to the surface of the planet, then the surface is stress-free and the toroidal part has ``\sigma_{\ell m}^L = \pmb{0}``, and the solution no longer depends on ``\pmb{b}^L``. However, as in our formalism the solid is often loaded by a liquid magma ocean, we cannot ignore this term. - -The integration constants follow from +Finally, to find the specific solution for the case where a planet is orbiting around a star-like object, we can specify the surface (``r=a^-``) boundary condition. In particular we need to constrain the 3rd, 4th, and 6th ``y``-values. The integration constants will follow from ```math \pmb{C} = @@ -574,22 +510,18 @@ Thus, \pmb{b}. ``` -To solve this system we thus need only provide ``\pmb{P}_1\,\pmb{y}(a^-)``, we will provide some examples in Chapter 7 at the end of this component. - ---- - -Similarly, for the relaxation method, we can impose a free-surface outer boundary. The boundary conditions can be written in the same form as the inner system with +To solve this system we thus need only provide ``\pmb{P}_1\,\pmb{y}(a^-)``. Additionally for the relaxation method we can impose a semi-free-surface outer boundary. The boundary conditions can be written in the same form as the inner system with ```math \mathbf{B} = \begin{pmatrix} 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 \\ -0 & 0 & 0 & 0 & 0 & 1 +0 & 0 & 0 & 0 & (n+1)/R & 1 \end{pmatrix} ``` -This corresponds to free-surface conditions applied to ``y_1``, ``y_2``, and ``y_5``, while the remaining components are constrained accordingly. In the general case, let ``P`` denote the external atmospheric pressure, ``\zeta`` the surface mass load, ``U`` an external potential, and ``\tau`` the tangential traction component. Then the boundary conditions for a spherical harmonic degree ``n`` can be written as: +This corresponds to free-surface conditions applied to ``y_1``, ``y_2``, and (semi) ``y_5``, while the remaining components are constrained accordingly. In the general case, let ``P`` denote the external atmospheric pressure, ``\zeta`` the surface mass load, ``U`` an external potential, and ``\tau`` the tangential traction component. Then the boundary conditions for a spherical harmonic degree ``n`` can be written as: ```math \begin{cases} @@ -613,7 +545,7 @@ We distinguish between Tidal Love numbers (response to an external potential per * TLN: ``(U, U', \tau, P) = (1, 0, 0, 0)`` * LLN: ``(U, U', \tau, P) = (0, 1, 0, 0)`` -When the outer boundary is driven by tides from an external perturber, the system can be written as: +When the outer boundary is driven by tides from an external perturber, the final step in the relaxation method system can be written as: ```math \mathbf{B} \mathbf{y} = \mathbf{b} @@ -630,16 +562,7 @@ with \end{pmatrix} ``` -This expression follows from a simplified form of the general boundary conditions, assuming the outer radius ``a = R`` (planetary radius) and neglecting higher-order corrections. - -This corresponds to the free-surface conditions: - -```math -X(R) = 0, \qquad Y(R) = 0, \qquad \psi(R) = \frac{2n+1}{R} -``` - -applied at ``r = R`` the planetary radius. - +This expression follows from a simplified form of the general boundary conditions, assuming the outer radius ``a = R`` (planetary radius). --- diff --git a/docs/src/reference/surface-loading.md b/docs/src/reference/surface-loading.md index f803f0e..fff1750 100644 --- a/docs/src/reference/surface-loading.md +++ b/docs/src/reference/surface-loading.md @@ -12,10 +12,7 @@ k_n(\sigma) = k_{T,n}^{(\mathrm{solid})}(\sigma) + \bigl[1 + k_{L,n}(\sigma)\bigr]\,k_{n}^{(\mathrm{fluid})}(\sigma). ``` -To include a sub-surface magma ocean, the full Love number is +Sub-surface magma oceans are included only through the solid formalism, based on viscous disipation and compaction. -```math -??? -``` --- \ No newline at end of file diff --git a/docs/src/reference/tidal-potentials.md b/docs/src/reference/tidal-potentials.md index 52e97d0..a287b9e 100644 --- a/docs/src/reference/tidal-potentials.md +++ b/docs/src/reference/tidal-potentials.md @@ -3,44 +3,6 @@ # Tidal potentials -Different sources can provide different tidal potentials, first we need to complete the solid-phase by providng the relevant surface boundary condition. Here we list some examples. - -### Loaded Surface - -For an impermeable load, - -```math -y_2 = - \frac{(2\ell + 1) g(R)}{4 \pi G R}, \qquad y_4 = 0, \qquad y_6 = \frac{2\ell + 1}{R}, \qquad y_8 = 0 -``` - -where ``g(R)`` is the gravitational acceleration at the surface (e.g., Saito, 1974). - - -### Lunar Tide - -For a fully solid mantle, the boundary conditions at the surface are - -```math -y_3 = y_4 = 0, \qquad y_6 = - \frac{(2\ell + 1) g_s}{4} \frac{M_M}{M_E} \left(\frac{R_E}{a}\right)^3. -``` - -(see *Takeuchi et al. 1972*). - -The final condition is - -```math -\pmb{P}_1\,\pmb{y}(a^-) = -\begin{pmatrix} -0 \\[1em] -0 \\[1em] --\dfrac{(2\ell+1) g_s}{4} -\dfrac{M_M}{M_E} -\left(\dfrac{R_E}{a}\right)^3 -\end{pmatrix}. -``` - -After having solved and generated the ``k_2 (\sigma)`` spectrum, we can find the power input from tides. - ### Tidal Dissipation The mode amplitude of the external tidal potential is diff --git a/src/Obliqua.jl b/src/Obliqua.jl index ffbd0e9..ead0960 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -20,7 +20,6 @@ module Obliqua # Include local jl files include("solid0d.jl") include("solid1d.jl") - include("solid1d_qr.jl") include("solid1d_mush.jl") include("solid1d_relax.jl") include("solid1d_mush_relax.jl") @@ -32,7 +31,6 @@ module Obliqua # Import submodules import .solid0d import .solid1d - import .solid1d_qr import .solid1d_mush import .solid1d_relax import .solid1d_mush_relax @@ -46,6 +44,7 @@ module Obliqua export solid1d export solid1d_mush export solid1d_relax + export solid1d_mush_relax export fluid0d export Hansen export load From eaf44c271c414e58cecb27e3c5fa3488d4d61c85 Mon Sep 17 00:00:00 2001 From: Marijn Date: Sat, 2 May 2026 20:03:25 +0000 Subject: [PATCH 15/36] Updated dependencies. --- Project.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 90b3c91..6fa432e 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ version = "0.1.0" [deps] AssociatedLegendrePolynomials = "2119f1ac-fb78-50f5-8cc0-dda848ebdb19" +DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" GenericLinearAlgebra = "14197337-ba66-59df-a3e3-ca00e7dcff7a" @@ -15,6 +16,7 @@ LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" NCDatasets = "85f8d34a-cbdd-5861-8df4-14fed0d494ab" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" @@ -22,15 +24,17 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [compat] +DelimitedFiles = "1.9.1" FFTW = "1.10.0" -GenericLinearAlgebra = "0.4.0" +GenericLinearAlgebra = "0" Interpolations = "0.16.2" JSON = "1.3.0" LoggingExtras = "1.2.0" NCDatasets = "0.14.15" Plots = "1.41.2" Printf = "1.11.0" +PythonPlot = "1.0.6" SparseArrays = "1.11.0" -SpecialFunctions = "2.7.2" +SpecialFunctions = "2.7" Statistics = "1.11.1" TOML = "1.0.3" From 99e03e8fd5fb4964a1a15577afb8aa76c1a0f14b Mon Sep 17 00:00:00 2001 From: Marijn Date: Sat, 2 May 2026 20:52:08 +0000 Subject: [PATCH 16/36] Updated relax models. solid1d_mush_relax still has an issue where the upper y-functions are discontinuous across interfaces. --- src/solid1d.jl | 184 +------- src/solid1d_mush.jl | 315 ++----------- src/solid1d_mush_relax.jl | 935 ++++++++++---------------------------- src/solid1d_relax.jl | 265 +---------- 4 files changed, 302 insertions(+), 1397 deletions(-) diff --git a/src/solid1d.jl b/src/solid1d.jl index 5baea65..795828b 100644 --- a/src/solid1d.jl +++ b/src/solid1d.jl @@ -2,6 +2,9 @@ module solid1d + include("common.jl") + using .common + using LinearAlgebra using DoubleFloats using AssociatedLegendrePolynomials @@ -209,156 +212,12 @@ module solid1d """ - get_Ic(r, ρ, g, μ, type, n; M=6, N=3) - - Get the core solution vector. - - # Arguments - - `r::prec` : Radius of the core boundary. - - `ρ::prec` : Density of the core. - - `g::prec` : Gravity at the core boundary. - - `μ::prec` : Shear modulus of the core. - - `type::String` : Type of core, either "liquid" or "solid". - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `M::Int=6` : Number of rows in the Ic matrix. This should be 6 for the solid-body problem. - - `N::Int=3` : Number of linearly independent solutions to compute. This should be 3 for the solid-body problem. - - # Returns - - `Ic::Array{precc,2}` : MxN array of linearly independent solutions at the core boundary. These are used as starting vectors for the numerical integration across the interior. - """ - function get_Ic(r, ρ, g, μ, type, n; M=6, N=3) - Ic = zeros(precc, M, N) - - if type=="liquid" - Ic[1,1] = -r^n / g - Ic[1,3] = 1.0 - Ic[2,2] = 1.0 - Ic[3,3] = g*ρ - Ic[5,1] = r^n - Ic[6,1] = 2(n-1)*r^(n-1) - Ic[6,3] = 4π * G * ρ - else # incompressible solid core - # First column - Ic[1, 1] = n*r^( n+1 ) / ( 2*( 2n + 3) ) - Ic[2, 1] = ( n+3 )*r^( n+1 ) / ( 2*( 2n+3 ) * ( n+1 ) ) - Ic[3, 1] = ( n*ρ*g*r + 2*( n^2 - n - 3)*μ ) * r^n / ( 2*( 2n + 3) ) - Ic[4, 1] = n *( n+2 ) * μ * r^n / ( ( 2n + 3 )*( n+1 ) ) - Ic[6, 1] = 2π*G*ρ*n*r^( n+1 ) / ( 2n + 3 ) - - # Second column - Ic[1, 2] = r^( n-1 ) - Ic[2, 2] = r^( n-1 ) / n - Ic[3, 2] = ( ρ*g*r + 2*( n-1 )*μ ) * r^( n-2 ) - Ic[4, 2] = 2*( n-1 ) * μ * r^( n-2 ) / n - Ic[6, 2] = 4π*G*ρ*r^( n-1 ) - - # Third column - Ic[3, 3] = -ρ * r^n - Ic[5, 3] = -r^n - Ic[6, 3] = -( 2n + 1) * r^( n-1 ) - - end - - return Ic - end - - - """ - get_A(r, ρ, g, μ, K, n) - - Compute the 6x6 `A` matrix in the ODE for the solid-body problem. - - # Arguments - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `n::Int` : Tidal degree. - - # Returns - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - - # Notes - See also [`get_A!`](@ref) - """ - function get_A(r, ρ, g, μ, K, n) - A = zeros(precc, 6, 6) - get_A!(A, r, ρ, g, μ, K, n) - return A - end - - - """ - get_A!(A, r, ρ, g, μ, K, n; λ=nothing) - - Compute the 6x6 `A` matrix in the ODE for the solid-body problem. These correspond to - the coefficients given in Equation S4.6 in Hay et al., (2025) when α=φ=0, as well as Sabadini and Vermeersen - (2016) Eq. 1.95. - - # Arguments - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. - - # Notes - See also [`get_A`](@ref) - """ - function get_A!(A::Matrix, r, ρ, g, μ, K, n; λ=nothing) - if isnothing(λ) - λ = K - 2μ/3 - end - - r_inv = 1.0/r - β_inv = 1.0/(2μ + λ) - rβ_inv = r_inv * β_inv - - A[1,1] = -2λ * r_inv*β_inv - A[2,1] = -r_inv - A[3,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) - A[4,1] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[5,1] = 4π * G * ρ - A[6,1] = 4π*(n+1)*G*ρ*r_inv - - A[1,2] = n*(n+1) * λ * r_inv*β_inv - A[2,2] = r_inv - A[3,2] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[4,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) - A[6,2] = -4π*n*(n+1)*G*ρ*r_inv - - A[1,3] = β_inv - A[3,3] = r_inv*β_inv * (-4μ ) - A[4,3] = -λ * r_inv*β_inv - - A[2,4] = 1.0 / μ - A[3,4] = n*(n+1)*r_inv - A[4,4] = -3r_inv - - A[3,5] = ρ * (n+1)*r_inv - A[4,5] = -ρ*r_inv - A[5,5] = -(n+1)r_inv - - A[3,6] = -ρ - A[5,6] = 1.0 - A[6,6] = (n-1)r_inv - end - - - """ - get_B(r1, r2, g1, g2, ρ, μ, K, n) + get_B(ω, r1, r2, g1, g2, ρ, μ, K, n) Compute the 6x6 numerical integrator matrix, which integrates dy/dr from `r1` to `r2` for the solid-body problem. # Arguments + - `ω::prec` : Forcing frequency. - `r1::prec` : Starting radius for integration. - `r2::prec` : Ending radius for integration. - `g1::prec` : Gravity at radius r1. @@ -374,21 +233,22 @@ module solid1d # Notes See 'get_B!' for definition. """ - function get_B(r1, r2, g1, g2, ρ, μ, K, n) + function get_B(ω, r1, r2, g1, g2, ρ, μ, K, n) B = zeros(precc, 6, 6) - get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) return B end """ - get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) Compute the 6x6 numerical integrator matrix, which integrates dy/dr from `r1` to `r2` for the solid-body problem. `B` here represnts the RK4 integrator, given by Eq. S5.5 in Hay et al., (2025). # Arguments - `B::Array{precc,2}` : 6x6 numerical integrator matrix for integrating dy/dr from r1 to r2 for the solid-body problem. + - `ω::prec` : Forcing frequency. - `r1::prec` : Starting radius for integration. - `r2::prec` : Ending radius for integration. - `g1::prec` : Gravity at radius r1. @@ -401,15 +261,15 @@ module solid1d # Notes See also [`get_B`](@ref) """ - function get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) + function get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) dr = r2 - r1 rhalf = r1 + 0.5dr ghalf = g1 + 0.5*(g2 - g1) - A1 = get_A(r1, ρ, g1, μ, K, n) - Ahalf = get_A(rhalf, ρ, ghalf, μ, K, n) - A2 = get_A(r2, ρ, g2, μ, K, n) + A1 = get_A(ω, r1, ρ, g1, μ, K, n) + Ahalf = get_A(ω, rhalf, ρ, ghalf, μ, K, n) + A2 = get_A(ω, r2, ρ, g2, μ, K, n) k16 = zeros(precc, 6, 6) k26 = zeros(precc, 6, 6) @@ -429,7 +289,7 @@ module solid1d """ - get_B_product!(Brod, r, ρ, g, μ, K, n) + get_B_product!(Brod, ω, r, ρ, g, μ, K, n) Compute the product of the 6x6 B matrices within a primary layer. This is used to propgate the y solution across one single-phase (solid) primary layer. Bprod is denoted by D in Eq. S5.14 @@ -437,6 +297,7 @@ module solid1d # Arguments - `Bprod2::Array{precc,4}` : 6x6x(nr-1)x(nlayers-1) array to store the B products across each secondary layer within each primary layer. + - `ω::prec` : Forcing frequency. - `r::Array{prec,2}` : 2D array of layer boundaries. - `ρ::Array{prec,1}` : 1D array of layer densities. - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. @@ -444,7 +305,7 @@ module solid1d - `K::Array{prec,1}` : 1D array of layer bulk moduli. - `n::Int` : Tidal degree. """ - function get_B_product!(Bprod2, r, ρ, g, μ, K, n) + function get_B_product!(Bprod2, ω, r, ρ, g, μ, K, n) Bstart = Matrix{precc}(I, 6, 6) B = zeros(precc, 6, 6) @@ -456,7 +317,7 @@ module solid1d g1 = g[j] g2 = g[j+1] - get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) Bprod2[:,:,j] .= B * (j==1 ? Bstart : Bprod2[:,:,j-1]) r1 = r2 @@ -465,11 +326,12 @@ module solid1d """ - compute_M(r, ρ, g, μ, K, n, ρ_core; core="liquid") + compute_M(ω, r, ρ, g, μ, K, n, ρ_core; core="liquid") Compute the M matrix, which is used to propagate the solution across the entire interior. This is used in the `compute_y` function. # Arguments + - `ω::prec` : Forcing frequency. - `r::Array{prec,2}` : 2D array of layer boundaries. - `ρ::Array{prec,1}` : 1D array of layer densities. - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. @@ -485,19 +347,19 @@ module solid1d - `M::Array{precc,2}` : 3x3 M matrix, which is used to propagate the solution across the entire interior. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. """ - function compute_M(r, ρ, g, μ, K, n, ρ_core; core="liquid") + function compute_M(ω, r, ρ, g, μ, K, n, ρ_core; core="liquid") r, ρ, g, μ, K = convert_params_to_prec(r, ρ, g, μ, K) nlayers = size(r)[2] nsublayers = size(r)[1] - y_start = get_Ic(r[end,1], ρ_core, g[end,1], μ[1], core, n; M=6, N=3) + y_start = get_Ic(ω, r[end,1], ρ_core, g[end,1], μ[1], K[1], core, n; Y=[1,2,3,4,5,6]) y1_4 = zeros(precc, 6, 3, nsublayers-1, nlayers) # Three linearly independent y solutions for i in 2:nlayers Bprod = zeros(precc, 6, 6, nsublayers-1) - @views get_B_product!(Bprod, r[:, i], ρ[1], g[:, i], μ[i], K[i], n) + @views get_B_product!(Bprod, ω, r[:, i], ρ[1], g[:, i], μ[i], K[i], n) for j in 1:nsublayers-1 y1_4[:,:,j,i] = @view(Bprod[:,:,j]) * y_start @@ -543,7 +405,7 @@ module solid1d U = 0.0 if load U_prime = 1.0 - elseif !load + else U = 1.0 end diff --git a/src/solid1d_mush.jl b/src/solid1d_mush.jl index 6625165..383b12e 100644 --- a/src/solid1d_mush.jl +++ b/src/solid1d_mush.jl @@ -2,6 +2,9 @@ module solid1d_mush + include("common.jl") + using .common + using LinearAlgebra using DoubleFloats using AssociatedLegendrePolynomials @@ -223,265 +226,14 @@ module solid1d_mush return (r_prec, ρ_prec, g_prec, μ_prec, κs_prec, ω_prec, ρl_prec, κl_prec, κd_prec, α_prec, ηl_prec, ϕ_prec, k_prec) end - - """ - get_Ic(r, ρ, g, μ, type, n; M=8, N=4) - - Get the core solution vector. - # Arguments - - `r::prec` : Radius of the core boundary. - - `ρ::prec` : Density of the core. - - `g::prec` : Gravity at the core boundary. - - `μ::prec` : Shear modulus of the core. - - `type::String` : Type of core, either "liquid" or "solid". - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `M::Int=8` : Number of rows in the Ic matrix. This should be 8 for the two-phase problem. - - `N::Int=4` : Number of linearly independent solutions to compute. This should be 4 for the two-phase problem. - - # Returns - - `Ic::Array{precc,2}` : MxN array of linearly independent solutions at the core boundary. These are used as starting vectors for the numerical integration across the interior. """ - function get_Ic(r, ρ, g, μ, type, n; M=8, N=4) - Ic = zeros(precc, M, N) - - if type=="liquid" - Ic[1,1] = -r^n / g - Ic[1,3] = 1.0 - Ic[2,2] = 1.0 - Ic[3,3] = g*ρ - Ic[5,1] = r^n - Ic[6,1] = 2(n-1)*r^(n-1) - Ic[6,3] = 4π * G * ρ - else # incompressible solid core - # First column - Ic[1, 1] = n*r^( n+1 ) / ( 2*( 2n + 3) ) - Ic[2, 1] = ( n+3 )*r^( n+1 ) / ( 2*( 2n+3 ) * ( n+1 ) ) - Ic[3, 1] = ( n*ρ*g*r + 2*( n^2 - n - 3)*μ ) * r^n / ( 2*( 2n + 3) ) - Ic[4, 1] = n *( n+2 ) * μ * r^n / ( ( 2n + 3 )*( n+1 ) ) - Ic[6, 1] = 2π*G*ρ*n*r^( n+1 ) / ( 2n + 3 ) - - # Second column - Ic[1, 2] = r^( n-1 ) - Ic[2, 2] = r^( n-1 ) / n - Ic[3, 2] = ( ρ*g*r + 2*( n-1 )*μ ) * r^( n-2 ) - Ic[4, 2] = 2*( n-1 ) * μ * r^( n-2 ) / n - Ic[6, 2] = 4π*G*ρ*r^( n-1 ) - - # Third column - Ic[3, 3] = -ρ * r^n - Ic[5, 3] = -r^n - Ic[6, 3] = -( 2n + 1) * r^( n-1 ) - - end - - return Ic - end - - - """ - get_A(r, ρ, g, μ, K, n) - - Compute the 6x6 `A` matrix in the ODE for the solid-body problem. - - # Arguments - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `n::Int` : Tidal degree. - - # Returns - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - - # Notes - See also [`get_A!`](@ref) - """ - function get_A(r, ρ, g, μ, K, n) - A = zeros(precc, 6, 6) - get_A!(A, r, ρ, g, μ, K, n) - return A - end - - - """ - get_A(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) - - Compute the 8x8 `A` matrix in the ODE for the two-phase problem. These correspond to - the coefficients given in Equation S4.6 in Hay et al., (2025). - - # Arguments - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `ω::prec` : Forcing frequency. - - `ρₗ::prec` : Liquid density at radius r. - - `Kl::prec` : Liquid bulk modulus at radius r. - - `Kd::prec` : Drained bulk modulus at radius r. - - `α::prec` : Biot coefficient at radius r. - - `ηₗ::prec` : Liquid viscosity at radius r. - - `ϕ::prec` : Porosity at radius r. - - `k::prec` : Permeability at radius r. - - `n::Int` : Tidal degree. - - # Returns - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - - See also [`get_A!`](@ref) - """ - function get_A(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) - A = zeros(precc, 8, 8) - get_A!(A, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) - return A - end - - - """ - get_A!(A, r, ρ, g, μ, K, n; λ=nothing) - - Compute the 6x6 `A` matrix in the ODE for the solid-body problem. These correspond to - the coefficients given in Equation S4.6 in Hay et al., (2025) when α=φ=0, as well as Sabadini and Vermeersen - (2016) Eq. 1.95. - - # Arguments - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. - - # Notes - See also [`get_A`](@ref) - """ - function get_A!(A::Matrix, r, ρ, g, μ, K, n; λ=nothing) - if isnothing(λ) - λ = K - 2μ/3 - end - - r_inv = 1.0/r - β_inv = 1.0/(2μ + λ) - rβ_inv = r_inv * β_inv - - A[1,1] = -2λ * r_inv*β_inv - A[2,1] = -r_inv - A[3,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) #- ω^2 * ρ# - A[4,1] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[5,1] = 4π * G * ρ - A[6,1] = 4π*(n+1)*G*ρ*r_inv - - A[1,2] = n*(n+1) * λ * r_inv*β_inv - A[2,2] = r_inv - A[3,2] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[4,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) #- ω^2 * ρ# - A[6,2] = -4π*n*(n+1)*G*ρ*r_inv - - A[1,3] = β_inv - A[3,3] = r_inv*β_inv * (-4μ ) - A[4,3] = -λ * r_inv*β_inv - - A[2,4] = 1.0 / μ - A[3,4] = n*(n+1)*r_inv - A[4,4] = -3r_inv - - A[3,5] = ρ * (n+1)*r_inv - A[4,5] = -ρ*r_inv - A[5,5] = -(n+1)r_inv - - A[3,6] = -ρ - A[5,6] = 1.0 - A[6,6] = (n-1)r_inv - end - - - """ - get_A!(A, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) - - Compute the 8x8 `A` matrix in the ODE for the two-phase problem. These correspond to - the coefficients given in Equation S4.6 in Hay et al., (2025). - - # Arguments - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `ω::prec` : Forcing frequency. - - `ρₗ::prec` : Liquid density at radius r. - - `Kl::prec` : Liquid bulk modulus at radius r. - - `Kd::prec` : Drained bulk modulus at radius r. - - `α::prec` : Biot coefficient at radius r. - - `ηₗ::prec` : Liquid viscosity at radius r. - - `ϕ::prec` : Porosity at radius r. - - `k::prec` : Permeability at radius r. - - `n::Int` : Tidal degree. - - # Notes - See also [`get_A`](@ref) - """ - function get_A!(A::Matrix, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) - λ = Kd .- 2μ/3 # Lame's second param, which uses the drained compaction modulus - S = ϕ/Kl + (α - ϕ)/K # Storavity, which uses liquid and solid grain bulk moduli - - # First add the solid-body coefficients, but using drained moduli. - get_A!(A, r, ρ, g, μ, Kd, n; λ=λ) # Note that here we replace the bulk modulus with the compaction modulus - - r_inv = 1.0/r - β_inv = 1.0/(2μ + λ) - - # If there is a porous layer, now add the two-phase components - if !iszero(ϕ) - A[1,7] = α * β_inv - - A[3,1] += 1im * k*ρₗ^2 *g^2 * n*(n+1) / (ω*ηₗ) * r_inv^2 - A[3,5] += -(n+1)r_inv * 1im *(k*ρₗ^2*g*n)/(ω*ηₗ) * r_inv - A[3,7] = 1im * (k*ρₗ*g*n*(n+1))/(ω*ηₗ)*r_inv^2 - 4μ*α*β_inv*r_inv - A[3,8] = 1im * (k*ρₗ^2*g^2*n*(n+1))/(ω*ηₗ)*r_inv^2 - 4ϕ*ρₗ*g*r_inv - - A[4,7] = 2α*μ*r_inv * β_inv - A[4,8] = ϕ*ρₗ*g*r_inv - - A[5,8] = 4π*G*ρₗ*ϕ - - A[6,1] += -1im * 4π*G*n*(n+1)*r_inv * (k*ρₗ^2*g/(ω*ηₗ)*r_inv) - A[6,5] = 1im*4π*n*(n+1)G*(ρₗ)^2*k*r_inv^2 / (ω*ηₗ) - A[6,7] = -1im *4π*n*(n+1)G*ρₗ*k*r_inv^2 / ( ω*ηₗ) - A[6,8] = 4π*G*(n+1)*r_inv * (ϕ*ρₗ - 1im * n*k*ρₗ^2*g/(ω*ηₗ)*r_inv) - - A[7,1] = ρₗ*g*r_inv * ( 4 - 1im *(k*ρₗ*g*n*(n+1)/(ω*ϕ*ηₗ))*r_inv) - A[7,2] = -ρₗ*n*(n+1)*r_inv*g - A[7,5] = -ρₗ*(n+1)r_inv * (1 - 1im*(k*ρₗ*g*n)/(ω*ϕ*ηₗ)*r_inv) - A[7,6] = ρₗ - A[7,7] = - 1im*(k*ρₗ*g*n*(n+1))/(ω*ϕ*ηₗ)*r_inv^2 - A[7,8] = -1im*ω*ϕ*ηₗ/k - 4π*G*(ρ - ϕ*ρₗ)*ρₗ + ρₗ*g*r_inv*(4 - 1im*(k*ρₗ*g*n*(n+1))/(ω*ϕ*ηₗ)*r_inv) - - A[8,1] = r_inv*( 1im * k*ρₗ*g*n*(n+1)/(ω*ϕ*ηₗ)*r_inv - α/ϕ * 4μ*β_inv) - A[8,2] = α/ϕ * 2n*(n+1)*μ *β_inv * r_inv - A[8,3] = -α/ϕ * β_inv - A[8,5] = -1im * k *ρₗ *n*(n+1) / (ω*ϕ*ηₗ)*r_inv^2 - A[8,7] = 1im*k*n*(n+1)/(ω*ϕ*ηₗ)*r_inv^2 - 1/ϕ * (S + α^2 * β_inv) # If solid and liquid are compressible, keep the 1/M term - A[8,8] = 1im * k *ρₗ*g *n*(n+1) / (ω*ϕ*ηₗ)*r_inv^2 - 2r_inv - end - - end - - - """ - get_B(r1, r2, g1, g2, ρ, μ, K, n) + get_B(ω, r1, r2, g1, g2, ρ, μ, K, n) Compute the 6x6 numerical integrator matrix, which integrates dy/dr from `r1` to `r2` for the solid-body problem. # Arguments + - `ω::prec` : Forcing frequency. - `r1::prec` : Starting radius for integration. - `r2::prec` : Ending radius for integration. - `g1::prec` : Gravity at radius r1. @@ -497,21 +249,22 @@ module solid1d_mush # Notes See 'get_B!' for definition. """ - function get_B(r1, r2, g1, g2, ρ, μ, K, n) + function get_B(ω, r1, r2, g1, g2, ρ, μ, K, n) B = zeros(precc, 6, 6) - get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) return B end """ - get_B!(B, r1, r2, g1, g2, ρ, μ, K) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) Compute the 6x6 numerical integrator matrix, which integrates dy/dr from `r1` to `r2` for the solid-body problem. `B` here represnts the RK4 integrator, given by Eq. S5.5 in Hay et al., (2025). # Arguments - `B::Array{precc,2}` : 6x6 numerical integrator matrix for integrating dy/dr from r1 to r2 for the solid-body problem. + - `ω::prec` : Forcing frequency. - `r1::prec` : Starting radius for integration. - `r2::prec` : Ending radius for integration. - `g1::prec` : Gravity at radius r1. @@ -524,16 +277,16 @@ module solid1d_mush # Notes See also [`get_B`](@ref) """ - function get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) + function get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) dr = r2 - r1 rhalf = r1 + 0.5dr ghalf = g1 + 0.5*(g2 - g1) - A1 = get_A(r1, ρ, g1, μ, K, n) - Ahalf = get_A(rhalf, ρ, ghalf, μ, K, n) - A2 = get_A(r2, ρ, g2, μ, K, n) - + A1 = get_A(ω, r1, ρ, g1, μ, K, n) + Ahalf = get_A(ω, rhalf, ρ, ghalf, μ, K, n) + A2 = get_A(ω, r2, ρ, g2, μ, K, n) + k16 = zeros(precc, 6, 6) k26 = zeros(precc, 6, 6) k36 = zeros(precc, 6, 6) @@ -550,11 +303,12 @@ module solid1d_mush """ - get_B(r1, r2, g1, g2, ρ, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + get_B(ω, r1, r2, g1, g2, ρ, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) Compute the 8x8 numerical integrator matrix, which integrates dy/dr from `r1` to `r2` for the two-phase problem. # Arguments + - `ω::prec` : Forcing frequency. - `r1::prec` : Starting radius for integration. - `r2::prec` : Ending radius for integration. - `g1::prec` : Gravity at radius r1. @@ -562,7 +316,6 @@ module solid1d_mush - `ρ::prec` : Density at radius r. - `μ::prec` : Shear modulus at radius r. - `K::prec` : Bulk modulus at radius r. - - `ω::prec` : Forcing frequency. - `ρₗ::prec` : Liquid density at radius r. - `Kl::prec` : Liquid bulk modulus at radius r. - `Kd::prec` : Drained bulk modulus at radius r. @@ -578,22 +331,23 @@ module solid1d_mush # Notes See 'get_B!' for definition. """ - function get_B(r1, r2, g1, g2, ρ, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + function get_B(ω, r1, r2, g1, g2, ρ, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) B = zeros(precc, 8, 8) - get_B!(B, r1, r2, g1, g2, ρ, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) return B end """ - get_B!(B, r1, r2, g1, g2, ρ, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) Compute the 8x8 numerical integrator matrix, which integrates dy/dr from `r1` to `r2` for the two-phase problem. `B` here represnts the RK4 integrator, given by Eq. S5.5 in Hay et al., (2025). # Arguments - `B::Array{precc,2}` : 6x6 numerical integrator matrix for integrating dy/dr from r1 to r2 for the solid-body problem. + - `ω::prec` : Forcing frequency. - `r1::prec` : Starting radius for integration. - `r2::prec` : Ending radius for integration. - `g1::prec` : Gravity at radius r1. @@ -601,7 +355,6 @@ module solid1d_mush - `ρ::prec` : Density at radius r. - `μ::prec` : Shear modulus at radius r. - `K::prec` : Bulk modulus at radius r. - - `ω::prec` : Forcing frequency. - `ρₗ::prec` : Liquid density at radius r. - `Kl::prec` : Liquid bulk modulus at radius r. - `Kd::prec` : Drained bulk modulus at radius r. @@ -614,7 +367,7 @@ module solid1d_mush # Notes See also [`get_B`](@ref) """ - function get_B!(B, r1, r2, g1, g2, ρ, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + function get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) dr = r2 - r1 rhalf = r1 + 0.5dr @@ -624,9 +377,9 @@ module solid1d_mush Amid_p = zeros(precc, 8, 8) Atop_p = zeros(precc, 8, 8) - get_A!(Abot_p, r1, ρ, g1, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) - get_A!(Amid_p, rhalf, ρ, ghalf, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) - get_A!(Atop_p, r2, ρ, g2, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + get_A!(Abot_p, ω, r1, ρ, g1, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + get_A!(Amid_p, ω, rhalf, ρ, ghalf, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + get_A!(Atop_p, ω, r2, ρ, g2, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) k18 = zeros(precc, 8, 8) k28 = zeros(precc, 8, 8) @@ -645,7 +398,7 @@ module solid1d_mush """ - get_B_product!(Brod, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + get_B_product!(Bprod2, ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) Compute the product of the 8x8 B matrices within a primary layer. This is used to propgate the y solution across a single two-phase primary layer. Bprod is denoted by D in Eq. S5.14 in @@ -653,12 +406,12 @@ module solid1d_mush # Arguments - `Bprod2::Array{precc,4}` : 8x8x(nr-1)x(nlayers-1) array to store the B products across each secondary layer within each primary layer. + - `ω::prec` : Forcing frequency. - `r::Array{prec,2}` : 2D array of layer boundaries. - `ρ::Array{prec,1}` : 1D array of layer densities. - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. - `μ::Array{prec,1}` : 1D array of layer shear moduli. - `K::Array{prec,1}` : 1D array of layer bulk moduli. - - `ω::prec` : Forcing frequency. - `ρₗ::Array{prec,1}` : 1D array of liquid densities at layer boundaries. - `Kl::Array{prec,1}` : 1D array of liquid bulk moduli at layer boundaries. - `Kd::Array{prec,1}` : 1D array of drained bulk moduli at layer boundaries. @@ -668,7 +421,7 @@ module solid1d_mush - `k::Array{prec,1}` : 1D array of permeabilities at layer boundaries. - `n::Int` : Tidal degree. """ - function get_B_product!(Bprod2, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + function get_B_product!(Bprod2, ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) # Check dimensions of Bprod2 nr = size(r)[1] @@ -694,9 +447,9 @@ module solid1d_mush g2 = g[j+1] if ϕ>0 - get_B!(B, r1, r2, g1, g2, ρ, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) else - get_B!(B, r1, r2, g1, g2, ρ, μ, K, n) + get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) end Bprod2[:,:,j] .= B * (j==1 ? Bstart : @view(Bprod2[:,:,j-1])) @@ -708,17 +461,17 @@ module solid1d_mush """ - compute_M(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; core="liquid") + compute_M(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; core="liquid") Compute the 4x4 M matrix, which relates the solution at the surface and porous layer interface to the core solution. # Arguments + - `ω::prec` : Forcing frequency. - `r::Array{prec,2}` : 2D array of layer boundaries. - `ρ::Array{prec,1}` : 1D array of layer densities. - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. - `μ::Array{prec,1}` : 1D array of layer shear moduli. - `K::Array{prec,1}` : 1D array of layer bulk moduli. - - `ω::prec` : Forcing frequency. - `ρₗ::Array{prec,1}` : 1D array of liquid densities at layer boundaries. - `Kl::Array{prec,1}` : 1D array of liquid bulk moduli at layer boundaries. - `Kd::Array{prec,1}` : 1D array of drained bulk moduli at layer boundaries. @@ -735,7 +488,7 @@ module solid1d_mush - `M::Array{precc,2}` : 4x4 M matrix, which is used to propagate the solution across the entire interior. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. """ - function compute_M(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core; core="liquid") + function compute_M(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core; core="liquid") porous_layer = ϕ .> 0.0 ## Convert parameters to the precision of precc: @@ -747,13 +500,13 @@ module solid1d_mush nsublayers = size(r)[1] # Define starting vector as the core solution matrix, Y_r_C (Eq. S5.15) - y_start = get_Ic(r[end,1], ρ_core, g[end,1], μ[1], core, n; M=8, N=4) + y_start = get_Ic(ω, r[end,1], ρ_core, g[end,1], μ[1], K[1], core, n; Y=[1,2,3,4,5,6,7,8]) y1_4 = zeros(precc, 8, 4, nsublayers-1, nlayers) # Four linearly independent y solutions for i in 2:nlayers Bprod = zeros(precc, 8, 8, nsublayers-1) # D matrix from Eq. S5.13 - @views get_B_product!(Bprod, r[:,i], ρ[i], g[:,i], μ[i], K[i], ω, ρₗ[i], Kl[i], Kd[i], α[i], ηₗ[i], ϕ[i], k[i], n) + @views get_B_product!(Bprod, ω, r[:,i], ρ[i], g[:,i], μ[i], K[i], ρₗ[i], Kl[i], Kd[i], α[i], ηₗ[i], ϕ[i], k[i], n) # Modify starting vector if the layer is porous # If a new porous layer (i.e., sitting on a non-porous layer) diff --git a/src/solid1d_mush_relax.jl b/src/solid1d_mush_relax.jl index dd3de40..eccb92f 100644 --- a/src/solid1d_mush_relax.jl +++ b/src/solid1d_mush_relax.jl @@ -2,6 +2,9 @@ module solid1d_mush_relax + include("common.jl") + using .common + using LinearAlgebra import GenericLinearAlgebra using DoubleFloats @@ -237,479 +240,6 @@ module solid1d_mush_relax solid1d_mush_relax.lons = lons end - - """ - get_scales(R0, M0, g0) - - Compute the characteristic scales for the problem based on a reference radius `R0`, mass `M0`, and gravity scale - `g0`. These scales are used to non-dimensionalize the equations and ensure numerical stability. - - # Arguments - - `R0::prec` : Reference radius scale (e.g., planetary radius). - - `M0::prec` : Reference mass scale (e.g., planetary mass). - - `g0::prec` : Reference gravity scale. - - # Returns - Tuple of characteristic scales: - - `R0::prec` : Length scale (m). - - `M0::prec` : Mass scale (kg). - - `s0::prec` : Time scale (s). - - `ρ0::prec` : Density scale (kg/m^3). - - `G0::prec` : Gravitational constant scale (m^3 kg^-1 s^-2). - - `g0::prec` : Gravity scale (m/s^2). - - `μ0::prec` : Shear modulus scale (Pa). - - `S::Diagonal{prec}` : Diagonal scaling matrix for state variables. - - `Sinv::Diagonal{prec}` : Inverse of the scaling matrix S. - """ - function get_scales(R0, M0, g0) - - ρ0 = M0 / R0^3 - P0 = g0 * R0 - μ0 = ρ0 * g0 * R0 - - s0 = sqrt(g0 / R0) - G0 = R0^3 / (M0 * s0^2) - - S = Diagonal(precc[ - R0, # y1: radial displacement (m) - R0, # y2: tangential displacement (m) - P0, # y5: potential (m^2/s^2) - μ0, # y7: pore pressure (Pa) - μ0, # y3: radial stress (Pa) - μ0, # y4: tangential stress (Pa) - g0, # y6: potential gradient/gravity (m/s^2) - R0, # y8: relative radial displacement (m) - ]) - - Sinv = inv(S) - return R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv - end - - - """ - doublefactorial(n) - - Compute the double factorial of an integer n, defined as n!! = n * (n-2) * (n-4) * ... until 1 or 0. - - # Arguments - - `n::Integer` : The integer for which to compute the double factorial. Must be non-negative. - - # Returns - - `result::Integer` : The double factorial of n. - """ - function doublefactorial(n::Integer) - n < 0 && error("doublefactorial not defined for negative n") - n == 0 && return one(n) - n == 1 && return one(n) - - result = one(n) - for k in n:-2:1 - result *= k - end - return result - end - - - """ - get_Ic(ω, r, ρ, g, μ, K, type, n; G0=1, M=8, N=4) - - Get the core solution vector. - https://academic.oup.com/gji/article/203/3/2150/2594863 - - # Arguments - - `ω::prec` : Angular frequency. - - `r::prec` : Radius of the core boundary. - - `ρ::prec` : Density of the core. - - `g::prec` : Gravity at the core boundary. - - `μ::prec` : Shear modulus of the core. - - `K::prec` : Bulk modulus of the core. - - `type::String` : Type of core, either "liquid" or "solid". - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `M::Int=6` : Number of rows in the Ic matrix. This should be 6 for the solid-body problem. - - `N::Int=3` : Number of linearly independent solutions to compute. This should be 3 for the solid-body problem. - - # Returns - - `Ic::Array{precc,2}` : MxN array of linearly independent solutions at the core boundary. These are used as starting vectors for the numerical integration across the interior. - """ - function get_Ic(ω, r, ρ, g, μ, K, type, n; G0=1, M=8, N=4) - Ic = zeros(precc, M, N) - - G_norm = G / G0 - - if type=="liquid" - if M == 6 - Ic[1,1] = -r^n / g - Ic[1,3] = 1.0 - Ic[2,2] = 1.0 - Ic[4,3] = g*ρ - Ic[3,1] = r^n - Ic[6,1] = 2(n-1)*r^(n-1) - Ic[6,3] = 4π * G_norm * ρ - elseif M == 8 - Ic[1,1] = -r^n / g - Ic[1,3] = 1.0 - Ic[2,2] = 1.0 - Ic[5,3] = g*ρ - Ic[3,1] = r^n - Ic[7,1] = 2(n-1)*r^(n-1) - Ic[7,3] = 4π * G_norm * ρ - end - elseif type == "inertial" - @warn "Inertial core boundary conditions have not been fully implemented. Use with caution." - - φ = 4π * G_norm * ρ / 3 - α = sqrt(K / ρ) - f = -ω^2 / φ - h = f - (n + 1) - k2 = (ω^2 + 4φ - n*(n+1)*φ^2 / ω^2) / α^2 - k = sqrt(Complex{BigFloat}(k2)) - x = k * r - - x64 = ComplexF64(x) - - jl_n = sphericalbesselj(n, x64) - jl_np1 = sphericalbesselj(n+1, x64) - - ϕl = doublefactorial(2n+1) / x^n * jl_n - ϕlp1 = doublefactorial(2n+3) / x^(n+1) * jl_np1 - ψl = 2*(2n+3)/x^2 * (1 - ϕl) - pref = -r^(n+1) / (2n + 3) - - if M == 6 - Ic[1,1] = n * r^(n-1) - Ic[2,1] = r^(n-1) - Ic[3,1] = 0.0 - Ic[4,1] = 0.0 - Ic[5,1] = -(n*φ - ω^2) * r^n - Ic[6,1] = -(2*(n-1)*n*φ - (2*n + 1)*ω^2) * r^(n-1) - - Ic[1,2] = pref * (0.5 * n * h * ψl + f * ϕlp1) - Ic[2,2] = pref * (0.5 * h * ψl - ϕlp1) - Ic[3,2] = -φ * r^n * f * ϕl - Ic[4,2] = 0.0 - Ic[5,2] = -r^(n+2) * ( - (α^2 * f)/r^2 - (3φ*f)/(2*(2n+3)) * ψl - ) - Ic[6,2] = -r^(n+1) * ( - (2n+1)*(α^2*f)/r^2 - - (3φ*((2n+1)*f - n*h))/(2*(2n+3)) * ψl - ) - - Ic[:,3] .= 0.0 - Ic[2,3] = 1.0 # tangential slip - elseif M == 8 - Ic[1,1] = n * r^(n-1) - Ic[2,1] = r^(n-1) - Ic[5,1] = 0.0 - Ic[6,1] = 0.0 - Ic[3,1] = -(n*φ - ω^2) * r^n - Ic[7,1] = -(2*(n-1)*n*φ - (2*n + 1)*ω^2) * r^(n-1) - - Ic[1,2] = pref * (0.5 * n * h * ψl + f * ϕlp1) - Ic[2,2] = pref * (0.5 * h * ψl - ϕlp1) - Ic[5,2] = -φ * r^n * f * ϕl - Ic[6,2] = 0.0 - Ic[3,2] = -r^(n+2) * ( - (α^2 * f)/r^2 - (3φ*f)/(2*(2n+3)) * ψl - ) - Ic[7,2] = -r^(n+1) * ( - (2n+1)*(α^2*f)/r^2 - - (3φ*((2n+1)*f - n*h))/(2*(2n+3)) * ψl - ) - - Ic[:,3] .= 0.0 - Ic[2,3] = 1.0 # tangential slip - end - elseif type == "solid" # incompressible solid core - if M == 6 - # First column - Ic[1, 1] = n*r^( n+1 ) / ( 2*( 2n + 3) ) - Ic[2, 1] = ( n+3 )*r^( n+1 ) / ( 2*( 2n+3 ) * ( n+1 ) ) - Ic[3, 1] = ( n*ρ*g*r + 2*( n^2 - n - 3)*μ ) * r^n / ( 2*( 2n + 3) ) - Ic[4, 1] = n *( n+2 ) * μ * r^n / ( ( 2n + 3 )*( n+1 ) ) - Ic[6, 1] = 2π*G_norm*ρ*n*r^( n+1 ) / ( 2n + 3 ) - - # Second column - Ic[1, 2] = r^( n-1 ) - Ic[2, 2] = r^( n-1 ) / n - Ic[3, 2] = ( ρ*g*r + 2*( n-1 )*μ ) * r^( n-2 ) - Ic[4, 2] = 2*( n-1 ) * μ * r^( n-2 ) / n - Ic[6, 2] = 4π*G_norm*ρ*r^( n-1 ) - - # Third column - Ic[3, 3] = -ρ * r^n - Ic[5, 3] = -r^n - Ic[6, 3] = -( 2n + 1) * r^( n-1 ) - elseif M == 8 - # First column - Ic[1, 1] = n*r^( n+1 ) / ( 2*( 2n + 3) ) - Ic[2, 1] = ( n+3 )*r^( n+1 ) / ( 2*( 2n+3 ) * ( n+1 ) ) - Ic[5, 1] = ( n*ρ*g*r + 2*( n^2 - n - 3)*μ ) * r^n / ( 2*( 2n + 3) ) - Ic[6, 1] = n *( n+2 ) * μ * r^n / ( ( 2n + 3 )*( n+1 ) ) - Ic[7, 1] = 2π*G_norm*ρ*n*r^( n+1 ) / ( 2n + 3 ) - - # Second column - Ic[1, 2] = r^( n-1 ) - Ic[2, 2] = r^( n-1 ) / n - Ic[5, 2] = ( ρ*g*r + 2*( n-1 )*μ ) * r^( n-2 ) - Ic[6, 2] = 2*( n-1 ) * μ * r^( n-2 ) / n - Ic[7, 2] = 4π*G_norm*ρ*r^( n-1 ) - - # Third column - Ic[5, 3] = -ρ * r^n - Ic[3, 3] = -r^n - Ic[7, 3] = -( 2n + 1) * r^( n-1 ) - end - else - error("Invalid core type: $type. Must be 'liquid', 'inertial', or 'solid'.") - end - - return Ic - end - - - """ - get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, M=8) - - Compute the 6x6 `A` matrix in the ODE for the solid-body problem. - - # Arguments - - `ω::prec` : Forcing frequency of the tidal forcing. - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. - - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. - - `M::Int=8` : Number of rows in the A matrix. This should be 6 for the solid-body problem, but can be 8 for the two-phase problem. - - # Returns - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - """ - function get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, M=8) - A = zeros(precc, 6, 6) - get_A!(A, ω, r, ρ, g, μ, K, n; G0=G0, λ=λ, M=M) - return A - end - - - """ - get_A(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, λ=nothing) - - Compute the 8x8 `A` matrix in the ODE for the two-phase problem. These correspond to - the coefficients given in Equation S4.6 in Hay et al., (2025). - - # Arguments - - `ω::prec` : Forcing frequency. - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `ρₗ::prec` : Liquid density at radius r. - - `Kl::prec` : Liquid bulk modulus at radius r. - - `Kd::prec` : Drained bulk modulus at radius r. - - `α::prec` : Biot coefficient at radius r. - - `ηₗ::prec` : Liquid viscosity at radius r. - - `ϕ::prec` : Porosity at radius r. - - `k::prec` : Permeability at radius r. - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. - - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. - - # Returns - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - - See also [`get_A!`](@ref) - """ - function get_A(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, λ=nothing) - A = zeros(precc, 8, 8) - get_A!(A, ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=G0, λ=λ) - return A - end - - - """ - get_A!(A, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, M=8) - - Compute the 6x6 `A` matrix in the ODE for the solid-body problem. These correspond to - the coefficients given in Equation S4.6 in Hay et al., (2025) when α=φ=0, as well as Sabadini and Vermeersen - (2016) Eq. 1.95. - - # Arguments - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - - `ω::prec` : Forcing frequency of the tidal forcing. - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. - - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. - - `M::Int=8` : Number of rows in the A matrix. This should be 6 for the solid-body problem, but can be 8 for the two-phase problem. - """ - function get_A!(A::Matrix, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, M=8) - if isnothing(λ) - λ = K - 2μ/3 - end - - G_norm = G / G0 - - r_inv = 1.0/r - β_inv = 1.0/(2μ + λ) - rβ_inv = r_inv * β_inv - - if M == 8 - A[1,1] = -2λ * r_inv*β_inv - A[2,1] = -r_inv - A[5,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) - ω^2 * ρ - A[6,1] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[3,1] = 4π * G_norm * ρ - A[7,1] = 4π*(n+1)*G_norm*ρ*r_inv - - A[1,2] = n*(n+1) * λ * r_inv*β_inv - A[2,2] = r_inv - A[5,2] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[6,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) - ω^2 * ρ - A[7,2] = -4π*n*(n+1)*G_norm*ρ*r_inv - - A[1,5] = β_inv - A[5,5] = r_inv*β_inv * (-4μ ) - A[6,5] = -λ * r_inv*β_inv - - A[2,6] = 1.0 / μ - A[5,6] = n*(n+1)*r_inv - A[6,6] = -3r_inv - - A[5,3] = ρ * (n+1)*r_inv - A[6,3] = -ρ*r_inv - A[3,3] = -(n+1)r_inv - - A[5,7] = -ρ - A[3,7] = 1.0 - A[7,7] = (n-1)r_inv - - elseif M ==6 - A[1,1] = -2λ * r_inv*β_inv - A[2,1] = -r_inv - A[4,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) - ω^2 * ρ - A[5,1] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[3,1] = 4π * G_norm * ρ - A[6,1] = 4π*(n+1)*G_norm*ρ*r_inv - - A[1,2] = n*(n+1) * λ * r_inv*β_inv - A[2,2] = r_inv - A[4,2] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[5,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) - ω^2 * ρ - A[6,2] = -4π*n*(n+1)*G_norm*ρ*r_inv - - A[1,4] = β_inv - A[4,4] = r_inv*β_inv * (-4μ ) - A[5,4] = -λ * r_inv*β_inv - - A[2,5] = 1.0 / μ - A[4,5] = n*(n+1)*r_inv - A[5,5] = -3r_inv - - A[4,3] = ρ * (n+1)*r_inv - A[5,3] = -ρ*r_inv - A[3,3] = -(n+1)r_inv - - A[4,6] = -ρ - A[3,6] = 1.0 - A[6,6] = (n-1)r_inv - end - end - - - """ - get_A!(A, ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) - - Compute the 8x8 `A` matrix in the ODE for the two-phase problem. These correspond to - the coefficients given in Equation S4.6 in Hay et al., (2025). - - # Arguments - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - - `ω::prec` : Forcing frequency. - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `ρₗ::prec` : Liquid density at radius r. - - `Kl::prec` : Liquid bulk modulus at radius r. - - `Kd::prec` : Drained bulk modulus at radius r. - - `α::prec` : Biot coefficient at radius r. - - `ηₗ::prec` : Liquid viscosity at radius r. - - `ϕ::prec` : Porosity at radius r. - - `k::prec` : Permeability at radius r. - - `n::Int` : Tidal degree. - - # Notes - See also [`get_A`](@ref) - """ - function get_A!(A::Matrix, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, λ=nothing) - # λ = K - 2μ/3 # Lame's second param, which uses the drained compaction modulus - λ = Kd .- 2μ/3 # Lame's second param, which uses the drained compaction modulus - S = ϕ/Kl + (α - ϕ)/K # Storavity, which uses liquid and solid grain bulk moduli - - # First add the solid-body coefficients, but using drained moduli. - get_A!(A, ω, r, ρ, g, μ, Kd, n; λ=λ, G0=G0, M=8) # Note that here we replace the bulk modulus with the compaction modulus - - r_inv = 1.0/r - β_inv = 1.0/(2μ + λ) - - G_norm = G / G0 - - # ϕ = 0. - # If there is a porous layer, now add the two-phase components - if !iszero(ϕ) - - A[1,4] = α * β_inv - - A[5,1] += 1im * k*ρₗ^2 *g^2 * n*(n+1) / (ω*ηₗ) * r_inv^2 - A[5,3] += -(n+1)r_inv * 1im *(k*ρₗ^2*g*n)/(ω*ηₗ) * r_inv - A[5,4] = 1im * (k*ρₗ*g*n*(n+1))/(ω*ηₗ)*r_inv^2 - 4μ*α*β_inv*r_inv - A[5,8] = 1im * (k*ρₗ^2*g^2*n*(n+1))/(ω*ηₗ)*r_inv^2 - 4ϕ*ρₗ*g*r_inv - - A[6,4] = 2α*μ*r_inv * β_inv - A[6,8] = ϕ*ρₗ*g*r_inv - - A[3,8] = 4π*G_norm*ρₗ*ϕ - - A[7,1] += -1im * 4π*G_norm*n*(n+1)*r_inv * (k*ρₗ^2*g/(ω*ηₗ)*r_inv) - A[7,3] = 1im*4π*n*(n+1)G_norm*(ρₗ)^2*k*r_inv^2 / (ω*ηₗ) - A[7,4] = -1im *4π*n*(n+1)G_norm*ρₗ*k*r_inv^2 / ( ω*ηₗ) - A[7,8] = 4π*G_norm*(n+1)*r_inv * (ϕ*ρₗ - 1im * n*k*ρₗ^2*g/(ω*ηₗ)*r_inv) - - A[4,1] = ρₗ*g*r_inv * ( 4 - 1im *(k*ρₗ*g*n*(n+1)/(ω*ϕ*ηₗ))*r_inv) - A[4,2] = -ρₗ*n*(n+1)*r_inv*g - A[4,3] = -ρₗ*(n+1)r_inv * (1 - 1im*(k*ρₗ*g*n)/(ω*ϕ*ηₗ)*r_inv) - A[4,7] = ρₗ - A[4,4] = - 1im*(k*ρₗ*g*n*(n+1))/(ω*ϕ*ηₗ)*r_inv^2 - A[4,8] = -1im*ω*ϕ*ηₗ/k - 4π*G_norm*(ρ - ϕ*ρₗ)*ρₗ + ρₗ*g*r_inv*(4 - 1im*(k*ρₗ*g*n*(n+1))/(ω*ϕ*ηₗ)*r_inv) - - A[8,1] = r_inv*( 1im * k*ρₗ*g*n*(n+1)/(ω*ϕ*ηₗ)*r_inv - α/ϕ * 4μ*β_inv) - A[8,2] = α/ϕ * 2n*(n+1)*μ *β_inv * r_inv - A[8,5] = -α/ϕ * β_inv - A[8,3] = -1im * k *ρₗ *n*(n+1) / (ω*ϕ*ηₗ)*r_inv^2 - A[8,4] = 1im*k*n*(n+1)/(ω*ϕ*ηₗ)*r_inv^2 - 1/ϕ * (S + α^2 * β_inv) # If solid and liquid are compressible, keep the 1/M term - A[8,8] = 1im * k *ρₗ*g *n*(n+1) / (ω*ϕ*ηₗ)*r_inv^2 - 2r_inv - end - - end - """ solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core; core="liquid") @@ -742,142 +272,147 @@ module solid1d_mush_relax - `y_t::Vector{precc}` : Vector of length 8 representing the tidal solution at the top of the mantle. This includes the displacements, stresses, and potential at the surface. - `y_l::Vector{precc}` : Vector of length 8 representing the load solution at the top of the mantle. This includes the displacements, stresses, and potential at the surface. - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. - - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. - `S::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the normalization. - - `ifc::Int` : Index of the first interface layer (the one closer to the core). - - `ifd::Int` : Index of the second interface layer (the one closer to the surface). + - `transitions::Vector{Int}` : Indices of the interface layers (the ones closer to the core and surface). """ function solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, M_tot; core="liquid") - # 1. Find the original interface index - ifc_orig = findfirst(k .> 0) - ifd_orig = findlast(k .> 0) + # Define the ordering of the solution vector components for the 6x6 and 8x8 cases + Y6 = [1,2,4,5,3,6] + Y8 = [1,2,5,6,3,7,4,8] - vars = (r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k) - new_vars = map(vars) do v - v_new = copy(v) - insert!(v_new, ifc_orig, v[ifc_orig]) - insert!(v_new, ifd_orig + 1, v[ifd_orig]) - v_new + # 1. Identify Regions and Transitions + # We define a boolean mask where true = mushy, false = solid + is_mush = k .> 0 + + # Detect indices where the state changes + transitions = findall(diff(is_mush) .!= 0) + + # 2. Duplicate nodes at transition points for the relaxation scheme + # We iterate backwards to keep indices valid during insertion + new_r, new_ρ, new_g, new_μ, new_K, new_ρₗ, new_Kl, new_Kd, new_α, new_ηₗ, new_ϕ, new_k = + copy(r), copy(ρ), copy(g), copy(μ), copy(K), copy(ρₗ), copy(Kl), copy(Kd), copy(α), copy(ηₗ), copy(ϕ), copy(k) + + for trans_idx in reverse(transitions) + for v in (new_r, new_ρ, new_g, new_μ, new_K, new_ρₗ, new_Kl, new_Kd, new_α, new_ηₗ, new_ϕ, new_k) + insert!(v, trans_idx + 1, v[trans_idx]) + end end - new_r, new_ρ, new_g, new_μ, new_K, new_ρₗ, new_Kl, new_Kd, new_α, new_ηₗ, new_ϕ, new_k = new_vars Nr = length(new_r) - ifc = ifc_orig # The first of the two duplicate layers - ifd = ifd_orig + 1 # The second of the two duplicate layers - - # 3. Define the new segments for the relaxation scheme - # Segment 3 now covers the transition between the two identical radial points - ids = [ - (1, 2), # 1: Core Boundary - (2, ifc-1), # 2: Solid Propagation - (ifc-1, ifc+1), # 3: Interface Transition (duplicate layer) - (ifc+1, ifd-1), # 4: Mushy Propagation - (ifd-1, ifd+1), # 5: Interface Transition (duplicate layer) - (ifd+1, Nr-1), # 6: Solid Propagation - (Nr-1, Nr) # 7: Surface Boundary - ] - - # 4. Non-dimensional scaling (Not working atm, use 1.,1.,1. for now) - R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(1., 1., 1.) - # R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(r[end], M_tot, g[end]) - - ωs = ω * s0 - rs = new_r ./ R0 - ρs = new_ρ ./ ρ0 - gs = new_g ./ g0 - μs = new_μ ./ μ0 - Ks = new_K ./ μ0 - ρₗs = new_ρₗ ./ ρ0 - Kls = new_Kl ./ μ0 - Kds = new_Kd ./ μ0 - ηₗs = new_ηₗ ./ (μ0 * s0) - ks = new_k ./ R0^2 - - # 5. Initialize Matrices - R = [zeros(precc, 8, 8) for _ in 1:Nr] - B = [zeros(precc, 8, 1) for _ in 1:Nr] - # Define the specific indices used by 6x6 - idx = [1, 2, 3, 5, 6, 7] + new_is_mush = new_k .> 0 - # Create the view using these indices for both rows and columns - R6_view = [view(R[i], idx, idx) for i in 1:Nr] - R8_view = [view(R[i], 1:8, 1:8) for i in 1:Nr] - B6_view = [view(B[i], idx, 1) for i in 1:Nr] - B8_view = [view(B[i], 1:8, 1) for i in 1:Nr] - - # component 1: apply core boundary condition and get first solution (3x6) - C1l, D2l = core_boundary(R6_view, ids[1], rs, ρs, gs, μs, Ks, ωs, ρ_core/ρ0, core, n; G0=G0) - - # component 1: apply core boundary condition and get first solution (4x8) - # C1l, D2l = core_boundary_mush(R8_view, ids[1], rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, α, ηₗs, ϕ, ks, ρ_core/ρ0, core, n; G0=G0) - - # component 2: propagate the solution up to the surface (6x6) - C1l, D2l = propagate_solid(R6_view, B6_view, C1l, D2l, ids[2], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) - - # component 3: interface between 6x6 and 8x8 - C1l, D2l = interface_solid_mush(R8_view, B8_view, C1l, D2l, ids[3]) + # 3. Dynamic Scaling (needs to be checked for consistency with the non-dimensionalization in get_scales) + R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(1., 1., 1.; Y=Y8) + ωs = ω * s0 + rs, ρs, gs, μs, Ks = new_r./R0, new_ρ./ρ0, new_g./g0, new_μ./μ0, new_K./μ0 + ρₗs, Kls, Kds, ηₗs, ks = new_ρₗ./ρ0, new_Kl./μ0, new_Kd./μ0, new_ηₗ./(μ0*s0), new_k./R0^2 - # component 4: propagate the solution up to the surface (8x8) - C1l, D2l = propagate_mush(R8_view, B8_view, C1l, D2l, ids[4], rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, new_α, ηₗs, new_ϕ, ks, n; G0=G0) + # 4. Initialize Matrices + R = [Matrix{precc}(I, 8, 8) for _ in 1:Nr] + B = [zeros(precc, 8, 1) for _ in 1:Nr] + idx_6 = [1, 2, 3, 5, 6, 7] + R6_view = [view(R[i], idx_6, idx_6) for i in 1:Nr] + R8_view = [view(R[i], 1:8, 1:8) for i in 1:Nr] - # component 5: interface between 8x8 and 6x6 - C1l, D2l = interface_mush_solid(R8_view, B8_view, C1l, D2l, ids[5]) + # 5. Adaptive Propagation Loop + @debug("\n--- Adaptive Solver Plan ---") + @debug("Total Nodes (Nr): $Nr") + @debug("Transitions at indices: $transitions") + + curr_idx = 1 + # Step 1: Core + if !new_is_mush[1] + @debug("STEP 1: [Core Boundary] Solid | Indices: (1, 2)") + C1l, D2l = core_boundary(R6_view, (1, 2), rs, ρs, gs, μs, Ks, ωs, ρ_core/ρ0, core, n; G0=G0, Y=Y6) + curr_idx = 2 + else + @debug("STEP 1: [Core Boundary] Mushy | Indices: (1, 2)") + C1l, D2l = core_boundary_mush(R8_view, (1, 2), rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, new_α, ηₗs, new_ϕ, ks, ρ_core/ρ0, core, n; G0=G0, Y=Y8) + curr_idx = 2 + end - # component 6: propagate the solution up to the surface (6x6) - C1l, D2l = propagate_solid(R6_view, B6_view, C1l, D2l, ids[6], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) + # Step 2: Propagation and Jumps + while curr_idx < Nr + next_change = findnext(x -> x != new_is_mush[curr_idx], new_is_mush, curr_idx) + segment_end = (next_change === nothing) ? Nr : next_change - 1 + + # Safety check for empty ranges + if segment_end > curr_idx + if !new_is_mush[curr_idx] + @debug("STEP: [Propagate Solid] | Range: ($curr_idx, $segment_end)") + C1l, D2l = propagate_solid(R6_view, C1l, D2l, (curr_idx, segment_end-1), + rs, ρs, gs, μs, Ks, ωs, n; G0=G0, Y=Y6) + else + @debug("STEP: [Propagate Mushy] | Range: ($curr_idx, $segment_end)") + C1l, D2l = propagate_mush(R8_view, C1l, D2l, (curr_idx, segment_end-1), + rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, new_α, ηₗs, new_ϕ, ks, n; G0=G0, Y=Y8) + end + end - # component 7: apply surface boundary condition and solve for the final solution at the surface - y_t, y_l = surface_boundary(R6_view, B6_view, C1l, D2l, ids[7], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) + if next_change !== nothing + # The jump occurs at the duplicated node + trans_range = (next_change - 1, next_change + 1) + if !new_is_mush[curr_idx] && new_is_mush[next_change] + @debug("STEP: [Interface Jump] Solid -> Mushy | Range: $trans_range") + C1l, D2l = interface_solid_mush(R8_view, C1l, D2l, trans_range; Y=Y8) + else + @debug("STEP: [Interface Jump] Mushy -> Solid | Range: $trans_range") + C1l, D2l = interface_mush_solid(R8_view, C1l, D2l, trans_range; Y=Y8) + end + curr_idx = next_change + 1 + else + curr_idx = Nr + end + end - # component 3: apply surface boundary condition and solve for the final solution at the surface - # y_t, y_l = surface_boundary_mush(R8_view, B8_view, C1l, D2l, ids[5], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) + # Step 3: Surface + if !new_is_mush[Nr] + @debug("STEP: [Surface Boundary] Solid | Indices: ($(Nr-1), $Nr)") + y_t, y_l = surface_boundary(R6_view, C1l, D2l, (Nr-1, Nr), rs, ρs, gs, μs, Ks, ωs, n; G0=G0, Y=Y6) + else + @debug("STEP: [Surface Boundary] Mushy | Indices: ($(Nr-1), $Nr)") + y_t, y_l = surface_boundary_mush(R8_view, C1l, D2l, (Nr-1, Nr), rs, ρs, gs, μs, Ks, ωs, n; G0=G0, Y=Y8) + end + @debug("--- Solver Complete ---\n") - return y_t, y_l, R, B, S, ifc, ifd + return y_t, y_l, R, S, transitions end """ - interface_mush_solid(R8, B8, Cn_l, Dnp_l, ids) + interface_mush_solid(R8, Cn_l, Dnp_l, ids; Y=[1,2,3,4,5,6,7,8]) Perform the forward-backward relaxation step at the interface between the mushy layer and the solid layer. This function implements the recursion described in N. Kobayashi (2007) for the transition from the 8x8 system to the 6x6 system. # Arguments - `R8::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. - - `B8::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. - `Cn_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Cn matrix from the previous step. - `Dnp_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Dnp matrix from the previous step. - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + # keyword arguments + - `Y::Vector{Int}=1:8` : Vector of column indices corresponding to the 6x6 system variables in the 8x8 system. This allows for + # Returns - `Cn_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Cn matrix for the next iteration. - `Dnp_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. """ - function interface_mush_solid(R8, B8, Cn_l, Dnp_l, ids) + function interface_mush_solid(R8, Cn_l, Dnp_l, ids; Y=[1,2,3,4,5,6,7,8]) start_id, end_id = ids - # Impose continuity at the boundary - # Make sure bn[4, 1] = 0.0, i.e. zero darcy flux at boundary - bn = zeros(precc, 8, 1) - - I86 = zeros(precc, 8, 8) I8 = Matrix{precc}(I, 8, 8) - icc = [1.,1.,1.,0.,1.,1.,1.,1.] - idd = [1.,1.,1.,0.,1.,1.,1.,0.] - for i in 1:8 - I86[i, i] = icc[i] - I8[i, i] = idd[i] - end - - Cn = I86 + Cn = I8 Dnp = -I8 + Dnp[4, 4] = 0.0 + Dnp[8, 8] = 0.0 - target_cols = [1, 2, 3, 5, 6, 7] + target_cols = [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]] # 1. Use the "stored" lower halves from the previous step # to fill the upper blocks of P and S. Pn_u = Cn_l @@ -893,12 +428,15 @@ module solid1d_mush_relax Sn = [Sn_u; Cn_u] Qn = [Qn_u; Dnp_u] + Kn = zeros(precc, 8, 8) + Kn[8, 8] = 1.0 + # 4. Perform recursion - Xn = Pn * R8[start_id] + Sn + Xn = Pn * R8[start_id] + Sn + Kn + R_ifc = - pinv(Xn) * Qn R8[start_id+1] .= R_ifc - B8[start_id+1] .= pinv(Xn) * (bn - Pn * B8[start_id]) # 5. Update the "stored" lower halves for the next iteration Cn_l = Cn[5:7, target_cols] @@ -909,80 +447,74 @@ module solid1d_mush_relax """ - interface_solid_mush(R8, B8, Cn_l, Dnp_l, ids) + interface_solid_mush(R, Cn_l, Dnp_l, ids; Y=[1,2,3,4,5,6,7,8]) Perform the forward-backward relaxation step at the interface between the solid layer and the mushy layer. This function implements the recursion described in N. Kobayashi (2007) for the transition from the 6x6 system to the 8x8 system. # Arguments - - `R8::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. - - `B8::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. + - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. - `Cn_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Cn matrix from the previous step. - `Dnp_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Dnp matrix from the previous step. - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. + # keyword arguments + - `Y::Vector{Int}=1:8` : Vector of column indices corresponding to the 6x6 system variables in the 8x8 system. This allows for + # Returns - `Cn_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Cn matrix for the next iteration. - `Dnp_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. """ - function interface_solid_mush(R8, B8, Cn_l, Dnp_l, ids) + function interface_solid_mush(R, Cn_l, Dnp_l, ids; Y=[1,2,3,4,5,6,7,8]) start_id, end_id = ids - # impose continuity at the boundary - bn = zeros(precc, 8, 1) - # Induce some pore pressure - bn[4, 1] = -1.0 - - I86 = zeros(precc, 8, 8) - iss = [1.,1.,1.,1.,1.,1.,1.,0.] - for i in 1:8 - I86[i, i] = iss[i] - end - - I8 = Matrix{precc}(I, 8, 8) - - Cn = I86 - Dnp = -I8 - - target_cols = [1, 2, 3, 5, 6, 7] - # 1. Use the "stored" lower halves from the previous step - # to fill the upper blocks of P and S. + # target_cols + target_cols = [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]] + + # expand the incoming 3x6 Solid Lower blocks to 4x8 Pn_u = zeros(precc, 4, 8) Pn_u[1:3, target_cols] .= Cn_l - + Sn_u = zeros(precc, 4, 8) Sn_u[1:3, target_cols] .= Dnp_l - - Qn_u = zeros(precc, 4, 8) - - # 2. Get the upper halves of the NEWLY calculated Cn and Dnp - Cn_u = Cn[1:4, :] - Dnp_u = Dnp[1:4, :] - - # 3. Build the 8x8 blocks + + # current Layer (Porous side) + # treat the interface as an infinitesimal jump where C = I, D = -I + I8 = Matrix{precc}(I, 8, 8) + Cn_curr = I8 + Cn_curr[4, 4] = 0.0 + Cn_curr[8, 8] = 0.0 + Dnp_curr = -I8 + + Cn_u = Cn_curr[1:4, :] + Dnp_u = Dnp_curr[1:4, :] + + # assemble 8x8 system Pn = [Pn_u; zeros(precc, 4, 8)] Sn = [Sn_u; Cn_u] - Qn = [Qn_u; Dnp_u] + Qn = [zeros(precc, 4, 8); Dnp_u] - # 4. Perform recursion - Xn = Pn * R8[start_id] + Sn - R_ifc = - pinv(Xn) * Qn - - R8[start_id+1] .= R_ifc - B8[start_id+1] .= pinv(Xn) * (bn - Pn * B8[start_id]) - - # 5. Update the "stored" lower halves for the next iteration - Cn_l = Cn[5:8, :] - Dnp_l = Dnp[5:8, :] + # introduce some pore pressure at the boundary to drive the solution in the mushy layer + Kn = zeros(precc, 8, 8) + Kn[8, 8] = 1.0 - return Cn_l, Dnp_l + # solve the jump + Xn = Pn * R[start_id] + Sn + Kn + R_ifc = - pinv(Xn) * Qn + R[start_id+1] .= R_ifc + + # pass the Lower halves of the Porous Identity to the next propagator + Cn_l_new = Cn_curr[5:8, :] + Dnp_l_new = Dnp_curr[5:8, :] + + return Cn_l_new, Dnp_l_new end """ - core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n) + core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n; G0=1, Y=[1,2,3,4,5,6]) Perform the forward-backward relaxation step at the core boundary. This function implements the recursion described in N. Kobayashi (2007) for the initial step of the relaxation scheme, where we apply the core boundary condition and @@ -1003,12 +535,13 @@ module solid1d_mush_relax Keyword Arguments - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + - `Y::Vector{Int}=[1,2,3,4,5,6]` : Ordering of the solution vector components. # Returns - `C1l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the C1 matrix for the next iteration. - `D2l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the D2 matrix for the next iteration. """ - function core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n; G0=1) + function core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n; G0=1, Y=[1,2,3,4,5,6]) start_id, end_id = ids @@ -1018,8 +551,8 @@ module solid1d_mush_relax # first layer (n = 1) dr = r[end_id] - r[start_id] - A1 = get_A(ω, r[start_id], ρ[start_id], g[start_id], μ[start_id], K[start_id], n; G0=G0, M=6) - A2 = get_A(ω, r[end_id], ρ[end_id], g[end_id], μ[end_id], K[end_id], n; G0=G0, M=6) + A1 = get_A(ω, r[start_id], ρ[start_id], g[start_id], μ[start_id], K[start_id], n; G0=G0, Y=Y) + A2 = get_A(ω, r[end_id], ρ[end_id], g[end_id], μ[end_id], K[end_id], n; G0=G0, Y=Y) I6 = Matrix{precc}(I, 6, 6) @@ -1042,7 +575,7 @@ module solid1d_mush_relax """ - core_boundary_mush(R, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, ρ_core, core, n) + core_boundary_mush(R, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, ρ_core, core, n; G0=1, Y=[1,2,3,4,5,6,7,8]) Perform the forward-backward relaxation step at the core boundary for the two-phase problem. This function implements the recursion described in N. Kobayashi (2007) for the initial step of the relaxation scheme, where we apply the core @@ -1076,21 +609,21 @@ module solid1d_mush_relax - `C1l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the C1 matrix for the next iteration. - `D2l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the D2 matrix for the next iteration. """ - function core_boundary_mush(R, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, ρ_core, core, n; G0=1) + function core_boundary_mush(R, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, ρ_core, core, n; G0=1, Y=[1,2,3,4,5,6,7,8]) start_id, end_id = ids # boundary conditions - B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ[start_id], K[start_id], core, n; G0=G0, M=8, N=4) + B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ[start_id], K[start_id], core, n; G0=G0, Y=Y) # first layer (n = 1) dr = r[end_id] - r[start_id] A1 = get_A(ω, r[start_id], ρ[start_id], g[start_id], μ[start_id], K[start_id], - ρₗ[start_id], Kl[start_id], Kd[start_id], α[start_id], ηₗ[start_id], ϕ[start_id], k[start_id], n; G0=G0) + ρₗ[start_id], Kl[start_id], Kd[start_id], α[start_id], ηₗ[start_id], ϕ[start_id], k[start_id], n; G0=G0, Y=Y) A2 = get_A(ω, r[end_id], ρ[end_id], g[end_id], μ[end_id], K[end_id], - ρₗ[end_id], Kl[end_id], Kd[end_id], α[end_id], ηₗ[end_id], ϕ[end_id], k[end_id], n; G0=G0) + ρₗ[end_id], Kl[end_id], Kd[end_id], α[end_id], ηₗ[end_id], ϕ[end_id], k[end_id], n; G0=G0, Y=Y) I8 = Matrix{precc}(I, 8, 8) @@ -1113,7 +646,7 @@ module solid1d_mush_relax """ - propagate_solid(R, B, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, n) + propagate_solid(R, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, n; G0=1, Y=[1,2,3,4,5,6]) Perform the forward-backward relaxation step for the solid propagation segments. This function implements the recursion described in N. Kobayashi (2007) for the segments of the radial grid that correspond to the solid @@ -1121,7 +654,6 @@ module solid1d_mush_relax # Arguments - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. - - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. - `Cn_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Cn matrix from the previous step. - `Dnp_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the Dnp matrix from the previous step. - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. @@ -1135,12 +667,13 @@ module solid1d_mush_relax Keyword Arguments - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + - `Y::Vector{Int}=[1,2,3,4,5,6]` : Ordering of the solution vector components. # Returns - `Cn_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Cn matrix for the next iteration. - `Dnp_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. """ - function propagate_solid(R, B, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, n; G0=1) + function propagate_solid(R, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, n; G0=1, Y=[1,2,3,4,5,6]) start_id, end_id = ids @@ -1155,8 +688,8 @@ module solid1d_mush_relax dr = r[i+1] - r[i] # Calculate A at current and next step - A_n = get_A(ω, r[i], ρ[i], g[i], μ[i], K[i], n; G0=G0, M=6) - A_np = get_A(ω, r[i+1], ρ[i+1], g[i+1], μ[i+1], K[i+1], n; G0=G0, M=6) + A_n = get_A(ω, r[i], ρ[i], g[i], μ[i], K[i], n; G0=G0, Y=Y) + A_np = get_A(ω, r[i+1], ρ[i+1], g[i+1], μ[i+1], K[i+1], n; G0=G0, Y=Y) Cn = I6 + 0.5 * dr * A_n Dnp = -I6 + 0.5 * dr * A_np @@ -1178,9 +711,14 @@ module solid1d_mush_relax # 4. Perform recursion Xn = Pn * R[i-1] + Sn - R[i] .= -Xn \ Qn - B[i] .= Xn \ (-Pn * B[i-1]) - + + if i == start_id + # For the first step into the mush, we may need to use pinv if the system is not yet fully constrained by the solid boundary conditions. + R[i] .= - pinv(Xn) * Qn + else + R[i] .= -Xn \ Qn + end + # 5. Update the "stored" lower halves for the next iteration Cn_l = Cn[4:6, :] Dnp_l = Dnp[4:6, :] @@ -1191,7 +729,7 @@ module solid1d_mush_relax """ - propagate_mush(R, B, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + propagate_mush(R, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, Y=[1,2,3,4,5,6,7,8]) Perform the forward-backward relaxation step for the mushy layer propagation segment. This function implements the recursion described in N. Kobayashi (2007) for the segment of the radial grid that corresponds to the mushy layer, @@ -1200,7 +738,6 @@ module solid1d_mush_relax # Arguments - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. - - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. - `Cn_l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the Cn matrix from the previous step. - `Dnp_l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the Dnp matrix from the previous step. - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. @@ -1221,12 +758,13 @@ module solid1d_mush_relax Keyword Arguments - `G0::prec=1` : Gravitational constant used for non-dimensional scaling. + - `Y::Vector{Int}=[1,2,3,4,5,6,7,8]` : Indices for the state variables (default is for standard case). # Returns - `Cn_l::Matrix{precc}` : Updated 4x8 matrix representing the "stored" lower half of the Cn matrix for the next iteration. - `Dnp_l::Matrix{precc}` : Updated 4x8 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. """ - function propagate_mush(R, B, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1) + function propagate_mush(R, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, Y=[1,2,3,4,5,6,7,8]) start_id, end_id = ids @@ -1242,10 +780,10 @@ module solid1d_mush_relax # Calculate A at current and next step A_n = get_A(ω, r[i], ρ[i], g[i], μ[i], K[i], - ρₗ[i], Kl[i], Kd[i], α[i], ηₗ[i], ϕ[i], k[i], n; G0=G0) + ρₗ[i], Kl[i], Kd[i], α[i], ηₗ[i], ϕ[i], k[i], n; G0=G0, Y=Y) A_np = get_A(ω, r[i+1], ρ[i+1], g[i+1], μ[i+1], K[i+1], - ρₗ[i+1], Kl[i+1], Kd[i+1], α[i+1], ηₗ[i+1], ϕ[i+1], k[i+1], n; G0=G0) + ρₗ[i+1], Kl[i+1], Kd[i+1], α[i+1], ηₗ[i+1], ϕ[i+1], k[i+1], n; G0=G0, Y=Y) Cn = I8 + 0.5 * dr * A_n Dnp = -I8 + 0.5 * dr * A_np @@ -1268,8 +806,12 @@ module solid1d_mush_relax # 4. Perform recursion Xn = Pn * R[i-1] + Sn - R[i] .= -Xn \ Qn - B[i] .= Xn \ (-Pn * B[i-1]) + if i == start_id + # For the first step into the mush, we may need to use pinv if the system is not yet fully constrained by the solid boundary conditions. + R[i] .= -pinv(Xn) * Qn + else + R[i] .= -Xn \ Qn + end # 5. Update the "stored" lower halves for the next iteration Cn_l = Cn[5:8, :] @@ -1281,7 +823,7 @@ module solid1d_mush_relax """ - surface_boundary(R, B, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n) + surface_boundary(R, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n) Perform the forward-backward relaxation step at the surface boundary. This function implements the recursion described in N. Kobayashi (2007) for the final step of the relaxation scheme, where we apply the surface boundary condition and @@ -1289,7 +831,6 @@ module solid1d_mush_relax # Arguments - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. - - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. - `CNm_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the CNm matrix from the previous step. - `DN_l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the DN matrix from the previous step. - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. @@ -1308,14 +849,14 @@ module solid1d_mush_relax - `y_t::Matrix{precc}` : 6x1 matrix representing the solution at the surface for the tidal problem. - `y_l::Matrix{precc}` : 6x1 matrix representing the solution at the surface for the load problem. """ - function surface_boundary(R, B, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n; G0=1) + function surface_boundary(R, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n; G0=1, Y=Y) start_id, end_id = ids # tidal surface boundary condition - BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0; G0=G0, M=6, N=3) + BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0; G0=G0, Y=Y) # load surface boundary condition - BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0; G0=G0, M=6, N=3) + BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0; G0=G0, Y=Y) PN = [CNm_l; zeros(3,6)] SN_t = [DN_l; BN_t] @@ -1324,12 +865,10 @@ module solid1d_mush_relax XN_t = PN * R[start_id] + SN_t XN_l = PN * R[start_id] + SN_l - BN = - XN_t \ PN * B[start_id] - # solve outer (tides) - y_t = XN_t \ b_t + BN + y_t = XN_t \ b_t # solve outer (load) - y_l = XN_l \ b_l + BN + y_l = XN_l \ b_l return y_t, y_l @@ -1337,7 +876,7 @@ module solid1d_mush_relax """ - surface_boundary_mush(R, B, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n) + surface_boundary_mush(R, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n) Perform the forward-backward relaxation step at the surface boundary for the two-phase problem. This function implements the recursion described in N. Kobayashi (2007) for the final step of the relaxation scheme, where we apply the surface @@ -1346,7 +885,6 @@ module solid1d_mush_relax # Arguments - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. - - `B::Vector{Matrix{precc}}` : Vector of 8x1 matrices representing the inhomogeneous terms of the ODE system at each radial layer. - `CNm_l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the CNm matrix from the previous step. - `DN_l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the DN matrix from the previous step. - `ids::Tuple{Int, Int}` : Tuple containing the start and end indices of the current segment in the radial grid. @@ -1365,14 +903,14 @@ module solid1d_mush_relax - `y_t::Matrix{precc}` : 8x1 matrix representing the solution at the surface for the tidal problem. - `y_l::Matrix{precc}` : 8x1 matrix representing the solution at the surface for the load problem. """ - function surface_boundary_mush(R, B, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n; G0=1) + function surface_boundary_mush(R, CNm_l, DN_l, ids, r, ρ, g, μ, K, ω, n; G0=1, Y=[1,2,3,4,5,6,7,8]) start_id, end_id = ids # tidal surface boundary condition - BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0; M=8, N=4) + BN_t, b_t = get_surface_bc!(r[end], g[end], n, 1, 0, 0, 0; Y=Y) # load surface boundary condition - BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0; M=8, N=4) + BN_l, b_l = get_surface_bc!(r[end], g[end], n, 0, 1, 0, 0; Y=Y) PN = [CNm_l; zeros(4,8)] SN_t = [DN_l; BN_t] @@ -1381,12 +919,10 @@ module solid1d_mush_relax XN_t = PN * R[start_id] + SN_t XN_l = PN * R[start_id] + SN_l - BN = - XN_t \ PN * B[start_id] - # solve outer (tides) - y_t = pinv(XN_t) * b_t + BN + y_t = pinv(XN_t) * b_t # solve outer (load) - y_l = pinv(XN_l) * b_l + BN + y_l = pinv(XN_l) * b_l return y_t, y_l @@ -1394,7 +930,7 @@ module solid1d_mush_relax """ - get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1, M=6, N=3) + get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1, Y=[1,2,3,4,5,6]) Get the core boundary condition matrix `B` for the solid-body problem. The core boundary conditions are derived from the requirement that the radial stress at the core-mantle @@ -1403,34 +939,37 @@ module solid1d_mush_relax # Arguments - `ω::Float64` : Forcing frequency. - `r::prec` : Radial position of the core-mantle boundary. - - `ρ::prec` : Density at the core-mantle boundary. + - `ρ::prec` : Average core density. - `g::prec` : Gravity at the core-mantle boundary. - - `μ::precc` : Complex shear modulus at the core-mantle boundary. - - `K::prec` : Bulk modulus at the core-mantle boundary. + - `μ::precc` : Average core complex shear modulus. + - `K::prec` : Average core bulk modulus. - `type::String` : Type of core boundary condition to apply. Options are "liquid" for a fluid core, "solid" for a solid core, and "inertial" for a core with inertial response. - `n::Int` : Tidal degree. # Keyword Arguments - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. - - `M::Int=6` : Dimensionality of the system (6 for standard, 8 for mushy layer). - - `N::Int=3` : Number of boundary conditions to apply (3 for standard, 4 for mushy layer). + - `Y::Vector{Int}=[1,2,3,4,5,6]` : Indices for the state variables (default is for standard case). # Returns - `B::Array{precc,2}` : 3x6 matrix representing the linear relationship between the state variables at the core and the boundary conditions. """ - function get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1, M=6, N=3) + function get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1, Y=[1,2,3,4,5,6]) + + M = length(Y) + N = Int(M / 2) + # 1. Get the Initial Conditions matrix - Ic = get_Ic(ω, r, ρ, g, μ, K, type, n; G0=G0, M=M, N=N) + Ic = get_Ic(ω, r, ρ, g, μ, K, type, n; G0=G0, Y=Y) # 2. Define indices based on dimensionality # If M=8 (Mushing/Hay 2025): U=1, V=2, phi=5, P=7 | X=3, Y=4, psi=6, R=8 # If M=6 (Standard/Takeuchi): U=1, V=2, phi=5 | X=3, Y=4, psi=6 if M == 8 - idx_u = [1, 2, 3, 4] - idx_s = [5, 6, 7, 8] + idx_u = [Y[1], Y[2], Y[5], Y[7]] + idx_s = [Y[3], Y[4], Y[6], Y[8]] else - idx_u = [1, 2, 5] - idx_s = [3, 4, 6] + idx_u = [Y[1], Y[2], Y[5]] + idx_s = [Y[3], Y[4], Y[6]] end # 3. Partition and calculate the boundary condition coefficients @@ -1456,7 +995,7 @@ module solid1d_mush_relax """ - get_surface_bc!(R, g, n, U, U_prime, tau, P; G0=1, M=6, N=3) + get_surface_bc!(R, g, n, U, U_prime, tau, P; G0=1, Y=[1,2,3,4,5,6,7,8]) Get the surface boundary condition vector `b` and matrix `BN` for the solid-body problem. The surface boundary conditions are determined by setting, respectively (U, U', tau, P) to (1,0,0,0) for tidal Love @@ -1475,15 +1014,17 @@ module solid1d_mush_relax # Keyword Arguments - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. - - `M::Int=6` : Dimensionality of the system (6 for standard, 8 for mushy layer). - - `N::Int=3` : Number of boundary conditions to apply (3 for standard, 4 for mushy layer). + - `Y::Vector{Int}=[1,2,3,4,5,6,7,8]` : Indices for the state variables (default is for standard case). # Returns - `B::Array{precc,2}` : 3x6 matrix representing the linear relationship between the state variables at the surface and the boundary conditions. - `b::Vector{precc}` : Vector of length 6 representing the inhomogeneous part of the surface boundary conditions. """ - function get_surface_bc!(R, g, n, U, U_prime, tau, P; G0=1, M=8, N=4) + function get_surface_bc!(R, g, n, U, U_prime, tau, P; G0=1, Y=[1,2,3,4,5,6,7,8]) + M = length(Y) + N = Int(M / 2) + # Define surface mass load (zeta) based on Farrell/Longman relation zeta = ((2 * n + 1) / (4 * pi * G/G0 * R)) * U_prime @@ -1492,25 +1033,25 @@ module solid1d_mush_relax if M == 8 # radial Stress y3 - b[5] = -g * zeta * (G/G0) / R - P + b[Y[3]] = -g * zeta * (G/G0) / R - P # tangential Stress y4 - b[6] = tau + b[Y[4]] = tau # gravitational potential boundary - b[7] = ((2 * n + 1) / R) * U + 4 * pi * G/G0 * zeta + b[Y[6]] = ((2 * n + 1) / R) * U + 4 * pi * G/G0 * zeta # darcy flux boundary - b[8] = 0. + b[Y[8]] = 0 elseif M == 6 # radial Stress y3 - b[4] = -g * zeta * (G/G0) / R - P + b[Y[3]] = -g * zeta * (G/G0) / R - P # tangential Stress y4 - b[5] = tau + b[Y[4]] = tau # gravitational potential boundary - b[6] = ((2 * n + 1) / R) * U - 4 * pi * G/G0 * zeta + b[Y[6]] = ((2 * n + 1) / R) * U - 4 * pi * G/G0 * zeta else error("Unsupported M value. M should be either 6 or 8.") end @@ -1521,20 +1062,20 @@ module solid1d_mush_relax if M == 8 # stress components - B[1, 5] = 1.0 # radial stress y3 - B[2, 6] = 1.0 # tangential stress y4 + B[1, Y[3]] = 1.0 # radial stress y3 + B[2, Y[4]] = 1.0 # tangential stress y4 # potential component - B[3, 3] = (n + 1) / R - B[3, 7] = 1.0 - B[4, 8] = 1.0 + B[3, Y[5]] = (n + 1) / R + B[3, Y[6]] = 1.0 + B[4, Y[8]] = 1.0 elseif M == 6 # stress components - B[1, 4] = 1.0 # radial stress y3 - B[2, 5] = 1.0 # tangential stress y4 + B[1, Y[3]] = 1.0 # radial stress y3 + B[2, Y[4]] = 1.0 # tangential stress y4 # potential component - B[3, 3] = (n + 1) / R - B[3, 6] = 1.0 + B[3, Y[5]] = (n + 1) / R + B[3, Y[6]] = 1.0 end return B, b @@ -1576,9 +1117,9 @@ module solid1d_mush_relax function compute_y(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, M_tot; core="liquid") # solve radial system to get surface solution and recursion matrices - yN_t, yN_l, R, B, S, ifc, ifd = solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, M_tot; core=core) + yN_t, yN_l, R, S, transitions = solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, M_tot; core=core) - Nr = length(r) + 2 + Nr = length(r) + length(transitions) T = eltype(yN_t) M = length(yN_t) @@ -1593,23 +1134,19 @@ module solid1d_mush_relax # back-substitution for i in Nr-1:-1:1 - if i > ifc && i < ifd - # in the mushy region, use the mushy recursion - y_t[:, i] = R[i] * y_t[:, i+1] + B[i] - else - # in the solid region, use the solid recursion - y_t[:, i] = R[i] * y_t[:, i+1] + B[i] - end + # if this is not a transition point, we perform the recursion step as normal + y_t[:, i] = R[i] * y_t[:, i+1] end - # Create a mask for all columns except the two interface indices - mask = [i for i in 1:size(y_t, 2) if i != ifc && i != ifd] + # keep only indices NOT in the transition list + mask = filter(i -> !(i in transitions), 1:size(y_t, 2)) # Apply the mask to the columns y_t = y_t[:, mask] # apply scaling to get dimensional solution - for i in 1:Nr-2 + # for i in 1:Nr-2 + for i in 1:Nr-length(transitions) y_t[:, i] = S * y_t[:, i] end diff --git a/src/solid1d_relax.jl b/src/solid1d_relax.jl index ed46a1d..1f17710 100644 --- a/src/solid1d_relax.jl +++ b/src/solid1d_relax.jl @@ -2,6 +2,9 @@ module solid1d_relax + include("common.jl") + using .common + using LinearAlgebra using DoubleFloats using AssociatedLegendrePolynomials @@ -223,52 +226,6 @@ module solid1d_relax end - """ - get_scales(R0, M0, g0) - - Compute the characteristic scales for the problem based on a reference radius `R0`, mass `M0`, and density - scale `g0`. These scales are used to non-dimensionalize the equations and ensure numerical stability. - - # Arguments - - `R0::prec` : Reference radius scale (e.g., planetary radius). - - `M0::prec` : Reference mass scale (e.g., planetary mass). - - `g0::prec` : Reference density scale. - - # Returns - Tuple of characteristic scales: - - `R0::prec` : Length scale (m). - - `M0::prec` : Mass scale (kg). - - `s0::prec` : Time scale (s). - - `ρ0::prec` : Density scale (kg/m^3). - - `G0::prec` : Gravitational constant scale (m^3 kg^-1 s^-2). - - `g0::prec` : Gravity scale (m/s^2). - - `μ0::prec` : Shear modulus scale (Pa). - - `S::Diagonal{prec}` : Diagonal scaling matrix for state variables. - - `Sinv::Diagonal{prec}` : Inverse of the scaling matrix S. - """ - function get_scales(R0, M0, g0) - - ρ0 = M0 / R0^3 - P0 = g0 * R0 - μ0 = ρ0 * g0 * R0 - - s0 = sqrt(g0 / R0) - G0 = R0^3 / (M0 * s0^2) - - S = Diagonal(precc[ - R0, # y1: radial displacement (m) - R0, # y2: tangential displacement (m) - μ0, # y3: radial stress (Pa) - μ0, # y4: tangential stress (Pa) - P0, # y5: potential (m^2/s^2) - g0 # y6: potential gradient/gravity (m/s^2) - ]) - - Sinv = inv(S) - return R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv - end - - """ doublefactorial(n) @@ -293,205 +250,6 @@ module solid1d_relax end - """ - get_Ic(ω, r, ρ, g, μ, K, type, n; G0=1, M=6, N=3) - - Get the core solution vector. - https://academic.oup.com/gji/article/203/3/2150/2594863 - - # Arguments - - `ω::prec` : Angular frequency. - - `r::prec` : Radius of the core boundary. - - `ρ::prec` : Density of the core. - - `g::prec` : Gravity at the core boundary. - - `μ::prec` : Shear modulus of the core. - - `K::prec` : Bulk modulus of the core. - - `type::String` : Type of core, either "liquid" or "solid". - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. - - `M::Int=6` : Number of rows in the Ic matrix. This should be 6 for the solid-body problem. - - `N::Int=3` : Number of linearly independent solutions to compute. This should be 3 for the solid-body problem. - - # Returns - - `Ic::Array{precc,2}` : MxN array of linearly independent solutions at the core boundary. These are used as starting vectors for the numerical integration across the interior. - """ - function get_Ic(ω, r, ρ, g, μ, K, type, n; G0=1, M=6, N=3) - Ic = zeros(precc, M, N) - - G_norm = G / G0 - - if type=="liquid" - Ic[1,1] = -r^n / g - Ic[1,3] = 1.0 - Ic[2,2] = 1.0 - Ic[3,3] = g*ρ - Ic[5,1] = r^n - Ic[6,1] = 2(n-1)*r^(n-1) - Ic[6,3] = 4π * G_norm * ρ - elseif type == "inertial" - - @warn "Inertial core boundary conditions have not been fully implemented. Use with caution." - - φ = 4π * G_norm * ρ / 3 - - Ic[1,1] = n * r^(n-1) - Ic[2,1] = r^(n-1) - Ic[3,1] = 0.0 - Ic[4,1] = 0.0 - Ic[5,1] = -(n*φ - ω^2) * r^n - Ic[6,1] = -(2*(n-1)*n*φ - (2*n + 1)*ω^2) * r^(n-1) - - α = sqrt(K / ρ) - f = -ω^2 / φ - h = f - (n + 1) - k2 = (ω^2 + 4φ - n*(n+1)*φ^2 / ω^2) / α^2 - k = sqrt(Complex{BigFloat}(k2)) - x = k * r - - x64 = ComplexF64(x) - - jl_n = sphericalbesselj(n, x64) - jl_np1 = sphericalbesselj(n+1, x64) - - ϕl = doublefactorial(2n+1) / x^n * jl_n - ϕlp1 = doublefactorial(2n+3) / x^(n+1) * jl_np1 - ψl = 2*(2n+3)/x^2 * (1 - ϕl) - pref = -r^(n+1) / (2n + 3) - - Ic[1,2] = pref * (0.5 * n * h * ψl + f * ϕlp1) - Ic[2,2] = pref * (0.5 * h * ψl - ϕlp1) - Ic[3,2] = -φ * r^n * f * ϕl - Ic[4,2] = 0.0 - Ic[5,2] = -r^(n+2) * ( - (α^2 * f)/r^2 - (3φ*f)/(2*(2n+3)) * ψl - ) - Ic[6,2] = -r^(n+1) * ( - (2n+1)*(α^2*f)/r^2 - - (3φ*((2n+1)*f - n*h))/(2*(2n+3)) * ψl - ) - - Ic[:,3] .= 0.0 - Ic[2,3] = 1.0 # tangential slip - elseif type == "solid" # incompressible solid core - # First column - Ic[1, 1] = n*r^( n+1 ) / ( 2*( 2n + 3) ) - Ic[2, 1] = ( n+3 )*r^( n+1 ) / ( 2*( 2n+3 ) * ( n+1 ) ) - Ic[3, 1] = ( n*ρ*g*r + 2*( n^2 - n - 3)*μ ) * r^n / ( 2*( 2n + 3) ) - Ic[4, 1] = n *( n+2 ) * μ * r^n / ( ( 2n + 3 )*( n+1 ) ) - Ic[6, 1] = 2π*G_norm*ρ*n*r^( n+1 ) / ( 2n + 3 ) - - # Second column - Ic[1, 2] = r^( n-1 ) - Ic[2, 2] = r^( n-1 ) / n - Ic[3, 2] = ( ρ*g*r + 2*( n-1 )*μ ) * r^( n-2 ) - Ic[4, 2] = 2*( n-1 ) * μ * r^( n-2 ) / n - Ic[6, 2] = 4π*G_norm*ρ*r^( n-1 ) - - # Third column - Ic[3, 3] = -ρ * r^n - Ic[5, 3] = -r^n - Ic[6, 3] = -( 2n + 1) * r^( n-1 ) - else - error("Invalid core type: $type. Must be 'liquid', 'inertial', or 'solid'.") - end - - return Ic - end - - - """ - get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing) - - Compute the 6x6 `A` matrix in the ODE for the solid-body problem. - - # Arguments - - `ω::prec` : Forcing frequency of the tidal forcing. - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. - - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. - - # Returns - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - """ - function get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing) - A = zeros(precc, 6, 6) - get_A!(A, ω, r, ρ, g, μ, K, n; G0=G0, λ=λ) - return A - end - - - """ - get_A!(A, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing) - - Compute the 6x6 `A` matrix in the ODE for the solid-body problem. These correspond to - the coefficients given in Equation S4.6 in Hay et al., (2025) when α=φ=0, as well as Sabadini and Vermeersen - (2016) Eq. 1.95. - - # Arguments - - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. - - `ω::prec` : Forcing frequency of the tidal forcing. - - `r::prec` : Radius at which to compute the A matrix. - - `ρ::prec` : Density at radius r. - - `g::prec` : Gravity at radius r. - - `μ::prec` : Shear modulus at radius r. - - `K::prec` : Bulk modulus at radius r. - - `n::Int` : Tidal degree. - - # Keyword Arguments - - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. - - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. - """ - function get_A!(A::Matrix, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing) - if isnothing(λ) - λ = K - 2μ/3 - end - - G_norm = G / G0 - - r_inv = 1.0/r - β_inv = 1.0/(2μ + λ) - rβ_inv = r_inv * β_inv - - A[1,1] = -2λ * r_inv*β_inv - A[2,1] = -r_inv - A[3,1] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) - ω^2 * ρ - A[4,1] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[5,1] = 4π * G_norm * ρ - A[6,1] = 4π*(n+1)*G_norm*ρ*r_inv - - A[1,2] = n*(n+1) * λ * r_inv*β_inv - A[2,2] = r_inv - A[3,2] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) - A[4,2] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) - ω^2 * ρ - A[6,2] = -4π*n*(n+1)*G_norm*ρ*r_inv - - A[1,3] = β_inv - A[3,3] = r_inv*β_inv * (-4μ ) - A[4,3] = -λ * r_inv*β_inv - - A[2,4] = 1.0 / μ - A[3,4] = n*(n+1)*r_inv - A[4,4] = -3r_inv - - A[3,5] = ρ * (n+1)*r_inv - A[4,5] = -ρ*r_inv - A[5,5] = -(n+1)r_inv - - A[3,6] = -ρ - A[5,6] = 1.0 - A[6,6] = (n-1)r_inv - end - - """ solve_radial_system(r, ρ, g, μ, K, ω, n, R_planet, ρ_core; core="liquid") @@ -756,10 +514,10 @@ module solid1d_relax # Arguments - `ω::Float64` : Forcing frequency. - `r::prec` : Radial position of the core-mantle boundary. - - `ρ::prec` : Density at the core-mantle boundary. + - `ρ::prec` : Average core density. - `g::prec` : Gravity at the core-mantle boundary. - - `μ::precc` : Complex shear modulus at the core-mantle boundary. - - `K::prec` : Bulk modulus at the core-mantle boundary. + - `μ::precc` : Average core complex shear modulus. + - `K::prec` : Average core bulk modulus. - `type::String` : Type of core boundary condition to apply. Options are "liquid" for a fluid core, "solid" for a solid core, and "inertial" for a core with inertial response. - `n::Int` : Tidal degree. @@ -771,9 +529,9 @@ module solid1d_relax # Returns - `B::Array{precc,2}` : 3x6 matrix representing the linear relationship between the state variables at the core and the boundary conditions. """ - function get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1, M=6, N=3) + function get_core_bc!(ω, r, ρ, g, μ, K, type, n; G0=1) - Ic = get_Ic(ω, r, ρ, g, μ, K, type, n; G0=G0, M=M, N=N) + Ic = get_Ic(ω, r, ρ, g, μ, K, type, n; G0=G0) # Define indices based on Takeuchi & Saito (1972) # u vectors (Displacements/Potential): U=1, V=2, phi=5 @@ -789,7 +547,7 @@ module solid1d_relax # Construct the 3x6 B matrix T = eltype(b) - B = zeros(T, 3, M) + B = zeros(T, 3, 6) for i in 1:3 B[i, idx_u[i]] = 1.0 # Identity for u components @@ -907,11 +665,6 @@ module solid1d_relax y_t[:, i] = R[i] * y_t[:, i+1] end - # # apply scaling to get dimensional solution - # for i in 1:Nr - # y_t[:, i] = S * y_t[:, i] - # end - return y_t, y_l end From 85f890add85c542923f3a8b52a65c5770974cad9 Mon Sep 17 00:00:00 2001 From: Marijn Date: Sat, 2 May 2026 20:55:22 +0000 Subject: [PATCH 17/36] Updated Obliqua with fixes for solid1d_mush_relax. --- Project.toml | 2 -- src/Obliqua.jl | 62 ++++++++++++++------------------------------------ 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/Project.toml b/Project.toml index 6fa432e..63f9b04 100644 --- a/Project.toml +++ b/Project.toml @@ -16,7 +16,6 @@ LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" NCDatasets = "85f8d34a-cbdd-5861-8df4-14fed0d494ab" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" -PythonPlot = "274fc56d-3b97-40fa-a1cd-1b4a50311bf9" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" @@ -24,7 +23,6 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [compat] -DelimitedFiles = "1.9.1" FFTW = "1.10.0" GenericLinearAlgebra = "0" Interpolations = "0.16.2" diff --git a/src/Obliqua.jl b/src/Obliqua.jl index ead0960..d8c79c5 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -764,15 +764,25 @@ module Obliqua U2 = abs2.(U) + # return bulk heating at each frequency + P_T_1_blk = prefactor .* imag_k2 .* U2 + # return power profile at each frequency P_T_1_prf = zeros(prec, N_σ, length(shear)) for iss in 1:N_σ unorm_prf = radial_profile_at_sigma(Float64(σ_range[iss]), prf_itp_shells) P_T_1_prf[iss, :] = unorm_prf .* U2 - end - # return bulk heating at each frequency - P_T_1_blk = prefactor .* imag_k2 .* U2 + # Integrate the radial profile over the volume for this frequency + # Assuming 'dv' is the differential volume element array + integrated_profile_power = sum(dv .* P_T_1_prf[iss, :]) + + # Calculate ratio + ratio = P_T_1_blk[iss] / integrated_profile_power + + # Print formatted results + @debug("Freq Index: %d | Ratio: %.6f\n", iss, ratio) + end @info "Mapping 1 --> $(σ_range[1]) /s, and $N_σ --> $(σ_range[end]) /s." @@ -1187,7 +1197,7 @@ module Obliqua solid1d.define_spherical_grid(res, n, m) # get y-functions - M, y1_4 = solid1d.compute_M(rr, ρ, g, μc, κ, n, ρ_core; core=core) + M, y1_4 = solid1d.compute_M(omega, rr, ρ, g, μc, κ, n, ρ_core; core=core) # Tidal tidal_solution_T = solid1d.compute_y(rr, g, M, y1_4, n; load=false) # Load @@ -1404,7 +1414,7 @@ module Obliqua solid1d_mush.define_spherical_grid(res, n, m) # get y-functions - M, y1_4 = solid1d_mush.compute_M(rr, ρs, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, ρ_core; core=core) + M, y1_4 = solid1d_mush.compute_M(omega, rr, ρs, g, μc, κs, ρl, κl, κd, α, ηl, ϕ, k, n, ρ_core; core=core) # Tidal tidal_solution_T = solid1d_mush.compute_y(rr, g, M, y1_4, n; load=false) # Load @@ -1504,45 +1514,13 @@ module Obliqua k = zeros(prec, length(r)) # find where the mush interface occurs (excluding the CMB layer) - # 1. Get all indices above the threshold + # get all indices above the threshold ii_all = findall(ϕ .> porosity_thresh) - # 2. Find the start of the continuous region that reaches the surface - # We look for the first index such that all subsequent indices are also > threshold - if isempty(ii_all) - ii = nothing # No mushy layer found - else - # Logic: If it's a single continuous layer at the top, - # the start index will be the first one in a contiguous sequence ending at Nr. - if ii_all[end] == length(ϕ) - # Work backwards from the end to find the first break in continuity - continuous_from_top = findall(diff(ii_all) .!= 1) - if isempty(continuous_from_top) - ii = ii_all[1] # The whole thing is mushy - else - # The start is the index right after the last "break" in the sequence - ii = ii_all[continuous_from_top[end] + 1] - end - else - ii = nothing # Porosity > threshold, but it doesn't reach the surface - end - end - - # If the porosity = 0, throw error (because the matrix cannot be resolved, instead use 1 phase model) - if ϕ[ii] <= prec(porosity_thresh) - throw("No mush region identified in viscosity profile.") - end - - # Get top layer where porosity exceeds mush threshold - ii_top = maximum(findall(ϕ .< 0.60)) - # update the liquid arrays κl .= prec(bulk_l) # liquid bulk modulus ηl .= prec(visc_l) # liquid viscosity - k[ii:ii_top] .= prec(permea) # permeability - - # set porosity to zero outside mush region (otherwise code cannot solve system) - ϕ[1:ii-1] .= 0.0 # zero below ii + k[ii_all] .= prec(permea) # permeability # resample profiles onto new grid r_grid, ρ, η, μc, κs, κl, κd, α, ηl, ϕ, k, g, M_tot = solid1d_mush_relax.resample_profiles(r, ρ, η, μc, κs, κl, κd, α, ηl, ϕ, k, m_core, dr_min, dr_max) @@ -1559,12 +1537,6 @@ module Obliqua # solve y functions across grid y_t, y_l = solid1d_mush_relax.compute_y(r_centers, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, ρ_core, M_tot; core=core) - # for debugging: plot y-function relaxation solution - # in particular, observe the oscillating behavior near transition zones - # and also near the surface for high frequencies - # plotting.plot_relaxation_solution(y_t, r_centers, - # filename="$OUT_DIR/relaxation_solution.png") - # Love numbers k2_T = (y_t[3, end] - 1) .* (maximum(r) ./ R) k2_L = (y_l[3, 1] - 1) .* (maximum(r) ./ R) From b96358180cb2ff5b7439864a3724f07444a87288 Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 3 May 2026 22:15:07 +0000 Subject: [PATCH 18/36] Added heat distribution map. Moved duplicate functions to common.jl --- src/Obliqua.jl | 144 ++++-- src/common.jl | 1033 +++++++++++++++++++++++++++++++++++++ src/plotting.jl | 35 +- src/solid1d.jl | 11 +- src/solid1d_mush.jl | 9 +- src/solid1d_mush_relax.jl | 548 +++++++------------- src/solid1d_relax.jl | 254 +-------- 7 files changed, 1383 insertions(+), 651 deletions(-) create mode 100644 src/common.jl diff --git a/src/Obliqua.jl b/src/Obliqua.jl index d8c79c5..da681e5 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -63,7 +63,7 @@ module Obliqua const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 const M_Earth::prec = prec(5.9724e24) # kg - const res::Float64 = 20.0 # angular resolution in degrees + const res::Float64 = 10.0 # angular resolution in degrees """ @@ -377,6 +377,8 @@ module Obliqua end # get core mass from core density and radius + μ_core = convert(prec, cfg["struct"]["core_shear"]) + κ_core = convert(prec, cfg["struct"]["core_bulk"]) ρ_core = convert(prec, cfg["struct"]["core_density"]) m_core = (4/3)*π*r[1]^3*ρ_core @@ -438,6 +440,7 @@ module Obliqua # initiate forcing frequency dependent heating profile prf_total = zeros(prec, N_σ, N_layers) + map_total = zeros(prec, N_σ, length(segments), length(collect(0:res:180)), length(collect(0:res:360-0.001))) # core density for bottom boundary ρ_mean_lower = ρ_core @@ -477,6 +480,7 @@ module Obliqua # preallocate heating profile for segment prf_seg = zeros(prec, N_σ, length(r_seg)-1) + map_seg = zeros(prec, N_σ, length(collect(0:res:180)), length(collect(0:res:360-0.001))) # mean density in current segment if length(ρ_seg) == 1 @@ -515,30 +519,23 @@ module Obliqua ) # elseif 1D interior and heating profile from strain tensor elseif module_solid=="solid1d" - # calculate tides in solid region prf_seg[iss,:], kT, kL = run_solid1d( σ, ρ_seg, r_seg, η_seg, μc_seg[:, iss], - κ_seg, R, m_core, ρ_core; - ncalc=ncalc, n=n, m=m - ) - elseif module_solid=="solid1d-qr" - # calculate tides in solid region - prf_seg[iss,:], kT, kL = run_solid1d_qr( - σ, ρ_seg, - r_seg, η_seg, - μc_seg[:, iss], - κ_seg, R, m_core, ρ_core; + κ_seg, R, + m_core, ρ_core, + μ_core, κ_core; ncalc=ncalc, n=n, m=m ) # elseif 1D interior and heating profile from strain tensor elseif module_solid=="solid1d-relax" - # calculate tides in solid region - prf_seg[iss,:], kT, kL = run_solid1d_relax( + prf_seg[iss,:], map_seg[iss,:, :], kT, kL = run_solid1d_relax( σ, ρ_seg, r_seg, η_seg, μc_seg[:, iss], - κ_seg, R, m_core, ρ_core; + κ_seg, R, + m_core, ρ_core, + μ_core, κ_core; dr_min=dr_min, dr_max=dr_max, n=n, m=m, core=core ) @@ -547,15 +544,19 @@ module Obliqua prf_seg[iss,:], kT, kL = run_solid1d_mush( σ, ρ_seg, r_seg, η_seg, μc_seg[:, iss], - κ_seg, ϕ_seg, R, m_core, ρ_core; + κ_seg, ϕ_seg, R, + m_core, ρ_core, + μ_core, κ_core; ncalc=ncalc, n=n, m=m, core=core, visc_l=visc_l, bulk_l=bulk_l, permea=permea, porosity_thresh=porosity_thresh ) elseif module_solid=="solid1d-mush-relax" - prf_seg[iss,:], kT, kL = run_solid1d_mush_relax( + prf_seg[iss,:], map_seg[iss,:, :], kT, kL = run_solid1d_mush_relax( σ, ρ_seg, r_seg, η_seg, μc_seg[:, iss], - κ_seg, ϕ_seg, R, m_core, ρ_core; + κ_seg, ϕ_seg, R, + m_core, ρ_core, + μ_core, κ_core; dr_min=dr_min, dr_max=dr_max, n=n, m=m, core=core, visc_l=visc_l, bulk_l=bulk_l, permea=permea, porosity_thresh=porosity_thresh @@ -671,6 +672,7 @@ module Obliqua # append segment heating profile to global heating profile prf_total[:, i_start:i_end] .= prf_seg[:, :] + map_total[:, iseg, :, :] .= map_seg[:, :, :] # turn off interpolator after completion interp_previous = false @@ -781,7 +783,7 @@ module Obliqua ratio = P_T_1_blk[iss] / integrated_profile_power # Print formatted results - @debug("Freq Index: %d | Ratio: %.6f\n", iss, ratio) + @info("Freq Index: %d | Ratio: %.6f\n", iss, ratio) end @info "Mapping 1 --> $(σ_range[1]) /s, and $N_σ --> $(σ_range[end]) /s." @@ -827,6 +829,7 @@ module Obliqua # initialize frequency dependent heating profile P_T_s_prf = zeros(prec, length(s_range), length(shear)) + P_T_s_map = zeros(prec, length(s_range), length(collect(0:res:180)), length(collect(0:res:360-0.001))) # loop over tidal modes for (iss, ss) in pairs(s_range) @@ -856,9 +859,11 @@ module Obliqua # get global heating profile at forcing frequency unorm_prf = prf_total[iss, :] + unorm_map = sum(map_total[iss, :, :, :], dims=1) # sum over layers to get surface map # Hansen renorm P_T_s_prf[iss, :] = unorm_prf .* U2 + P_T_s_map[iss, :, :] = unorm_map .* U2 # For debugging purposes log the ratio between expected and radially integrated heating ratio = P_T_s_blk[iss] / sum(dv .* P_T_s_prf[iss, :]) @@ -879,6 +884,15 @@ module Obliqua # get radial heating profile W/m^3 P_T_prf = [sum(P_T_s_prf[:,j]) for j in 1:size(P_T_s_prf,2)] + P_T_map = sum(P_T_s_map, dims=1) # sum over frequencies to get total surface map + + # plot map of surface heating + plt = plotting.plot_surface_heating( + P_T_map[1, :, :], + res; + filename="$OUT_DIR/tidal_heating_map_surface.png", + title_str="Surface heating map" + ) # determine the total heat input from heating profile P_T_prf_blk = sum(dv .* P_T_prf) @@ -1137,7 +1151,7 @@ module Obliqua """ - run_solid1d(omega, rho, radius, visc, shear, bulk, R, m_core, ρ_core; ncalc=2000, n=2, m=2, core="liquid") + run_solid1d(omega, rho, radius, visc, shear, bulk, R, m_core, ρ_core, μ_core, κ_core; ncalc=2000, n=2, m=2, core="liquid") Use 1D solid tides model to calculate k2 Lovenumbers, and compute 1D heating profile from strain tensor. This method ignores inertia effects, since they break the numerical stability. @@ -1152,6 +1166,8 @@ module Obliqua - `R::prec` : Planet radius. - `m_core::prec` : Core mass. - `ρ_core::prec` : Core density. + - `μ_core::prec` : Core shear modulus. + - `κ_core::prec` : Core bulk modulus. # Keyword Arguments - `ncalc::Int=2000` : Number of sublayers. @@ -1172,7 +1188,9 @@ module Obliqua bulk::Array{prec,1}, R::prec, m_core::prec, - ρ_core::prec; + ρ_core::prec, + μ_core::prec, + κ_core::prec; ncalc::Int=2000, n::Int=2, m::Int=2, @@ -1197,7 +1215,7 @@ module Obliqua solid1d.define_spherical_grid(res, n, m) # get y-functions - M, y1_4 = solid1d.compute_M(omega, rr, ρ, g, μc, κ, n, ρ_core; core=core) + M, y1_4 = solid1d.compute_M(omega, rr, ρ, g, μc, κ, n, ρ_core, μ_core, κ_core; core=core) # Tidal tidal_solution_T = solid1d.compute_y(rr, g, M, y1_4, n; load=false) # Load @@ -1208,7 +1226,7 @@ module Obliqua k2_L = (tidal_solution_L[5, end, end] - 1) .* (maximum(r) ./ R) # Get profile power output (W m-3), converted to W/kg - (Eμ, Eκ) = solid1d.get_heating_profile(tidal_solution_T, rr, ρ, g, μc, κ, n, omega; lay=nothing) + Eμ, Eκ = solid1d.get_heating_profile(tidal_solution_T, rr, ρ, g, μc, κ, n, omega; lay=nothing) Eμ_tot, _ = Eμ # shear (W), (W/m3) Eκ_tot, _ = Eκ # compaction (W), (W/m3) @@ -1221,7 +1239,7 @@ module Obliqua """ - run_solid1d_relax(omega, rho, radius, visc, shear, bulk, R, m_core, ρ_core; dr_min=300, dr_max=3000, n=2, m=2, core="liquid") + run_solid1d_relax(omega, rho, radius, visc, shear, bulk, R, m_core, ρ_core, μ_core, κ_core; dr_min=300, dr_max=3000, n=2, m=2, core="liquid") Use 1D solid tides model with relaxation method to calculate k2 Lovenumbers, and compute 1D heating profile from strain tensor. This method includes inertia effects, but is more computationally expensive. @@ -1236,6 +1254,8 @@ module Obliqua - `R::prec` : Planet radius. - `m_core::prec` : Core mass. - `ρ_core::prec` : Core density. + - `μ_core::prec` : Core shear modulus. + - `κ_core::prec` : Core bulk modulus. # Keyword Arguments - `dr_min::Int=300` : Minimum layer thickness in m. @@ -1246,6 +1266,7 @@ module Obliqua # Returns - `power_prf::Array{prec,1}` : Heating profile. + - `power_map::Array{prec,4}` : Heating map (frequency, colatitude, longitude). - `k2_T::precc` : Complex Tidal k2 Lovenumber. - `k2_L::precc` : Complex Load k2 Lovenumber. """ @@ -1257,13 +1278,15 @@ module Obliqua bulk::Array{prec,1}, R::prec, m_core::prec, - ρ_core::prec; + ρ_core::prec, + μ_core::prec, + κ_core::prec; dr_min::Int=300, dr_max::Int=3000, n::Int=2, m::Int=2, core::String="liquid" - )::Tuple{Array{prec,1},precc,precc} + )::Tuple{Array{prec,1},Matrix{prec},precc,precc} # convert inputs ρ = convert(Vector{prec}, rho) @@ -1279,10 +1302,10 @@ module Obliqua r_centers = 0.5 .* (r_grid[1:end-1] .+ r_grid[2:end]) # define angular grid - solid1d_relax.define_spherical_grid(res, n, m) + SphericalGrid = solid1d_relax.define_spherical_grid(res, n, m) # solve y functions across grid - y_t, y_l = solid1d_relax.compute_y(r_centers, ρ, g, μ, κ, omega, n, ρ_core, M_tot; core=core) + y_t, y_l = solid1d_relax.compute_y(r_centers, ρ, g, μ, κ, omega, n, ρ_core, μ_core, κ_core, M_tot; core=core) # for debugging: plot y-function relaxation solution # in particular, observe the oscillating behavior near transition zones @@ -1295,11 +1318,16 @@ module Obliqua k2_L = (y_l[5, 1] - 1) .* (maximum(r) ./ R) # heating profile - (Eμ_tot, Eκ_tot) = solid1d_relax.get_heating_profile( - y_t, r_grid, ρ, g, μ, κ, n, omega + Eμ_tot, Eκ_tot = solid1d_relax.get_heating_profile( + y_t, r_grid, ρ, g, μ, κ, n, omega, SphericalGrid + ) + + Eμ_map, Eκ_map = solid1d_mush_relax.get_heating_map( + y_t, r_grid, ρ, g, μ, κ, n, omega, SphericalGrid ) - power_prf = abs.(Eμ_tot .+ Eκ_tot) + power_prf = abs.(Eμ_tot .+ Eκ_tot) + power_map = abs.(Eμ_map .+ Eκ_map) # interpolate from grid back to original radius points itp = linear_interpolation(r_centers, power_prf, extrapolation_bc=Line()) @@ -1309,12 +1337,12 @@ module Obliqua power_prf = itp.(r_orig_centers) - return power_prf, k2_T, k2_L + return power_prf, power_map, k2_T, k2_L end """ - run_solid1d_mush(omega, rho, radius, visc, shear, bulk, phi, R; ncalc=2000, n=2, m=2, visc_l=1e2, bulk_l=1e9, permea=1e-7, porosity_thresh=1e-5) + run_solid1d_mush(omega, rho, radius, visc, shear, bulk, phi, R, ρ_core, μ_core, κ_core; ncalc=2000, n=2, m=2, visc_l=1e2, bulk_l=1e9, permea=1e-7, porosity_thresh=1e-5) Use 1D solid tides model with mush interface to calculate k2 Lovenumbers, and compute 1D heating profile from strain tensor. @@ -1329,6 +1357,8 @@ module Obliqua - `R::prec` : Planet radius. - `m_core::prec` : Core mass. - `ρ_core::prec` : Core density. + - `μ_core::prec` : Core shear modulus. + - `κ_core::prec` : Core bulk modulus. # Keyword Arguments - `ncalc::Int=2000` : Number of sublayers. @@ -1354,7 +1384,9 @@ module Obliqua phi::Array{prec,1}, R::prec, m_core::prec, - ρ_core::prec; + ρ_core::prec, + μ_core::prec, + κ_core::prec; ncalc::Int=2000, n::Int=2, m::Int=2, @@ -1414,7 +1446,7 @@ module Obliqua solid1d_mush.define_spherical_grid(res, n, m) # get y-functions - M, y1_4 = solid1d_mush.compute_M(omega, rr, ρs, g, μc, κs, ρl, κl, κd, α, ηl, ϕ, k, n, ρ_core; core=core) + M, y1_4 = solid1d_mush.compute_M(omega, rr, ρs, g, μc, κs, ρl, κl, κd, α, ηl, ϕ, k, n, ρ_core, μ_core, κ_core; core=core) # Tidal tidal_solution_T = solid1d_mush.compute_y(rr, g, M, y1_4, n; load=false) # Load @@ -1425,7 +1457,7 @@ module Obliqua k2_L = (tidal_solution_L[5, end, end] - 1) .* (maximum(r) ./ R) # Get profile power output (W m-3), converted to W/kg - (Eμ, Eκ, El) = solid1d_mush.get_heating_profile(tidal_solution_T, + Eμ, Eκ, El = solid1d_mush.get_heating_profile(tidal_solution_T, rr, ρs, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n) @@ -1442,7 +1474,7 @@ module Obliqua """ - run_solid1d_mush_relax(omega, rho, radius, visc, shear, bulk, R, m_core, ρ_core; dr_min=300, dr_max=3000, n=2, m=2, core="liquid") + run_solid1d_mush_relax(omega, rho, radius, visc, shear, bulk, R, m_core, ρ_core, μ_core, κ_core; dr_min=300, dr_max=3000, n=2, m=2, core="liquid") Use 1D solid tides model with relaxation method to calculate k2 Lovenumbers, and compute 1D heating profile from strain tensor. This method includes inertia effects, but is more computationally expensive. @@ -1458,6 +1490,8 @@ module Obliqua - `R::prec` : Planet radius. - `m_core::prec` : Core mass. - `ρ_core::prec` : Core density. + - `μ_core::prec` : Core shear modulus. + - `κ_core::prec` : Core bulk modulus. # Keyword Arguments - `dr_min::Int=300` : Minimum layer thickness in m. @@ -1472,6 +1506,7 @@ module Obliqua # Returns - `power_prf::Array{prec,1}` : Heating profile. + - `power_map::Array{prec,4}` : Heating map (frequency, colatitude, longitude). - `k2_T::precc` : Complex Tidal k2 Lovenumber. - `k2_L::precc` : Complex Load k2 Lovenumber. """ @@ -1484,7 +1519,9 @@ module Obliqua phi::Array{prec,1}, R::prec, m_core::prec, - ρ_core::prec; + ρ_core::prec, + μ_core::prec, + κ_core::prec; dr_min::Int=300, dr_max::Int=3000, n::Int=2, @@ -1494,7 +1531,7 @@ module Obliqua bulk_l::Float64=1e9, permea::Float64=1e-7, porosity_thresh::Float64=1e-5 - )::Tuple{Array{prec,1},precc,precc} + )::Tuple{Array{prec,1},Matrix{prec},precc,precc} # internal structure arrays. # first element is the innermost layer, last element is the outermost layer @@ -1513,10 +1550,13 @@ module Obliqua ηl = zeros(prec, length(r)) k = zeros(prec, length(r)) + # add small non-zero porosity to avoid numerical issues with zero porosity layers + ϕ[ϕ .< prec(porosity_thresh)] .= prec(10*porosity_thresh) + # find where the mush interface occurs (excluding the CMB layer) # get all indices above the threshold ii_all = findall(ϕ .> porosity_thresh) - + # update the liquid arrays κl .= prec(bulk_l) # liquid bulk modulus ηl .= prec(visc_l) # liquid viscosity @@ -1532,21 +1572,29 @@ module Obliqua r_centers = 0.5 .* (r_grid[1:end-1] .+ r_grid[2:end]) # define angular grid - solid1d_mush_relax.define_spherical_grid(res, n, m) + SphericalGrid = solid1d_mush_relax.define_spherical_grid(res, n, m) # solve y functions across grid - y_t, y_l = solid1d_mush_relax.compute_y(r_centers, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, ρ_core, M_tot; core=core) + y_t, y_l = solid1d_mush_relax.compute_y(r_centers, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, ρ_core, μ_core, κ_core, M_tot; core=core) + + # plotting.plot_relaxation_solution(y_t, r_centers, + # filename="$OUT_DIR/relaxation_solution.png") # Love numbers - k2_T = (y_t[3, end] - 1) .* (maximum(r) ./ R) - k2_L = (y_l[3, 1] - 1) .* (maximum(r) ./ R) + k2_T = (y_t[5, end] - 1) .* (maximum(r) ./ R) + k2_L = (y_l[5, 1] - 1) .* (maximum(r) ./ R) # heating profile - (Eμ_tot, Eκ_tot) = solid1d_mush_relax.get_heating_profile( - y_t, r_grid, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n + Eμ_tot, Eκ_tot, El_tot = solid1d_mush_relax.get_heating_profile( + y_t, r_grid, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, SphericalGrid ) - power_prf = abs.(Eμ_tot .+ Eκ_tot) + Eμ_map, Eκ_map, El_map = solid1d_mush_relax.get_heating_map( + y_t, r_grid, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, SphericalGrid + ) + + power_prf = abs.(Eμ_tot .+ Eκ_tot .+ El_tot) + power_map = abs.(Eμ_map .+ Eκ_map .+ El_map) # interpolate from grid back to original radius points itp = linear_interpolation(r_centers, power_prf, extrapolation_bc=Line()) @@ -1556,7 +1604,7 @@ module Obliqua power_prf = itp.(r_orig_centers) - return power_prf, k2_T, k2_L + return power_prf, power_map, k2_T, k2_L end diff --git a/src/common.jl b/src/common.jl new file mode 100644 index 0000000..a02d679 --- /dev/null +++ b/src/common.jl @@ -0,0 +1,1033 @@ + + + +module common + + import GenericLinearAlgebra + using DoubleFloats + using AssociatedLegendrePolynomials + using StaticArrays + using SpecialFunctions + using SparseArrays + + export define_spherical_grid, get_scales, get_Ic, get_A, get_A!, get_heating_profile, get_heating_map + + prec = BigFloat + precc = Complex{BigFloat} + + const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 + + + """ + Ynm(n, m, theta, phi) + + Compute the spherical harmonic Ynm for given n, m, theta, and phi. + + # Arguments + - `n::Int` : Tidal degree. + - `m::Int` : Tidal order. + - `theta::Array{Float64,1}` : Array of colatitudes in radians. + - `phi::Array{Float64,1}` : Array of longitudes in radians. + + # Returns + - `Ynm::Array{ComplexF64,2}` : 2D array of spherical harmonic values for each combination of theta and phi. + """ + function Ynm(n, m, theta, phi) + return Plm.(n, m, cos.(theta)) .* exp.(1im * m .* phi) + end + + + """ + define_spherical_grid(res, n, m) + + Create the spherical grid of angular resolution `res` in degrees. This is used for + numerical integrations over solid angle. A new grid can easily be defined by + recalling the function with a new `res`. + + # Arguments + - `res::Float64` : Desired angular resolution in degrees. + - `n::Int` : Tidal degree. + - `m::Int` : Tidal order. + + # Returns + - `SphericalGrid` : A tuple containing the spherical grid information. + """ + function define_spherical_grid(res, n, m) + # θ and φ grids + lons = deg2rad.(collect(0:res:360-0.001))' + clats = deg2rad.(collect(0:res:180)) + clats[1] += 1e-6 + clats[end] -= 1e-6 + + # allocate arrays + Y = zeros(ComplexF64, 1, length(clats), length(lons)) + dYdθ = similar(Y) + dYdϕ = similar(Y) + Z = similar(Y) + X = similar(Y) + + sinθ = sin.(clats) + cosθ = cos.(clats) + cotθ = cosθ ./ sinθ + cscθ = csc.(clats) + + # Normalization factor for spherical harmonics + norm = sqrt((2*n+1) * factorial(n-m) / (4π * factorial(n+m))) + + i = 1 + + # Y + Y[i,:,:] = Ynm(n,m,clats,lons) + + # ∂Y/∂θ + Pn = Plm.(n, m, cosθ) + if n > m + Pn_1 = Plm.(n-1, m, cosθ) + dPdθ = (n .* cosθ .* Pn .- (n + m) .* Pn_1) ./ (sinθ) + else + # m == n -> P_{n-1}^m = 0 + dPdθ = (n .* cosθ .* Pn) ./ (sinθ) + end + dYdθ[i,:,:] .= dPdθ .* exp.(1im .* m .* lons) + + # ∂Y/∂ϕ + dYdϕ[i,:,:] .= 1im * m .* Y[i,:,:] + + # Z = 2 ((1/sinθ) ∂²Y/∂θ∂ϕ - cotθ cscθ ∂Y/∂ϕ) + Z[i,:,:] .= 2 .* (1im * m ./ sinθ .* dYdθ[i,:,:] .- cotθ .* cscθ .* dYdϕ[i,:,:]) + + # X = -2 (cotθ ∂Y/∂θ + csc²θ ∂²Y/∂ϕ²) - n(n+1)) Y + X[i,:,:] .= -2 .* (cotθ .* dYdθ[i,:,:] .- cscθ.^2 .* m^2 .* Y[i,:,:]) .- n*(n+1) .* Y[i,:,:] + + # Normalize + Y[i,:,:] .*= norm + dYdθ[i,:,:] .*= norm + dYdϕ[i,:,:] .*= norm + Z[i,:,:] .*= norm + X[i,:,:] .*= norm + + SphericalGrid = ( + res = res, + clats = clats, + lons = lons, + Y = Y, + dYdθ = dYdθ, + dYdϕ = dYdϕ, + Z = Z, + X = X + ) + + return SphericalGrid + end + + + """ + get_scales(R0, M0, g0) + + Compute the characteristic scales for the problem based on a reference radius `R0`, mass `M0`, and gravity scale + `g0`. These scales are used to non-dimensionalize the equations and ensure numerical stability. + + # Arguments + - `R0::prec` : Reference radius scale (e.g., planetary radius). + - `M0::prec` : Reference mass scale (e.g., planetary mass). + - `g0::prec` : Reference gravity scale. + + # Returns + Tuple of characteristic scales: + - `R0::prec` : Length scale (m). + - `M0::prec` : Mass scale (kg). + - `s0::prec` : Time scale (s). + - `ρ0::prec` : Density scale (kg/m^3). + - `G0::prec` : Gravitational constant scale (m^3 kg^-1 s^-2). + - `g0::prec` : Gravity scale (m/s^2). + - `μ0::prec` : Shear modulus scale (Pa). + - `S::Diagonal{prec}` : Diagonal scaling matrix for state variables. + - `Sinv::Diagonal{prec}` : Inverse of the scaling matrix S. + """ + function get_scales(R0, M0, g0; Y=[1,2,3,4,5,6,7,8]) + + ρ0 = M0 / R0^3 + P0 = g0 * R0 + μ0 = ρ0 * g0 * R0 + + s0 = sqrt(g0 / R0) + G0 = R0^3 / (M0 * s0^2) + + S = zeros(prec, length(Y), length(Y)) + S[Y[1], Y[1]] = R0 # y1: radial displacement (m) + S[Y[2], Y[2]] = R0 # y2: tangential displacement (m) + S[Y[3], Y[3]] = μ0 # y3: radial stress (Pa) + S[Y[4], Y[4]] = μ0 # y4: tangential stress (Pa) + S[Y[5], Y[5]] = P0 # y5: potential (m^2/s^2) + S[Y[6], Y[6]] = g0 # y6: potential gradient/gravity (m/s^2) + S[Y[7], Y[7]] = μ0 # y7: pore pressure (Pa) + S[Y[8], Y[8]] = R0 # y8: relative radial displacement (m) + + Sinv = inv(S) + return R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv + end + + + """ + doublefactorial(n) + + Compute the double factorial of an integer n, defined as n!! = n * (n-2) * (n-4) * ... until 1 or 0. + + # Arguments + - `n::Integer` : The integer for which to compute the double factorial. Must be non-negative. + + # Returns + - `result::Integer` : The double factorial of n. + """ + function doublefactorial(n::Integer) + n < 0 && error("doublefactorial not defined for negative n") + n == 0 && return one(n) + n == 1 && return one(n) + + result = one(n) + for k in n:-2:1 + result *= k + end + return result + end + + + """ + get_Ic(ω, r, ρ, g, μ, K, type, n; G0=1, Y=[1,2,3,4,5,6]) + + Get the core solution vector. This function computes the initial solution vectors at the core-mantle boundary + to serve as starting conditions for numerical integration through a planetary interior. It supports three + distinct physical regimes: a solid (incompressible elastic) core, a liquid (quasi-static inviscid) core, and + an inertial (dynamic compressible fluid) core. + + Use Solid for a rigid inner core, Liquid for a quick/stable calculation of a fluid outer core at low tidal + frequencies, and Inertial if you are looking for dynamic resonances or high-frequency tidal interactions where + the sound speed and fluid inertia matter. + + https://academic.oup.com/gji/article/203/3/2150/2594863 + + Note: Ordering in Y corresponds to the mapping of the ith element, hence the y-functions order + + Y = [1,2,5,3,4,6] + + would correspond to + + Y = [1,2,4,5,3,6] # <== + + and similarly for + + Y = [1,2,5,7,3,4,6,8] + + one should pass + + Y = [1,2,5,6,3,7,4,8] # <== + + # Arguments + - `ω::prec` : Angular frequency. + - `r::prec` : Radius of the core boundary. + - `ρ::prec` : Density of the core. + - `g::prec` : Gravity at the core boundary. + - `μ::prec` : Shear modulus of the core. + - `K::prec` : Bulk modulus of the core. + - `type::String` : Type of core, either "liquid", "inertial", or "solid". + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `Y::Vector{Int}=[1,2,3,4,5,6]` : Ordering of the solution vector components. This allows for different conventions in the literature. + + # Returns + - `Ic::Array{precc,2}` : MxN array of linearly independent solutions at the core boundary. These are used as starting vectors for the numerical integration across the interior. + """ + function get_Ic(ω, r, ρ, g, μ, K, type, n; G0=1, Y=[1,2,3,4,5,6]) + + # determine the size of the solution vector based on the type of core + M = length(Y) + N = Int(M / 2) + + Ic = zeros(precc, M, N) + + G_norm = G / G0 + + if type=="liquid" + Ic[Y[1],1] = -r^n / g + Ic[Y[1],3] = 1.0 + Ic[Y[2],2] = 1.0 + Ic[Y[3],3] = g*ρ + Ic[Y[5],1] = r^n + Ic[Y[6],1] = 2(n-1)*r^(n-1) + Ic[Y[6],3] = 4π * G_norm * ρ + elseif type == "inertial" + @warn "Inertial core boundary conditions have not been fully implemented. Use with caution." + + φ = 4π * G_norm * ρ / 3 + α = sqrt(K / ρ) + f = -ω^2 / φ + h = f - (n + 1) + k2 = (ω^2 + 4φ - n*(n+1)*φ^2 / ω^2) / α^2 + k = sqrt(Complex{BigFloat}(k2)) + x = k * r + + x64 = ComplexF64(x) + + jl_n = sphericalbesselj(n, x64) + jl_np1 = sphericalbesselj(n+1, x64) + + ϕl = doublefactorial(2n+1) / x^n * jl_n + ϕlp1 = doublefactorial(2n+3) / x^(n+1) * jl_np1 + ψl = 2*(2n+3)/x^2 * (1 - ϕl) + pref = -r^(n+1) / (2n + 3) + + Ic[Y[1],1] = n * r^(n-1) + Ic[Y[2],1] = r^(n-1) + Ic[Y[3],1] = 0.0 + Ic[Y[4],1] = 0.0 + Ic[Y[5],1] = -(n*φ - ω^2) * r^n + Ic[Y[6],1] = -(2*(n-1)*n*φ - (2*n + 1)*ω^2) * r^(n-1) + + Ic[Y[1],2] = pref * (0.5 * n * h * ψl + f * ϕlp1) + Ic[Y[2],2] = pref * (0.5 * h * ψl - ϕlp1) + Ic[Y[3],2] = -φ * r^n * f * ϕl + Ic[Y[4],2] = 0.0 + Ic[Y[5],2] = -r^(n+2) * ( + (α^2 * f)/r^2 - (3φ*f)/(2*(2n+3)) * ψl + ) + Ic[Y[6],2] = -r^(n+1) * ( + (2n+1)*(α^2*f)/r^2 - + (3φ*((2n+1)*f - n*h))/(2*(2n+3)) * ψl + ) + + Ic[:,3] .= 0.0 + Ic[Y[2],3] = 1.0 # tangential slip + elseif type == "solid" # incompressible solid core + # First column + Ic[Y[1], 1] = n*r^( n+1 ) / ( 2*( 2n + 3) ) + Ic[Y[2], 1] = ( n+3 )*r^( n+1 ) / ( 2*( 2n+3 ) * ( n+1 ) ) + Ic[Y[3], 1] = ( n*ρ*g*r + 2*( n^2 - n - 3)*μ ) * r^n / ( 2*( 2n + 3) ) + Ic[Y[4], 1] = n *( n+2 ) * μ * r^n / ( ( 2n + 3 )*( n+1 ) ) + Ic[Y[6], 1] = 2π*G_norm*ρ*n*r^( n+1 ) / ( 2n + 3 ) + + # Second column + Ic[Y[1], 2] = r^( n-1 ) + Ic[Y[2], 2] = r^( n-1 ) / n + Ic[Y[3], 2] = ( ρ*g*r + 2*( n-1 )*μ ) * r^( n-2 ) + Ic[Y[4], 2] = 2*( n-1 ) * μ * r^( n-2 ) / n + Ic[Y[6], 2] = 4π*G_norm*ρ*r^( n-1 ) + + # Third column + Ic[Y[3], 3] = -ρ * r^n + Ic[Y[5], 3] = -r^n + Ic[Y[6], 3] = -( 2n + 1) * r^( n-1 ) + else + error("Invalid core type: $type. Must be 'liquid', 'inertial', or 'solid'.") + end + + return Ic + end + + + """ + get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, Y=[1,2,3,4,5,6]) + + Compute the 6x6 `A` matrix in the ODE for the solid-body problem. + + # Arguments + - `ω::prec` : Forcing frequency of the tidal forcing. + - `r::prec` : Radius at which to compute the A matrix. + - `ρ::prec` : Density at radius r. + - `g::prec` : Gravity at radius r. + - `μ::prec` : Shear modulus at radius r. + - `K::prec` : Bulk modulus at radius r. + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. + - `Y::Vector{Int}=[1,2,3,4,5,6]` : Ordering of the solution vector components. This allows for different conventions in the literature. + + # Returns + - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. + """ + function get_A(ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, Y=[1,2,3,4,5,6]) + M = length(Y) + A = zeros(precc, M, M) + get_A!(A, ω, r, ρ, g, μ, K, n; G0=G0, λ=λ, Y=Y) + return A + end + + + """ + get_A(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, λ=nothing, Y=[1,2,3,4,5,6,7,8]) + + Compute the 8x8 `A` matrix in the ODE for the two-phase problem. These correspond to + the coefficients given in Equation S4.6 in Hay et al., (2025). + + # Arguments + - `ω::prec` : Forcing frequency. + - `r::prec` : Radius at which to compute the A matrix. + - `ρ::prec` : Density at radius r. + - `g::prec` : Gravity at radius r. + - `μ::prec` : Shear modulus at radius r. + - `K::prec` : Bulk modulus at radius r. + - `ρₗ::prec` : Liquid density at radius r. + - `Kl::prec` : Liquid bulk modulus at radius r. + - `Kd::prec` : Drained bulk modulus at radius r. + - `α::prec` : Biot coefficient at radius r. + - `ηₗ::prec` : Liquid viscosity at radius r. + - `ϕ::prec` : Porosity at radius r. + - `k::prec` : Permeability at radius r. + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. + - `Y::Vector{Int}=[1,2,3,4,5,6,7,8]` : Ordering of the solution vector components. This allows for different conventions in the literature. + + # Returns + - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. + + See also [`get_A!`](@ref) + """ + function get_A(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, λ=nothing, Y=[1,2,3,4,5,6,7,8]) + A = zeros(precc, 8, 8) + get_A!(A, ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=G0, λ=λ, Y=Y) + return A + end + + + """ + get_A!(A, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, Y=[1,2,3,4,5,6]) + + Compute the 6x6 `A` matrix in the ODE for the solid-body problem. These correspond to + the coefficients given in Equation S4.6 in Hay et al., (2025) when α=φ=0, as well as Sabadini and Vermeersen + (2016) Eq. 1.95. + + # Arguments + - `A::Array{precc,2}` : 6x6 A matrix at radius r, which is used in the ODE for the solid-body problem. + - `ω::prec` : Forcing frequency of the tidal forcing. + - `r::prec` : Radius at which to compute the A matrix. + - `ρ::prec` : Density at radius r. + - `g::prec` : Gravity at radius r. + - `μ::prec` : Shear modulus at radius r. + - `K::prec` : Bulk modulus at radius r. + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. + - `Y::Vector{Int}=[1,2,3,4,5,6]` : Ordering of the solution vector components. This allows for different conventions in the literature.""" + function get_A!(A::Matrix, ω, r, ρ, g, μ, K, n; G0=1, λ=nothing, Y=[1,2,3,4,5,6]) + if isnothing(λ) + λ = K - 2μ/3 + end + + G_norm = G / G0 + + r_inv = 1.0/r + β_inv = 1.0/(2μ + λ) + rβ_inv = r_inv * β_inv + + A[Y[1],Y[1]] = -2λ * r_inv*β_inv + A[Y[2],Y[1]] = -r_inv + A[Y[3],Y[1]] = 4r_inv * (3K*μ*r_inv*β_inv - ρ*g) - ω^2 * ρ + A[Y[4],Y[1]] = -r_inv * (6K*μ*r_inv*β_inv - ρ*g ) + A[Y[5],Y[1]] = 4π * G_norm * ρ + A[Y[6],Y[1]] = 4π*(n+1)*G_norm*ρ*r_inv + + A[Y[1],Y[2]] = n*(n+1) * λ * r_inv*β_inv + A[Y[2],Y[2]] = r_inv + A[Y[3],Y[2]] = -n*(n+1)*r_inv * (6K*μ*r_inv*β_inv - ρ*g ) + A[Y[4],Y[2]] = 2μ*r_inv^2 * (n*(n+1)*(1 + λ*β_inv) - 1.0 ) - ω^2 * ρ + A[Y[6],Y[2]] = -4π*n*(n+1)*G_norm*ρ*r_inv + + A[Y[1],Y[3]] = β_inv + A[Y[3],Y[3]] = r_inv*β_inv * (-4μ ) + A[Y[4],Y[3]] = -λ * r_inv*β_inv + + A[Y[2],Y[4]] = 1.0 / μ + A[Y[3],Y[4]] = n*(n+1)*r_inv + A[Y[4],Y[4]] = -3r_inv + + A[Y[3],Y[5]] = ρ * (n+1)*r_inv + A[Y[4],Y[5]] = -ρ*r_inv + A[Y[5],Y[5]] = -(n+1)r_inv + + A[Y[3],Y[6]] = -ρ + A[Y[5],Y[6]] = 1.0 + A[Y[6],Y[6]] = (n-1)r_inv + end + + + """ + get_A!(A, ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, λ=nothing, Y=[1,2,3,4,5,6,7,8]) + + Compute the 8x8 `A` matrix in the ODE for the two-phase problem. These correspond to + the coefficients given in Equation S4.6 in Hay et al., (2025). + + # Arguments + - `A::Array{precc,2}` : 8x8 A matrix at radius r, which is used in the ODE for the two-phase problem. + - `ω::prec` : Forcing frequency. + - `r::prec` : Radius at which to compute the A matrix. + - `ρ::prec` : Density at radius r. + - `g::prec` : Gravity at radius r. + - `μ::prec` : Shear modulus at radius r. + - `K::prec` : Bulk modulus at radius r. + - `ρₗ::prec` : Liquid density at radius r. + - `Kl::prec` : Liquid bulk modulus at radius r. + - `Kd::prec` : Drained bulk modulus at radius r. + - `α::prec` : Biot coefficient at radius r. + - `ηₗ::prec` : Liquid viscosity at radius r. + - `ϕ::prec` : Porosity at radius r. + - `k::prec` : Permeability at radius r. + - `n::Int` : Tidal degree. + + # Keyword Arguments + - `G0::prec=1` : Gravitational constant scale for non-dimensionalization. + - `λ::prec=nothing` : Lamé's first parameter at radius r. If not provided, it is computed as λ = K - 2μ/3. + - `Y::Vector{Int}=[1,2,3,4,5,6,7,8]` : Ordering of the solution vector components. This allows for different conventions in the literature. + + # Notes + See also [`get_A`](@ref) + """ + function get_A!(A::Matrix, ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, λ=nothing, Y=[1,2,3,4,5,6,7,8]) + # λ = K - 2μ/3 # Lame's second param, which uses the drained compaction modulus + λ = Kd .- 2μ/3 # Lame's second param, which uses the drained compaction modulus + S = ϕ/Kl + (α - ϕ)/K # Storavity, which uses liquid and solid grain bulk moduli + + # First add the solid-body coefficients, but using drained moduli. + get_A!(A, ω, r, ρ, g, μ, Kd, n; λ=λ, G0=G0, Y=Y) # Note that here we replace the bulk modulus with the compaction modulus + + r_inv = 1.0/r + β_inv = 1.0/(2μ + λ) + + G_norm = G / G0 + + # ϕ = 0. + # If there is a porous layer, now add the two-phase components + if !iszero(ϕ) + + A[Y[1],Y[7]] = α * β_inv + + A[Y[3],Y[1]] += 1im * k*ρₗ^2 *g^2 * n*(n+1) / (ω*ηₗ) * r_inv^2 + A[Y[3],Y[5]] += -(n+1)r_inv * 1im *(k*ρₗ^2*g*n)/(ω*ηₗ) * r_inv + A[Y[3],Y[7]] = 1im * (k*ρₗ*g*n*(n+1))/(ω*ηₗ)*r_inv^2 - 4μ*α*β_inv*r_inv + A[Y[3],Y[8]] = 1im * (k*ρₗ^2*g^2*n*(n+1))/(ω*ηₗ)*r_inv^2 - 4ϕ*ρₗ*g*r_inv + + A[Y[4],Y[7]] = 2α*μ*r_inv * β_inv + A[Y[4],Y[8]] = ϕ*ρₗ*g*r_inv + + A[Y[5],Y[8]] = 4π*G_norm*ρₗ*ϕ + + A[Y[6],Y[1]] += -1im * 4π*G_norm*n*(n+1)*r_inv * (k*ρₗ^2*g/(ω*ηₗ)*r_inv) + A[Y[6],Y[5]] = 1im*4π*n*(n+1)G_norm*(ρₗ)^2*k*r_inv^2 / (ω*ηₗ) + A[Y[6],Y[7]] = -1im *4π*n*(n+1)G_norm*ρₗ*k*r_inv^2 / ( ω*ηₗ) + A[Y[6],Y[8]] = 4π*G_norm*(n+1)*r_inv * (ϕ*ρₗ - 1im * n*k*ρₗ^2*g/(ω*ηₗ)*r_inv) + + A[Y[7],Y[1]] = ρₗ*g*r_inv * ( 4 - 1im *(k*ρₗ*g*n*(n+1)/(ω*ϕ*ηₗ))*r_inv) + A[Y[7],Y[2]] = -ρₗ*n*(n+1)*r_inv*g + A[Y[7],Y[5]] = -ρₗ*(n+1)r_inv * (1 - 1im*(k*ρₗ*g*n)/(ω*ϕ*ηₗ)*r_inv) + A[Y[7],Y[6]] = ρₗ + A[Y[7],Y[7]] = - 1im*(k*ρₗ*g*n*(n+1))/(ω*ϕ*ηₗ)*r_inv^2 + A[Y[7],Y[8]] = -1im*ω*ϕ*ηₗ/k - 4π*G_norm*(ρ - ϕ*ρₗ)*ρₗ + ρₗ*g*r_inv*(4 - 1im*(k*ρₗ*g*n*(n+1))/(ω*ϕ*ηₗ)*r_inv) + + A[Y[8],Y[1]] = r_inv*( 1im * k*ρₗ*g*n*(n+1)/(ω*ϕ*ηₗ)*r_inv - α/ϕ * 4μ*β_inv) + A[Y[8],Y[2]] = α/ϕ * 2n*(n+1)*μ *β_inv * r_inv + A[Y[8],Y[3]] = -α/ϕ * β_inv + A[Y[8],Y[5]] = -1im * k *ρₗ *n*(n+1) / (ω*ϕ*ηₗ)*r_inv^2 + A[Y[8],Y[7]] = 1im*k*n*(n+1)/(ω*ϕ*ηₗ)*r_inv^2 - 1/ϕ * (S + α^2 * β_inv) # If solid and liquid are compressible, keep the 1/M term + A[Y[8],Y[8]] = 1im * k *ρₗ*g *n*(n+1) / (ω*ϕ*ηₗ)*r_inv^2 - 2r_inv + end + + end + + + """ + compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, SphericalGrid) + + Calculate the strain tensor ϵ at a particular radial level. + + # Arguments + - `ϵ::Array{ComplexF64,3}` : 3D array to store the strain tensor at a particular radial level, with dimensions corresponding to latitude, longitude, and the 6 independent components of the strain tensor. + - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 6 components. + - `n::Int` : Tidal degree. + - `rr::prec` : Radius at which to compute the strain tensor. + - `ρr::prec` : Density at radius rr. + - `gr::prec` : Gravity at radius rr. + - `μr::prec` : Shear modulus at radius rr. + - `Ksr::prec` : Bulk modulus at radius rr. + - `SphericalGrid` : A struct containing the spherical grid information, including latitudes, longitudes, and the associated spherical harmonic functions. + """ + function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, SphericalGrid) + + i = 1 + + @views clats = SphericalGrid.clats + @views Y = SphericalGrid.Y[i,:,:] + @views dYdθ = SphericalGrid.dYdθ[i,:,:] + @views dYdϕ = SphericalGrid.dYdϕ[i,:,:] + @views Z = SphericalGrid.Z[i,:,:] + @views X = SphericalGrid.X[i,:,:] + + y1 = y[1] + y2 = y[2] + y3 = y[3] + y4 = y[4] + + λr = Ksr .- 2μr/3 + βr = λr + 2μr + + # Compute strain tensor + ϵ[:,:,1] = (-2λr*y1 + n*(n+1)λr*y2 + rr*y3)/(βr*rr) * Y + ϵ[:,:,2] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y + 0.5y2*X) + ϵ[:,:,3] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y - 0.5y2*X) + ϵ[:,:,4] = 0.5/μr * y4 * dYdθ + ϵ[:,:,5] = 0.5/μr * y4 * dYdϕ .* 1.0 ./ sin.(clats) + ϵ[:,:,6] = 0.5 * y2/rr * Z + end + + + """ + compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr, SphericalGrid) + + Calculate the strain tensor ϵ at a particular radial level. + + # Arguments + - `ϵ::Array{ComplexF64,4}` : 4D array to store the strain tensor at a particular radial level, with dimensions corresponding to latitude, longitude, and the 6 independent components of the strain tensor. + - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 6 components. + - `n::Int` : Tidal degree. + - `rr::prec` : Radius at which to compute the strain tensor. + - `ρr::prec` : Density at radius rr. + - `gr::prec` : Gravity at radius rr. + - `μr::prec` : Shear modulus at radius rr. + - `Ksr::prec` : Bulk modulus at radius rr. + - `ω::prec` : Forcing frequency. + - `ρlr::prec` : Liquid density at radius rr. + - `Klr::prec` : Liquid bulk modulus at radius rr. + - `Kdr::prec` : Drained bulk modulus at radius rr. + - `αr::prec` : Biot coefficient at radius rr. + - `ηlr::prec` : Liquid viscosity at radius rr. + - `ϕr::prec` : Porosity at radius rr. + - `kr::prec` : Permeability at radius rr. + - `SphericalGrid` : A struct containing the spherical grid information (Y, dYdθ, dYdϕ) for the current radial level. + """ + function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr, SphericalGrid) + i = 1 + + @views clats = SphericalGrid.clats + @views Y = SphericalGrid.Y[i,:,:] + @views dYdθ = SphericalGrid.dYdθ[i,:,:] + @views dYdϕ = SphericalGrid.dYdϕ[i,:,:] + @views Z = SphericalGrid.Z[i,:,:] + @views X = SphericalGrid.X[i,:,:] + + y1 = y[1] + y2 = y[2] + y3 = y[3] + y4 = y[4] + y7 = y[7] + + λr = Kdr - 2μr/3 + βr = λr + 2μr + + # Compute strain tensor + ϵ[:,:,1] = (-2λr*y1 + n*(n+1)λr*y2 + rr*y3 + rr*αr*y7)/(βr*rr) * Y # e_rr + ϵ[:,:,2] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y + 0.5y2*X) # e_ + ϵ[:,:,3] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y - 0.5y2*X) # e_ + ϵ[:,:,4] = 0.5/μr * y4 * dYdθ # e_rθ + ϵ[:,:,5] = 0.5/μr * y4 * dYdϕ .* 1.0 ./ sin.(clats) # e_rϕ + ϵ[:,:,6] = 0.5 * y2/rr * Z # e_ + end + + + """ + compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl, SphericalGrid) + + Calculate the Darcy displacement vector at a particular radial level. + + # Arguments + - `dis_rel::Array{ComplexF64,4}` : 4D array to store the Darcy displacement vector at a particular radial level, with dimensions corresponding to latitude, longitude, and the 3 components of the Darcy displacement vector. + - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 8 components. + - `n::Int` : Tidal degree. + - `r::prec` : Radius at which to compute the Darcy displacement vector. + - `ω::prec` : Forcing frequency. + - `ϕ::prec` : Porosity at radius r. + - `ηl::prec` : Liquid viscosity at radius r. + - `k::prec` : Permeability at radius r. + - `g::prec` : Gravity at radius r. + - `ρl::prec` : Liquid density at radius r. + - `SphericalGrid` : A struct containing the spherical grid information (Y, dYdθ, dYdϕ) for the current radial level. + """ + function compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl, SphericalGrid) + i = 1 + + @views clats = SphericalGrid.clats + @views Y = SphericalGrid.Y[i,:,:] + @views dYdθ = SphericalGrid.dYdθ[i,:,:] + @views dYdϕ = SphericalGrid.dYdϕ[i,:,:] + + y1 = y[1] + y5 = y[5] + y7 = y[7] + y8 = y[8] + y9 = 1im * k / (ω*ϕ*ηl*r) * (ρl*g*y1 - ρl * y5 + ρl*g*y8 + y7) + + dis_rel[:,:,1] = Y * y8 + dis_rel[:,:,2] = dYdθ * y9 + dis_rel[:,:,3] = dYdϕ * y9 ./ sin.(clats) + end + + + """ + compute_pore_pressure!(p, y, n, SphericalGrid) + + Calculate the fluid pore pressur at a particular radial level. + + # Arguments + - `p::Array{ComplexF64,4}` : 4D array to store the pore pressure at a particular radial level, with dimensions corresponding to latitude and longitude. + - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 8 components. + - `n::Int` : Tidal degree. + - `SphericalGrid` : A struct containing the spherical grid information (Y, dYdθ, dYdϕ) for the current radial level. + """ + function compute_pore_pressure!(p, y, n, SphericalGrid) + i = 1 + + @views Y = SphericalGrid.Y[i,:,:] + @views dYdθ = SphericalGrid.dYdθ[i,:,:] + @views dYdϕ = SphericalGrid.dYdϕ[i,:,:] + + y7 = y[7] + + p[:,:] = Y * y7 + end + + + """ + function get_heating_profile(y, r, ρ, g, μ, κ, n, ω, SphericalGrid) + + Get the radial volumetric heating for solid-body tides and eccentricity forcing, + assuming synchronous rotation. Heating rate is computed with numerical integration + using the solution `y`, using Eq. 2.39a/b integrated over solid angle. + + # Arguments + - `y::Array{ComplexF64,6}` : 6D array of the solution vector y across the interior, returned by `compute_y`. + - `r::AbstractVector` : 1D vector of radial coordinates or shell boundaries. + - `ρ::AbstractVector` : 1D vector of densities at each radial shell. + - `g::AbstractVector` : 1D vector of gravitational acceleration values at each radial shell. + - `μ::AbstractVector` : 1D vector of complex shear moduli at each radial shell. + - `κ::AbstractVector` : 1D vector of complex bulk moduli at each radial shell. + - `n::Int` : Tidal degree. + - `ω` : Tidal frequency in radians per second. + - `SphericalGrid` : A struct containing the spherical grid information, including latitudes, longitudes, and the associated spherical harmonic functions. + + # Returns + - `Eμ_tot::Array{Float64,1}` : 1D array of total power dissipated in each radial shell due to shear, in W. + - `Eκ_tot::Array{Float64,1}` : 1D array of total power dissipated in each radial shell due to compaction, in W. + """ + function get_heating_profile(y, r, ρ, g, μ, κ, n, ω, SphericalGrid) + + dres = deg2rad(SphericalGrid.res) + + clats = SphericalGrid.clats + lons = SphericalGrid.lons + + Nr = length(r) + nlats = length(clats) + nlons = length(lons) + + # strain tensor per radius + ϵ = zeros(ComplexF64, nlats, nlons, 6) + + # outputs + Eμ_tot = zeros(Float64, Nr-1) + Eκ_tot = zeros(Float64, Nr-1) + + for i in 1:Nr-1 + + rr = r[i] + dr = r[i+1] - r[i] + + dvol = 4π/3 * (r[i+1]^3 - r[i]^3) + + yrr = y[:, i] + + compute_strain_ten!(ϵ, yrr, n, rr, ρ[i], g[i], μ[i], κ[i], SphericalGrid) + + # shear heating + Eμ_loc = sum(abs.(ϵ[:,:,1:3]).^2, dims=3) .+ + 2sum(abs.(ϵ[:,:,4:6]).^2, dims=3) .- + 1/3 .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + + Eμ_loc .*= ω * imag(μ[i]) + + # bulk heating + Eκ_loc = ω/2 * imag(κ[i]) .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + + # angular integration + weight = sin.(clats) + + Eμ_tot[i] = sum(weight .* Eμ_loc * dres^2) * rr^2 * dr / dvol + Eκ_tot[i] = sum(weight .* Eκ_loc * dres^2) * rr^2 * dr / dvol + + end + + return Eμ_tot, Eκ_tot + end + + + """ + get_heating_profile(y, r, ρ, g, μ, Ks, ω, ρl, Kl, Kd, α, ηl, ϕ, k, n, SphericalGrid; lay=nothing) + + Get the radial volumetric heating for two-phase tides and eccentricity forcing, + assuming synchronous rotation. Heating rate is computed with numerical integration + using the solution `y`, using Eq. 2.39a/b/c integrated over solid angle. + + # Arguments + - `y::Array{ComplexF64,2}` : 2D array [state_vector, radius] of the solution vector. + - `r::AbstractVector` : 1D vector of radial boundaries/shell coordinates. + - `ρ::AbstractVector` : 1D vector of layer densities. + - `g::AbstractVector` : 1D vector of gravity values. + - `μ::AbstractVector` : 1D vector of complex shear moduli. + - `Ks::AbstractVector` : 1D vector of bulk moduli for shear dissipation. + - `ω::Float64` : Tidal frequency in radians per second. + - `ρl::AbstractVector` : 1D vector of liquid densities. + - `Kl::AbstractVector` : 1D vector of liquid bulk moduli. + - `Kd::AbstractVector` : 1D vector of drained bulk moduli. + - `α::AbstractVector` : 1D vector of Biot coefficients. + - `ηl::AbstractVector` : 1D vector of liquid viscosities. + - `ϕ::AbstractVector` : 1D vector of porosities. + - `k::AbstractVector` : 1D vector of permeabilities. + - `n::Int` : Tidal degree. + - `SphericalGrid` : A struct containing the spherical grid information (Y, dYdθ, dYdϕ) for the current radial level. + + # Returns + - `Eμ_tot::Vector{Float64}` : Total power dissipated due to shear (W/m³). + - `Eκ_tot::Vector{Float64}` : Total power dissipated due to compaction (W/m³). + - `El_tot::Vector{Float64}` : Total power dissipated due to Darcy flow (W/m³). + """ + function get_heating_profile(y, r, ρ, g, μ, Ks, ω, ρl, Kl, Kd, α, ηl, ϕ, k, n, SphericalGrid) + + dres = deg2rad(SphericalGrid.res) + clats = SphericalGrid.clats + lons = SphericalGrid.lons + + # Reference spherical harmonic Y if needed globally + # (Assuming the logic uses the same spatial resolution as the first example) + Y = SphericalGrid.Y[1, :, :] + + Nr = length(r) + nlats = length(clats) + nlons = length(lons) + + # Pre-allocate spatial buffers + ϵ = zeros(ComplexF64, nlats, nlons, 6) + d_disp = zeros(ComplexF64, nlats, nlons, 3) + p = zeros(ComplexF64, nlats, nlons) + + # Output vectors (per radial shell) + Eμ_tot = zeros(Float64, Nr - 1) + Eκ_tot = zeros(Float64, Nr - 1) + El_tot = zeros(Float64, Nr - 1) + + for i in 1:Nr-1 + rr = r[i] + dr = r[i+1] - r[i] + dvol = 4π/3 * (r[i+1]^3 - r[i]^3) + yrr = y[:, i] + + # 1. Compute Tensors/Fields + compute_strain_ten!(ϵ, yrr, n, rr, ρ[i], g[i], μ[i], Ks[i], ω, ρl[i], Kl[i], Kd[i], α[i], ηl[i], ϕ[i], k[i], SphericalGrid) + + if ϕ[i] > 0 + compute_darcy_displacement!(d_disp, yrr, n, rr, ω, ϕ[i], ηl[i], k[i], g[i], ρl[i], SphericalGrid) + p .= yrr[7] .* Y + else + p .= 0.0 + end + + # 2. Shear Heating (Solid) + Eμ_loc = sum(abs.(ϵ[:,:,1:3]).^2, dims=3) .+ + 2sum(abs.(ϵ[:,:,4:6]).^2, dims=3) .- + 1/3 .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + Eμ_loc .*= ω * imag(μ[i]) + + # 3. Compaction Heating (Bulk) + Eκ_loc = ω/2 * imag(Kd[i]) .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + if ϕ[i] > 0 + Eκ_loc .+= (ω/2 * imag(Kd[i]) .* (abs.(p) ./ Ks[i]).^2) + end + + # 4. Darcy Dissipation (Liquid) + El_loc = zeros(nlats, nlons) + if ϕ[i] > 0 + El_loc .= 0.5 * ϕ[i]^2 * ω^2 * (ηl[i] / k[i]) .* (abs.(d_disp[:,:,1]).^2 .+ abs.(d_disp[:,:,2]).^2 .+ abs.(d_disp[:,:,3]).^2) + end + + # 5. Angular Integration and Volume Averaging + weight = sin.(clats) + + Eμ_tot[i] = sum(weight .* Eμ_loc .* dres^2) * rr^2 * dr / dvol + Eκ_tot[i] = sum(weight .* Eκ_loc .* dres^2) * rr^2 * dr / dvol + El_tot[i] = sum(weight .* El_loc .* dres^2) * rr^2 * dr / dvol + end + + return Eμ_tot, Eκ_tot, El_tot + end + + + """ + function get_heating_map(y, r, ρ, g, μ, κ, n, ω, SphericalGrid) + + Get the surface heating map for solid-body tides, assuming synchronous rotation. + The heating rate is computed by radially integrating the local volumetric + dissipation rates determined from the solution `y` and Eq. 2.39a/b. + + # Arguments + - `y::Array{ComplexF64,2}` : 2D array [state_vector, radius] of the solution vector. + - `r::AbstractVector` : 1D vector of radial shell boundaries. + - `ρ::AbstractVector` : 1D vector of densities at each radial shell. + - `g::AbstractVector` : 1D vector of gravity values. + - `μ::AbstractVector` : 1D vector of complex shear moduli. + - `κ::AbstractVector` : 1D vector of complex bulk moduli. + - `n::Int` : Tidal degree. + - `ω` : Tidal frequency in radians per second. + - `SphericalGrid` : A struct formed by `define_spherical_grid`, containing + the geographic grid and spherical harmonic derivatives. + + # Returns + - `Eμ_map::Array{Float64,2}` : 2D geographic map of shear dissipation (W/m²). + - `Eκ_map::Array{Float64,2}` : 2D geographic map of compaction/bulk dissipation (W/m²). + """ + function get_heating_map(y, r, ρ, g, μ, κ, n, ω, SphericalGrid) + + clats = SphericalGrid.clats + lons = SphericalGrid.lons + + Nr = length(r) + nlats = length(clats) + nlons = length(lons) + + # Pre-allocate spatial buffers for local shell calculation + ϵ = zeros(ComplexF64, nlats, nlons, 6) + + # Initialize output maps (Integrated over radius) + Eμ_map = zeros(Float64, nlats, nlons) + Eκ_map = zeros(Float64, nlats, nlons) + + for i in 1:Nr-1 + + rr = r[i] + dr = r[i+1] - r[i] + yrr = y[:, i] + + # 1. Compute the strain tensor for the current shell + compute_strain_ten!(ϵ, yrr, n, rr, ρ[i], g[i], μ[i], κ[i], SphericalGrid) + + # 2. Local Volumetric Shear Heating (W/m³) + Eμ_loc = sum(abs.(ϵ[:,:,1:3]).^2, dims=3) .+ + 2sum(abs.(ϵ[:,:,4:6]).^2, dims=3) .- + 1/3 .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + + Eμ_loc .*= ω * imag(μ[i]) + + # 3. Local Volumetric Bulk Heating (W/m³) + Eκ_loc = ω/2 * imag(κ[i]) .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + + # 4. Radial Integration (W/m³ * m -> W/m²) + # We accumulate the heat flux from each shell into the final map + Eμ_map .+= Eμ_loc .* dr + Eκ_map .+= Eκ_loc .* dr + + end + + return Eμ_map, Eκ_map + end + + + """ + function get_heating_map(y, r, ρ, g, μ, Ks, ω, ρl, Kl, Kd, α, ηl, ϕ, k, n, SphericalGrid) + + Get the surface heating map for two-phase tides, assuming synchronous rotation. + The local volumetric heating rates (W/m³) are computed from the solution `y` + using the rheological parameters for both the solid matrix and the melt, then + radially integrated to obtain surface maps in W/m². + + # Arguments + - `y::Array{ComplexF64,2}` : 2D array [state_vector, radius] of the solution vector. + - `r::AbstractVector` : 1D vector of radial shell boundaries. + - `ρ`, `g`, `μ`, `Ks` : Solid phase properties (density, gravity, shear modulus, grain bulk modulus). + - `ω` : Tidal frequency in radians per second. + - `ρl`, `Kl`, `Kd`, `α`, `ηl`, `ϕ`, `k`: Liquid/two-phase properties (liquid density, liquid bulk modulus, + drained bulk modulus, Biot coefficient, viscosity, porosity, permeability). + - `n::Int` : Tidal degree. + - `SphericalGrid` : A struct containing geographic grid and spherical harmonic data. + + # Returns + - `Eμ_map::Array{Float64,2}` : Surface map of shear dissipation (W/m²). + - `Eκ_map::Array{Float64,2}` : Surface map of compaction dissipation (W/m²). + - `El_map::Array{Float64,2}` : Surface map of Darcy (percolation) dissipation (W/m²). + """ + function get_heating_map(y, r, ρ, g, μ, Ks, ω, ρl, Kl, Kd, α, ηl, ϕ, k, n, SphericalGrid) + + clats = SphericalGrid.clats + lons = SphericalGrid.lons + Y = SphericalGrid.Y[1, :, :] + + Nr = length(r) + nlats = length(clats) + nlons = length(lons) + + # Pre-allocate spatial buffers for local shell calculation + ϵ = zeros(ComplexF64, nlats, nlons, 6) + d_disp = zeros(ComplexF64, nlats, nlons, 3) + p = zeros(ComplexF64, nlats, nlons) + + # Initialize output maps (Integrated over radius) + Eμ_map = zeros(Float64, nlats, nlons) + Eκ_map = zeros(Float64, nlats, nlons) + El_map = zeros(Float64, nlats, nlons) + + for i in 1:Nr-1 + + rr = r[i] + dr = r[i+1] - r[i] + yrr = y[:, i] + + # 1. Compute Tensors/Fields for the current shell + compute_strain_ten!(ϵ, yrr, n, rr, ρ[i], g[i], μ[i], Ks[i], ω, ρl[i], Kl[i], Kd[i], α[i], ηl[i], ϕ[i], k[i], SphericalGrid) + + if ϕ[i] > 0 + compute_darcy_displacement!(d_disp, yrr, n, rr, ω, ϕ[i], ηl[i], k[i], g[i], ρl[i], SphericalGrid) + p .= yrr[7] .* Y + else + p .= 0.0 + end + + # 2. Local Volumetric Shear Heating (Solid) + Eμ_loc = sum(abs.(ϵ[:,:,1:3]).^2, dims=3) .+ + 2sum(abs.(ϵ[:,:,4:6]).^2, dims=3) .- + 1/3 .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + Eμ_loc .*= ω * imag(μ[i]) + + # 3. Local Volumetric Compaction Heating (Bulk) + Eκ_loc = ω/2 * imag(Kd[i]) .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 + if ϕ[i] > 0 + Eκ_loc .+= (ω/2 * imag(Kd[i]) .* (abs.(p) ./ Ks[i]).^2) + end + + # 4. Local Volumetric Darcy Dissipation (Liquid) + El_loc = zeros(nlats, nlons) + if ϕ[i] > 0 + El_loc .= 0.5 * ϕ[i]^2 * ω^2 * (ηl[i] / k[i]) .* (abs.(d_disp[:,:,1]).^2 .+ abs.(d_disp[:,:,2]).^2 .+ abs.(d_disp[:,:,3]).^2) + end + + # 5. Radial Integration (W/m³ * m -> W/m²) + Eμ_map .+= Eμ_loc .* dr + Eκ_map .+= Eκ_loc .* dr + El_map .+= El_loc .* dr + + end + + return Eμ_map, Eκ_map, El_map + end + +end \ No newline at end of file diff --git a/src/plotting.jl b/src/plotting.jl index a22d0e9..6156a60 100644 --- a/src/plotting.jl +++ b/src/plotting.jl @@ -327,14 +327,44 @@ module plotting end + function plot_surface_heating(H, res; filename="tidal_heating_map_surface.png", title_str="Surface heating map") + + # convert to Float64 + H_f64 = Float64.(H) + + # lat-lon grid + lons = collect(0:res:360-0.001) + lats = collect(0:res:180) + + # heatmap + plt = heatmap( + lons, + lats, + log10.(H_f64); + xlabel = "Latitude", + ylabel = "Longitude", + colorbar_title = "log10(surface heating)", + title = title_str, + aspect_ratio = :equal + ) + + savefig(plt, filename) + @info "Saved surface heating map to $filename" + + return plt + end + + function plot_relaxation_solution(y, r; filename="relaxation_solution.png") R = maximum(r) rnorm = r ./ R - plt = plot(layout=(2,3), size=(1000,600)) + N = Int(size(y, 1)) + + plt = plot(layout=(2,Int(N/2)), size=(1000,600)) - for i in 1:6 + for i in 1:N yi = y[i, :] # plot directly into subplot @@ -370,5 +400,4 @@ module plotting return plt end - end \ No newline at end of file diff --git a/src/solid1d.jl b/src/solid1d.jl index 795828b..6d55ba6 100644 --- a/src/solid1d.jl +++ b/src/solid1d.jl @@ -183,6 +183,7 @@ module solid1d # save grids solid1d.clats = clats solid1d.lons = lons + end @@ -326,7 +327,7 @@ module solid1d """ - compute_M(ω, r, ρ, g, μ, K, n, ρ_core; core="liquid") + compute_M(ω, r, ρ, g, μ, K, n, ρ_core, μ_core, κ_core; core="liquid") Compute the M matrix, which is used to propagate the solution across the entire interior. This is used in the `compute_y` function. @@ -339,6 +340,8 @@ module solid1d - `K::Array{prec,1}` : 1D array of layer bulk moduli. - `n::Int` : Tidal degree. - `ρ_core::prec` : Density of the core, which is used to compute the starting vector for the numerical integration across the interior. + - `μ_core::prec` : Shear modulus of the core. + - `κ_core::prec` : Bulk modulus of the core. # Keyword Arguments - `core::String="liquid"` : Type of core, either "liquid" or "solid". This is used to compute the starting vector for the numerical integration across the interior. @@ -347,13 +350,13 @@ module solid1d - `M::Array{precc,2}` : 3x3 M matrix, which is used to propagate the solution across the entire interior. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. """ - function compute_M(ω, r, ρ, g, μ, K, n, ρ_core; core="liquid") + function compute_M(ω, r, ρ, g, μ, K, n, ρ_core, μ_core, κ_core; core="liquid") r, ρ, g, μ, K = convert_params_to_prec(r, ρ, g, μ, K) nlayers = size(r)[2] nsublayers = size(r)[1] - y_start = get_Ic(ω, r[end,1], ρ_core, g[end,1], μ[1], K[1], core, n; Y=[1,2,3,4,5,6]) + y_start = get_Ic(ω, r[end,1], ρ_core, g[end,1], μ_core, κ_core, core, n; Y=[1,2,3,4,5,6]) y1_4 = zeros(precc, 6, 3, nsublayers-1, nlayers) # Three linearly independent y solutions @@ -453,6 +456,8 @@ module solid1d function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr) i = 1 + println(sin.(clats)) + @views Y = solid1d.Y[i,:,:] @views dYdθ = solid1d.dYdθ[i,:,:] @views dYdϕ = solid1d.dYdϕ[i,:,:] diff --git a/src/solid1d_mush.jl b/src/solid1d_mush.jl index 383b12e..5317c2f 100644 --- a/src/solid1d_mush.jl +++ b/src/solid1d_mush.jl @@ -461,7 +461,7 @@ module solid1d_mush """ - compute_M(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; core="liquid") + compute_M(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, μ_core, κ_core; core="liquid") Compute the 4x4 M matrix, which relates the solution at the surface and porous layer interface to the core solution. @@ -480,6 +480,9 @@ module solid1d_mush - `ϕ::Array{prec,1}` : 1D array of porosities at layer boundaries. - `k::Array{prec,1}` : 1D array of permeabilities at layer boundaries. - `n::Int` : Tidal degree. + - `ρ_core::prec` : Density of the core. + - `μ_core::prec` : Shear modulus of the core. + - `κ_core::prec` : Bulk modulus of the core. # Keyword Arguments - `core::String="liquid"` : Type of core, either "liquid" or "solid". This is used to compute the starting vector for the numerical integration across the interior. @@ -488,7 +491,7 @@ module solid1d_mush - `M::Array{precc,2}` : 4x4 M matrix, which is used to propagate the solution across the entire interior. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. """ - function compute_M(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core; core="liquid") + function compute_M(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, μ_core, κ_core; core="liquid") porous_layer = ϕ .> 0.0 ## Convert parameters to the precision of precc: @@ -500,7 +503,7 @@ module solid1d_mush nsublayers = size(r)[1] # Define starting vector as the core solution matrix, Y_r_C (Eq. S5.15) - y_start = get_Ic(ω, r[end,1], ρ_core, g[end,1], μ[1], K[1], core, n; Y=[1,2,3,4,5,6,7,8]) + y_start = get_Ic(ω, r[end,1], ρ_core, g[end,1], μ_core, κ_core, core, n; Y=[1,2,3,4,5,6,7,8]) y1_4 = zeros(precc, 8, 4, nsublayers-1, nlayers) # Four linearly independent y solutions diff --git a/src/solid1d_mush_relax.jl b/src/solid1d_mush_relax.jl index eccb92f..e54b466 100644 --- a/src/solid1d_mush_relax.jl +++ b/src/solid1d_mush_relax.jl @@ -8,7 +8,6 @@ module solid1d_mush_relax using LinearAlgebra import GenericLinearAlgebra using DoubleFloats - using AssociatedLegendrePolynomials using StaticArrays using SpecialFunctions using SparseArrays @@ -19,15 +18,6 @@ module solid1d_mush_relax const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 - clats = 0.0 - lons = 0.0 - Y = 0.0 - dYdθ = 0.0 - dYdϕ = 0.0 - Z = 0.0 - X = 0.0 - res = 20.0 - """ resample_profiles(radius, rho, visc, shear, bulk, phi, m_core, dr_min, dr_max) @@ -141,108 +131,9 @@ module solid1d_mush_relax return g, M_enc[end] end - - """ - Ynm(n, m, theta, phi) - - Compute the spherical harmonic Ynm for given n, m, theta, and phi. - - # Arguments - - `n::Int` : Tidal degree. - - `m::Int` : Tidal order. - - `theta::Array{Float64,1}` : Array of colatitudes in radians. - - `phi::Array{Float64,1}` : Array of longitudes in radians. - - # Returns - - `Ynm::Array{ComplexF64,2}` : 2D array of spherical harmonic values for each combination of theta and phi. - """ - function Ynm(n, m, theta, phi) - return Plm.(n, m, cos.(theta)) .* exp.(1im * m .* phi) - end - - - """ - define_spherical_grid(res) - - Create the spherical grid of angular resolution `res` in degrees. This is used for - numerical integrations over solid angle. A new grid can easily be defined by - recalling the function with a new `res`. - - # Arguments - - `res::Float64` : Desired angular resolution in degrees. - - `n::Int` : Tidal degree. - - `m::Int` : Tidal order. - - # Notes - The grid is internal to solid1d_relax, but can be accessed with - - solid1d_relax.clats[:] # colatitude grid - solid1d_relax.lons[:] # longitude grid - """ - function define_spherical_grid(res, n, m) - solid1d_mush_relax.res = res - - # θ and φ grids - lons = deg2rad.(collect(0:res:360-0.001))' - clats = deg2rad.(collect(0:res:180)) - clats[1] += 1e-6 - clats[end] -= 1e-6 - - # allocate arrays - solid1d_mush_relax.Y = zeros(ComplexF64, 1, length(clats), length(lons)) - solid1d_mush_relax.dYdθ = similar(solid1d_mush_relax.Y) - solid1d_mush_relax.dYdϕ = similar(solid1d_mush_relax.Y) - solid1d_mush_relax.Z = similar(solid1d_mush_relax.Y) - solid1d_mush_relax.X = similar(solid1d_mush_relax.Y) - - sinθ = sin.(clats) - cosθ = cos.(clats) - cotθ = cosθ ./ sinθ - cscθ = csc.(clats) - - # Normalization factor for spherical harmonics - norm = sqrt((2*n+1) * factorial(n-m) / (4π * factorial(n+m))) - - i = 1 - - # Y - solid1d_mush_relax.Y[i,:,:] = Ynm(n,m,clats,lons) - - # ∂Y/∂θ - Pn = Plm.(n, m, cosθ) - if n > m - Pn_1 = Plm.(n-1, m, cosθ) - dPdθ = (n .* cosθ .* Pn .- (n + m) .* Pn_1) ./ (sinθ) - else - # m == n -> P_{n-1}^m = 0 - dPdθ = (n .* cosθ .* Pn) ./ (sinθ) - end - solid1d_mush_relax.dYdθ[i,:,:] .= dPdθ .* exp.(1im .* m .* lons) - - # ∂Y/∂ϕ - solid1d_mush_relax.dYdϕ[i,:,:] .= 1im * m .* solid1d_mush_relax.Y[i,:,:] - - # Z = 2 ((1/sinθ) ∂²Y/∂θ∂ϕ - cotθ cscθ ∂Y/∂ϕ) - solid1d_mush_relax.Z[i,:,:] .= 2 .* (1im * m ./ sinθ .* solid1d_mush_relax.dYdθ[i,:,:] .- cotθ .* cscθ .* solid1d_mush_relax.dYdϕ[i,:,:]) - - # X = -2 (cotθ ∂Y/∂θ + csc²θ ∂²Y/∂ϕ²) - n(n+1)) Y - solid1d_mush_relax.X[i,:,:] .= -2 .* (cotθ .* solid1d_mush_relax.dYdθ[i,:,:] .- cscθ.^2 .* m^2 .* solid1d_mush_relax.Y[i,:,:]) .- n*(n+1) .* solid1d_mush_relax.Y[i,:,:] - - # Normalize - solid1d_mush_relax.Y[i,:,:] .*= norm - solid1d_mush_relax.dYdθ[i,:,:] .*= norm - solid1d_mush_relax.dYdϕ[i,:,:] .*= norm - solid1d_mush_relax.Z[i,:,:] .*= norm - solid1d_mush_relax.X[i,:,:] .*= norm - - # save grids - solid1d_mush_relax.clats = clats - solid1d_mush_relax.lons = lons - end - """ - solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core; core="liquid") + solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, μ_core, κ_core, M_tot; core="liquid") Solve the radial system of ODEs for the solid-body problem using a relaxation method. This function implements the forward-backward relaxation scheme described in the main text of N. Kobayashi (2006). @@ -254,15 +145,17 @@ module solid1d_mush_relax - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. - `ω::prec` : Angular frequency of the tidal forcing. - - `ρₗ::Vector{prec}` : Vector of liquid densities at the layer centers. + - `ρₗ::Vector{prec}` : Vector of liquid densities at the layer centers. - `Kl::Vector{prec}` : Vector of liquid bulk moduli at the layer centers. - `Kd::Vector{prec}` : Vector of drained bulk moduli at the layer centers. - `α::Vector{prec}` : Vector of Biot coefficients at the layer centers. - - `ηₗ::Vector{prec}` : Vector of liquid viscosities at the layer centers. + - `ηₗ::Vector{prec}` : Vector of liquid viscosities at the layer centers. - `ϕ::Vector{prec}` : Vector of porosities at the layer centers. - `k::Vector{prec}` : Vector of permeabilities at the layer centers. - `n::Int` : Tidal degree. - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `μ_core::precc` : Complex shear modulus of the core, used for core boundary conditions. + - `κ_core::precc` : Complex bulk modulus of the core, used for core boundary conditions. - `M_tot::prec` : Total mass of the body, used for non-dimensionalization. # Keyword Arguments @@ -272,113 +165,101 @@ module solid1d_mush_relax - `y_t::Vector{precc}` : Vector of length 8 representing the tidal solution at the top of the mantle. This includes the displacements, stresses, and potential at the surface. - `y_l::Vector{precc}` : Vector of length 8 representing the load solution at the top of the mantle. This includes the displacements, stresses, and potential at the surface. - `R::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the coefficients of the ODE system at each radial layer. - - `S::Vector{Matrix{precc}}` : Vector of 8x8 matrices representing the normalization. + - `Y_inv::Vector{Int}` : Vector of inverse ordering indices for the 8x8 case. - `transitions::Vector{Int}` : Indices of the interface layers (the ones closer to the core and surface). """ function solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, - ρ_core, M_tot; core="liquid") + ρ_core, μ_core, κ_core, M_tot; core="liquid") - # Define the ordering of the solution vector components for the 6x6 and 8x8 cases + # Define ordering Y6 = [1,2,4,5,3,6] Y8 = [1,2,5,6,3,7,4,8] + Y8_inv = [1,2,5,7,3,4,6,8] - # 1. Identify Regions and Transitions - # We define a boolean mask where true = mushy, false = solid + # 1. Identify Regions + # No duplication needed; we use the original grid dimensions is_mush = k .> 0 - - # Detect indices where the state changes - transitions = findall(diff(is_mush) .!= 0) - - # 2. Duplicate nodes at transition points for the relaxation scheme - # We iterate backwards to keep indices valid during insertion - new_r, new_ρ, new_g, new_μ, new_K, new_ρₗ, new_Kl, new_Kd, new_α, new_ηₗ, new_ϕ, new_k = - copy(r), copy(ρ), copy(g), copy(μ), copy(K), copy(ρₗ), copy(Kl), copy(Kd), copy(α), copy(ηₗ), copy(ϕ), copy(k) - - for trans_idx in reverse(transitions) - for v in (new_r, new_ρ, new_g, new_μ, new_K, new_ρₗ, new_Kl, new_Kd, new_α, new_ηₗ, new_ϕ, new_k) - insert!(v, trans_idx + 1, v[trans_idx]) - end - end - - Nr = length(new_r) - - new_is_mush = new_k .> 0 + Nr = length(r) - # 3. Dynamic Scaling (needs to be checked for consistency with the non-dimensionalization in get_scales) + # 2. Dynamic Scaling R0, M0, s0, ρ0, G0, g0, μ0, S, Sinv = get_scales(1., 1., 1.; Y=Y8) ωs = ω * s0 - rs, ρs, gs, μs, Ks = new_r./R0, new_ρ./ρ0, new_g./g0, new_μ./μ0, new_K./μ0 - ρₗs, Kls, Kds, ηₗs, ks = new_ρₗ./ρ0, new_Kl./μ0, new_Kd./μ0, new_ηₗ./(μ0*s0), new_k./R0^2 + rs, ρs, gs, μs, Ks = r./R0, ρ./ρ0, g./g0, μ./μ0, K./μ0 + ρₗs, Kls, Kds, ηₗs, ks = ρₗ./ρ0, Kl./μ0, Kd./μ0, ηₗ./(μ0*s0), k./R0^2 - # 4. Initialize Matrices + # 3. Initialize Matrices R = [Matrix{precc}(I, 8, 8) for _ in 1:Nr] B = [zeros(precc, 8, 1) for _ in 1:Nr] idx_6 = [1, 2, 3, 5, 6, 7] R6_view = [view(R[i], idx_6, idx_6) for i in 1:Nr] R8_view = [view(R[i], 1:8, 1:8) for i in 1:Nr] - # 5. Adaptive Propagation Loop - @debug("\n--- Adaptive Solver Plan ---") - @debug("Total Nodes (Nr): $Nr") - @debug("Transitions at indices: $transitions") + @debug("\n--- Adaptive Solver (Direct Interfaces) ---") curr_idx = 1 - # Step 1: Core - if !new_is_mush[1] - @debug("STEP 1: [Core Boundary] Solid | Indices: (1, 2)") - C1l, D2l = core_boundary(R6_view, (1, 2), rs, ρs, gs, μs, Ks, ωs, ρ_core/ρ0, core, n; G0=G0, Y=Y6) + # Step 1: Core Boundary + if !is_mush[1] + @debug("STEP 1: [Core Boundary] Solid") + C1l, D2l = core_boundary(R6_view, (1, 2), rs, ρs, gs, μs, Ks, ωs, ρ_core/ρ0, μ_core/μ0, κ_core/μ0, core, n; G0=G0, Y=Y6) curr_idx = 2 else - @debug("STEP 1: [Core Boundary] Mushy | Indices: (1, 2)") - C1l, D2l = core_boundary_mush(R8_view, (1, 2), rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, new_α, ηₗs, new_ϕ, ks, ρ_core/ρ0, core, n; G0=G0, Y=Y8) + @debug("STEP 1: [Core Boundary] Mushy") + C1l, D2l = core_boundary_mush(R8_view, (1, 2), rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, α, ηₗs, ϕ, ks, ρ_core/ρ0, μ_core/μ0, κ_core/μ0, core, n; G0=G0, Y=Y8) curr_idx = 2 end - # Step 2: Propagation and Jumps + # Step 2: Propagation and Transitions while curr_idx < Nr - next_change = findnext(x -> x != new_is_mush[curr_idx], new_is_mush, curr_idx) + # Find how long the current material state lasts + next_change = findnext(x -> x != is_mush[curr_idx], is_mush, curr_idx) segment_end = (next_change === nothing) ? Nr : next_change - 1 - # Safety check for empty ranges + # Propagate through the uniform segment if segment_end > curr_idx - if !new_is_mush[curr_idx] + if !is_mush[curr_idx] @debug("STEP: [Propagate Solid] | Range: ($curr_idx, $segment_end)") C1l, D2l = propagate_solid(R6_view, C1l, D2l, (curr_idx, segment_end-1), rs, ρs, gs, μs, Ks, ωs, n; G0=G0, Y=Y6) else @debug("STEP: [Propagate Mushy] | Range: ($curr_idx, $segment_end)") C1l, D2l = propagate_mush(R8_view, C1l, D2l, (curr_idx, segment_end-1), - rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, new_α, ηₗs, new_ϕ, ks, n; G0=G0, Y=Y8) + rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, α, ηₗs, ϕ, ks, n; G0=G0, Y=Y8) end end + # Handle the material interface if one exists if next_change !== nothing - # The jump occurs at the duplicated node - trans_range = (next_change - 1, next_change + 1) - if !new_is_mush[curr_idx] && new_is_mush[next_change] - @debug("STEP: [Interface Jump] Solid -> Mushy | Range: $trans_range") - C1l, D2l = interface_solid_mush(R8_view, C1l, D2l, trans_range; Y=Y8) + # The interface function now operates directly between segment_end and next_change + trans_range = (segment_end, next_change) + + if !is_mush[segment_end] && is_mush[next_change] + @debug("STEP: [Interface] Solid -> Mushy | Indices: $trans_range") + C1l, D2l = interface_solid_mush(R8_view, C1l, D2l, trans_range, + rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, α, ηₗs, ϕ, ks, n; G0=G0, Y=Y8) else - @debug("STEP: [Interface Jump] Mushy -> Solid | Range: $trans_range") - C1l, D2l = interface_mush_solid(R8_view, C1l, D2l, trans_range; Y=Y8) + @debug("STEP: [Interface] Mushy -> Solid | Indices: $trans_range") + C1l, D2l = interface_mush_solid(R8_view, C1l, D2l, trans_range, + rs, ρs, gs, μs, Ks, ωs, ρₗs, Kls, Kds, α, ηₗs, ϕ, ks, n; G0=G0, Y=Y8) end - curr_idx = next_change + 1 + curr_idx = next_change else curr_idx = Nr end end - # Step 3: Surface - if !new_is_mush[Nr] - @debug("STEP: [Surface Boundary] Solid | Indices: ($(Nr-1), $Nr)") + # Step 3: Surface Boundary + if !is_mush[Nr] + @debug("STEP: [Surface Boundary] Solid") y_t, y_l = surface_boundary(R6_view, C1l, D2l, (Nr-1, Nr), rs, ρs, gs, μs, Ks, ωs, n; G0=G0, Y=Y6) else - @debug("STEP: [Surface Boundary] Mushy | Indices: ($(Nr-1), $Nr)") + @debug("STEP: [Surface Boundary] Mushy") y_t, y_l = surface_boundary_mush(R8_view, C1l, D2l, (Nr-1, Nr), rs, ρs, gs, μs, Ks, ωs, n; G0=G0, Y=Y8) end + @debug("--- Solver Complete ---\n") - return y_t, y_l, R, S, transitions + # Note: transitions returned here are based on the original grid for plotting/analysis + return y_t, y_l, R, Y8, findall(diff(is_mush) .!= 0) end @@ -401,18 +282,33 @@ module solid1d_mush_relax - `Cn_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Cn matrix for the next iteration. - `Dnp_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. """ - function interface_mush_solid(R8, Cn_l, Dnp_l, ids; Y=[1,2,3,4,5,6,7,8]) + function interface_mush_solid(R, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, Y=[1,2,3,4,5,6,7,8]) start_id, end_id = ids + i = start_id + + target_cols = [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]] - I8 = Matrix{precc}(I, 8, 8) + I8 = Matrix{precc}(I, 8, 8) Cn = I8 Dnp = -I8 - Dnp[4, 4] = 0.0 - Dnp[8, 8] = 0.0 + Dnp[Y[7], Y[7]] = 0.0 + Dnp[Y[8], Y[8]] = 0.0 + + # forward recursion + dr = r[i+1] - r[i] + + # Calculate A at current and next step + A_n = get_A(ω, r[i], ρ[i], g[i], μ[i], K[i], + ρₗ[i], Kl[i], Kd[i], α[i], ηₗ[i], ϕ[i], k[i], n; G0=G0, Y=Y) + + A_np = get_A(ω, r[i+1], ρ[i+1], g[i+1], μ[i+1], K[i+1], + ρₗ[i+1], Kl[i+1], Kd[i+1], α[i+1], ηₗ[i+1], ϕ[i+1], k[i+1], n; G0=G0, Y=Y) + + Cn .+= 0.5 * dr * A_n + Dnp .+= 0.5 * dr * A_np - target_cols = [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]] # 1. Use the "stored" lower halves from the previous step # to fill the upper blocks of P and S. Pn_u = Cn_l @@ -423,24 +319,30 @@ module solid1d_mush_relax Cn_u = Cn[1:4, :] Dnp_u = Dnp[1:4, :] - # 3. Build the 8x8 blocks + # 3. Build the 6x6 blocks Pn = [Pn_u; zeros(precc, 4, 8)] Sn = [Sn_u; Cn_u] Qn = [Qn_u; Dnp_u] - Kn = zeros(precc, 8, 8) - Kn[8, 8] = 1.0 + Kn[Y[4], Y[4]] = 1.0 # 4. Perform recursion - Xn = Pn * R8[start_id] + Sn + Kn + Xn = Pn * R[i-1] + Sn + Kn - R_ifc = - pinv(Xn) * Qn - - R8[start_id+1] .= R_ifc + if i == start_id + # For the first step into the mush, we may need to use pinv if the system is not yet fully constrained by the solid boundary conditions. + R[i] .= -pinv(Xn) * Qn + else + R[i] .= -Xn \ Qn + end # 5. Update the "stored" lower halves for the next iteration - Cn_l = Cn[5:7, target_cols] - Dnp_l = Dnp[5:7, target_cols] + Cn_l = Cn[5:8, :] + Dnp_l = Dnp[5:8, :] + + # return as 3x6 for the next step + Cn_l = Cn_l[1:3, target_cols] + Dnp_l = Dnp_l[1:3, target_cols] return Cn_l, Dnp_l end @@ -465,56 +367,75 @@ module solid1d_mush_relax - `Cn_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Cn matrix for the next iteration. - `Dnp_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. """ - function interface_solid_mush(R, Cn_l, Dnp_l, ids; Y=[1,2,3,4,5,6,7,8]) + function interface_solid_mush(R, Cn_l, Dnp_l, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n; G0=1, Y=[1,2,3,4,5,6,7,8]) start_id, end_id = ids + i = start_id # target_cols target_cols = [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]] + I8 = Matrix{precc}(I, 8, 8) + + Cn = I8 + Cn[Y[7], Y[7]] = 0.0 + Cn[Y[8], Y[8]] = 0.0 + Dnp = -I8 + + # forward recursion + dr = r[i+1] - r[i] + + # Calculate A at current and next step + A_n = get_A(ω, r[i], ρ[i], g[i], μ[i], K[i], + ρₗ[i], Kl[i], Kd[i], α[i], ηₗ[i], ϕ[i], k[i], n; G0=G0, Y=Y) + + A_np = get_A(ω, r[i+1], ρ[i+1], g[i+1], μ[i+1], K[i+1], + ρₗ[i+1], Kl[i+1], Kd[i+1], α[i+1], ηₗ[i+1], ϕ[i+1], k[i+1], n; G0=G0, Y=Y) + + Cn .+= 0.5 * dr * A_n + Dnp .+= 0.5 * dr * A_np + + # 1. Use the "stored" lower halves from the previous step + # to fill the upper blocks of P and S. # expand the incoming 3x6 Solid Lower blocks to 4x8 Pn_u = zeros(precc, 4, 8) Pn_u[1:3, target_cols] .= Cn_l Sn_u = zeros(precc, 4, 8) Sn_u[1:3, target_cols] .= Dnp_l - - # current Layer (Porous side) - # treat the interface as an infinitesimal jump where C = I, D = -I - I8 = Matrix{precc}(I, 8, 8) - Cn_curr = I8 - Cn_curr[4, 4] = 0.0 - Cn_curr[8, 8] = 0.0 - Dnp_curr = -I8 - - Cn_u = Cn_curr[1:4, :] - Dnp_u = Dnp_curr[1:4, :] - - # assemble 8x8 system + Qn_u = zeros(precc, 4, 8) + + # 2. Get the upper halves of the NEWLY calculated Cn and Dnp + Cn_u = Cn[1:4, :] + Dnp_u = Dnp[1:4, :] + + # 3. Build the 6x6 blocks Pn = [Pn_u; zeros(precc, 4, 8)] Sn = [Sn_u; Cn_u] - Qn = [zeros(precc, 4, 8); Dnp_u] - - # introduce some pore pressure at the boundary to drive the solution in the mushy layer + Qn = [Qn_u; Dnp_u] Kn = zeros(precc, 8, 8) - Kn[8, 8] = 1.0 + Kn[Y[8], Y[8]] = 1.0 - # solve the jump - Xn = Pn * R[start_id] + Sn + Kn - R_ifc = - pinv(Xn) * Qn + # 4. Perform recursion + Xn = Pn * R[i-1] + Sn + Kn - R[start_id+1] .= R_ifc - - # pass the Lower halves of the Porous Identity to the next propagator - Cn_l_new = Cn_curr[5:8, :] - Dnp_l_new = Dnp_curr[5:8, :] - - return Cn_l_new, Dnp_l_new + if i == start_id + # For the first step into the mush, we may need to use pinv if the system is not yet fully constrained by the solid boundary conditions. + R[i] .= -pinv(Xn) * Qn + else + R[i] .= -Xn \ Qn + end + + # 5. Update the "stored" lower halves for the next iteration + Cn_l = Cn[5:8, :] + Dnp_l = Dnp[5:8, :] + + return Cn_l, Dnp_l end """ - core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n; G0=1, Y=[1,2,3,4,5,6]) + core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, μ_core, κ_core, core, n; G0=1, Y=[1,2,3,4,5,6]) Perform the forward-backward relaxation step at the core boundary. This function implements the recursion described in N. Kobayashi (2007) for the initial step of the relaxation scheme, where we apply the core boundary condition and @@ -530,6 +451,8 @@ module solid1d_mush_relax - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. - `ω::prec` : Angular frequency of the tidal forcing. - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `μ_core::prec` : Complex shear modulus of the core, used for core boundary conditions. + - `κ_core::prec` : Complex bulk modulus of the core, used for core boundary conditions. - `core::String` : Type of core boundary condition to apply. - `n::Int` : Tidal degree. @@ -541,12 +464,12 @@ module solid1d_mush_relax - `C1l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the C1 matrix for the next iteration. - `D2l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the D2 matrix for the next iteration. """ - function core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n; G0=1, Y=[1,2,3,4,5,6]) + function core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, μ_core, κ_core, core, n; G0=1, Y=[1,2,3,4,5,6]) start_id, end_id = ids # boundary conditions - B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ[start_id], K[start_id], core, n; G0=G0, M=6, N=3) + B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ_core, κ_core, core, n; G0=G0, Y=Y) # first layer (n = 1) dr = r[end_id] - r[start_id] @@ -575,7 +498,7 @@ module solid1d_mush_relax """ - core_boundary_mush(R, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, ρ_core, core, n; G0=1, Y=[1,2,3,4,5,6,7,8]) + core_boundary_mush(R, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, ρ_core, μ_core, κ_core, core, n; G0=1, Y=[1,2,3,4,5,6,7,8]) Perform the forward-backward relaxation step at the core boundary for the two-phase problem. This function implements the recursion described in N. Kobayashi (2007) for the initial step of the relaxation scheme, where we apply the core @@ -591,14 +514,16 @@ module solid1d_mush_relax - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. - `ω::prec` : Angular frequency of the tidal forcing. - - `ρₗ::Vector{prec}` : Vector of liquid densities at the layer centers. + - `ρₗ::Vector{prec}` : Vector of liquid densities at the layer centers. - `Kl::Vector{prec}` : Vector of liquid bulk moduli at the layer centers. - `Kd::Vector{prec}` : Vector of drained bulk moduli at the layer centers. - `α::Vector{prec}` : Vector of Biot coefficients at the layer centers. - - `ηₗ::Vector{prec}` : Vector of liquid viscosities at the layer centers. + - `ηₗ::Vector{prec}` : Vector of liquid viscosities at the layer centers. - `ϕ::Vector{prec}` : Vector of porosities at the layer centers. - `k::Vector{prec}` : Vector of permeabilities at the layer centers. - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `μ_core::prec` : Complex shear modulus of the core, used for core boundary conditions. + - `κ_core::prec` : Complex bulk modulus of the core, used for core boundary conditions. - `core::String` : Type of core boundary condition to apply. - `n::Int` : Tidal degree. @@ -609,12 +534,12 @@ module solid1d_mush_relax - `C1l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the C1 matrix for the next iteration. - `D2l::Matrix{precc}` : 4x8 matrix representing the "stored" lower half of the D2 matrix for the next iteration. """ - function core_boundary_mush(R, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, ρ_core, core, n; G0=1, Y=[1,2,3,4,5,6,7,8]) + function core_boundary_mush(R, ids, r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, ρ_core, μ_core, κ_core, core, n; G0=1, Y=[1,2,3,4,5,6,7,8]) start_id, end_id = ids # boundary conditions - B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ[start_id], K[start_id], core, n; G0=G0, Y=Y) + B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ_core, κ_core, core, n; G0=G0, Y=Y) # first layer (n = 1) dr = r[end_id] - r[start_id] @@ -679,8 +604,8 @@ module solid1d_mush_relax I6 = Matrix{precc}(I, 6, 6) - Cn_u = zeros(3,6) - Dnp_u = zeros(3,6) + # Cn_u = zeros(3,6) + # Dnp_u = zeros(3,6) # forward recursion for i in start_id:end_id @@ -770,8 +695,8 @@ module solid1d_mush_relax I8 = Matrix{precc}(I, 8, 8) - Cn_u = zeros(4,8) - Dnp_u = zeros(4,8) + # Cn_u = zeros(4,8) + # Dnp_u = zeros(4,8) # forward recursion for i in start_id:end_id @@ -1083,7 +1008,7 @@ module solid1d_mush_relax """ - compute_y(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core; core="liquid") + compute_y(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, μ_core, κ_core, M_tot; core="liquid") Compute the solution `y` to the solid-body problem using a relaxation method. This function performs the forward-backward relaxation scheme described in the main text of N. Kobayashi (2006), where we first solve @@ -1097,15 +1022,17 @@ module solid1d_mush_relax - `μ::Vector{prec}` : Vector of shear moduli at the layer centers. - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. - `ω::prec` : Angular frequency of the tidal forcing. - - `ρₗ::Vector{prec}` : Vector of liquid densities at the layer centers. + - `ρₗ::Vector{prec}` : Vector of liquid densities at the layer centers. - `Kl::Vector{prec}` : Vector of liquid bulk moduli at the layer centers. - `Kd::Vector{prec}` : Vector of drained bulk moduli at the layer centers. - `α::Vector{prec}` : Vector of Biot coefficients at the layer centers. - - `ηₗ::Vector{prec}` : Vector of liquid viscosities at the layer centers. + - `ηₗ::Vector{prec}` : Vector of liquid viscosities at the layer centers. - `ϕ::Vector{prec}` : Vector of porosities at the layer centers. - `k::Vector{prec}` : Vector of permeabilities at the layer centers. - `n::Int` : Tidal degree. - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `μ_core::precc` : Complex shear modulus of the core, used for core boundary conditions. + - `κ_core::precc` : Complex bulk modulus of the core, used for core boundary conditions. - `M_tot::prec` : Total mass of the planet. # Keyword Arguments @@ -1114,12 +1041,12 @@ module solid1d_mush_relax # Returns - `y::Matrix{precc}` : 6xN matrix of the solution at all radial grid points, where N is the number of radial layers. Each column corresponds to a radial grid point, and each row corresponds to a state variable (displacements, stresses, potential). """ - function compute_y(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, M_tot; core="liquid") + function compute_y(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, μ_core, κ_core, M_tot; core="liquid") # solve radial system to get surface solution and recursion matrices - yN_t, yN_l, R, S, transitions = solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, M_tot; core=core) + yN_t, yN_l, R, Y8, transitions = solve_radial_system(r, ρ, g, μ, K, ω, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, μ_core, κ_core, M_tot; core=core) - Nr = length(r) + length(transitions) + Nr = length(r) T = eltype(yN_t) M = length(yN_t) @@ -1138,26 +1065,16 @@ module solid1d_mush_relax y_t[:, i] = R[i] * y_t[:, i+1] end - # keep only indices NOT in the transition list - mask = filter(i -> !(i in transitions), 1:size(y_t, 2)) - - # Apply the mask to the columns - y_t = y_t[:, mask] - - # apply scaling to get dimensional solution - # for i in 1:Nr-2 - for i in 1:Nr-length(transitions) - y_t[:, i] = S * y_t[:, i] - end - - y_l .= S * y_l + # reorder y-functions to standard ordering (U, V, X, Y, phi, psi, P, R) + y_t = y_t[Y8, :] + y_l = y_l[Y8, :] return y_t, y_l end """ - compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr) + compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr, SphericalGrid) Calculate the strain tensor ϵ at a particular radial level. @@ -1178,21 +1095,23 @@ module solid1d_mush_relax - `ηlr::prec` : Liquid viscosity at radius rr. - `ϕr::prec` : Porosity at radius rr. - `kr::prec` : Permeability at radius rr. + - `SphericalGrid` : A struct containing the spherical grid information (Y, dYdθ, dYdϕ) for the current radial level. """ - function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr) + function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr, SphericalGrid) i = 1 - @views Y = solid1d_mush_relax.Y[i,:,:] - @views dYdθ = solid1d_mush_relax.dYdθ[i,:,:] - @views dYdϕ = solid1d_mush_relax.dYdϕ[i,:,:] - @views Z = solid1d_mush_relax.Z[i,:,:] - @views X = solid1d_mush_relax.X[i,:,:] + @views clats = SphericalGrid.clats + @views Y = SphericalGrid.Y[i,:,:] + @views dYdθ = SphericalGrid.dYdθ[i,:,:] + @views dYdϕ = SphericalGrid.dYdϕ[i,:,:] + @views Z = SphericalGrid.Z[i,:,:] + @views X = SphericalGrid.X[i,:,:] y1 = y[1] y2 = y[2] - y3 = y[5] - y4 = y[6] - y7 = y[4] + y3 = y[3] + y4 = y[4] + y7 = y[7] λr = Kdr - 2μr/3 βr = λr + 2μr @@ -1208,7 +1127,7 @@ module solid1d_mush_relax """ - compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl) + compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl, SphericalGrid) Calculate the Darcy displacement vector at a particular radial level. @@ -1223,17 +1142,19 @@ module solid1d_mush_relax - `k::prec` : Permeability at radius r. - `g::prec` : Gravity at radius r. - `ρl::prec` : Liquid density at radius r. + - `SphericalGrid` : A struct containing the spherical grid information (Y, dYdθ, dYdϕ) for the current radial level. """ - function compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl) + function compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl, SphericalGrid) i = 1 - @views Y = solid1d_mush_relax.Y[i,:,:] - @views dYdθ = solid1d_mush_relax.dYdθ[i,:,:] - @views dYdϕ = solid1d_mush_relax.dYdϕ[i,:,:] + @views clats = SphericalGrid.clats + @views Y = SphericalGrid.Y[i,:,:] + @views dYdθ = SphericalGrid.dYdθ[i,:,:] + @views dYdϕ = SphericalGrid.dYdϕ[i,:,:] y1 = y[1] - y5 = y[3] - y7 = y[4] + y5 = y[5] + y7 = y[7] y8 = y[8] y9 = 1im * k / (ω*ϕ*ηl*r) * (ρl*g*y1 - ρl * y5 + ρl*g*y8 + y7) @@ -1244,7 +1165,7 @@ module solid1d_mush_relax """ - compute_pore_pressure!(p, y, n) + compute_pore_pressure!(p, y, n, SphericalGrid) Calculate the fluid pore pressur at a particular radial level. @@ -1252,117 +1173,18 @@ module solid1d_mush_relax - `p::Array{ComplexF64,4}` : 4D array to store the pore pressure at a particular radial level, with dimensions corresponding to latitude and longitude. - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 8 components. - `n::Int` : Tidal degree. + - `SphericalGrid` : A struct containing the spherical grid information (Y, dYdθ, dYdϕ) for the current radial level. """ - function compute_pore_pressure!(p, y, n) + function compute_pore_pressure!(p, y, n, SphericalGrid) i = 1 - @views Y = solid1d_mush_relax.Y[i,:,:] - @views dYdθ = solid1d_mush_relax.dYdθ[i,:,:] - @views dYdϕ = solid1d_mush_relax.dYdϕ[i,:,:] + @views Y = SphericalGrid.Y[i,:,:] + @views dYdθ = SphericalGrid.dYdθ[i,:,:] + @views dYdϕ = SphericalGrid.dYdϕ[i,:,:] - y7 = y[4] + y7 = y[7] p[:,:] = Y * y7 end - - """ - get_heating_profile(y, r, ρ, g, μ, Ks, ω, ρl, Kl, Kd, α, ηl, ϕ, k, n; lay=nothing) - - Get the radial volumetric heating for two-phase tides and eccentricity forcing, - assuming synchronous rotation. Heating rate is computed with numerical integration - using the solution `y`, using Eq. 2.39a/b/c integrated over solid angle. - - # Arguments - - `y::Array{ComplexF64,2}` : 2D array [state_vector, radius] of the solution vector. - - `r::AbstractVector` : 1D vector of radial boundaries/shell coordinates. - - `ρ::AbstractVector` : 1D vector of layer densities. - - `g::AbstractVector` : 1D vector of gravity values. - - `μ::AbstractVector` : 1D vector of complex shear moduli. - - `Ks::AbstractVector` : 1D vector of bulk moduli for shear dissipation. - - `ω::Float64` : Tidal frequency in radians per second. - - `ρl::AbstractVector` : 1D vector of liquid densities. - - `Kl::AbstractVector` : 1D vector of liquid bulk moduli. - - `Kd::AbstractVector` : 1D vector of drained bulk moduli. - - `α::AbstractVector` : 1D vector of Biot coefficients. - - `ηl::AbstractVector` : 1D vector of liquid viscosities. - - `ϕ::AbstractVector` : 1D vector of porosities. - - `k::AbstractVector` : 1D vector of permeabilities. - - `n::Int` : Tidal degree. - - # Returns - - `Eμ_tot::Vector{Float64}` : Total power dissipated due to shear (W/m³). - - `Eκ_tot::Vector{Float64}` : Total power dissipated due to compaction (W/m³). - - `El_tot::Vector{Float64}` : Total power dissipated due to Darcy flow (W/m³). - """ - function get_heating_profile(y, r, ρ, g, μ, Ks, ω, ρl, Kl, Kd, α, ηl, ϕ, k, n) - - dres = deg2rad(solid1d_mush_relax.res) - clats = solid1d_mush_relax.clats - lons = solid1d_mush_relax.lons - - # Reference spherical harmonic Y if needed globally - # (Assuming the logic uses the same spatial resolution as the first example) - Y = solid1d_mush_relax.Y[1, :, :] - - Nr = length(r) - nlats = length(clats) - nlons = length(lons) - - # Pre-allocate spatial buffers - ϵ = zeros(ComplexF64, nlats, nlons, 6) - d_disp = zeros(ComplexF64, nlats, nlons, 3) - p = zeros(ComplexF64, nlats, nlons) - - # Output vectors (per radial shell) - Eμ_tot = zeros(Float64, Nr - 1) - Eκ_tot = zeros(Float64, Nr - 1) - El_tot = zeros(Float64, Nr - 1) - - for i in 1:Nr-1 - rr = r[i] - dr = r[i+1] - r[i] - dvol = 4π/3 * (r[i+1]^3 - r[i]^3) - yrr = y[:, i] - - # 1. Compute Tensors/Fields - compute_strain_ten!(ϵ, yrr, n, rr, ρ[i], g[i], μ[i], Ks[i], ω, ρl[i], Kl[i], Kd[i], α[i], ηl[i], ϕ[i], k[i]) - - if ϕ[i] > 0 - compute_darcy_displacement!(d_disp, yrr, n, rr, ω, ϕ[i], ηl[i], k[i], g[i], ρl[i]) - # In your snippet, p was being overwritten by y7 * Y immediately - p .= yrr[7] .* Y - else - p .= 0.0 - end - - # 2. Shear Heating (Solid) - Eμ_loc = sum(abs.(ϵ[:,:,1:3]).^2, dims=3) .+ - 2sum(abs.(ϵ[:,:,4:6]).^2, dims=3) .- - 1/3 .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 - Eμ_loc .*= ω * imag(μ[i]) - - # 3. Compaction Heating (Bulk) - Eκ_loc = ω/2 * imag(Kd[i]) .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 - if ϕ[i] > 0 - Eκ_loc .+= (ω/2 * imag(Kd[i]) .* (abs.(p) ./ Ks[i]).^2) - end - - # 4. Darcy Dissipation (Liquid) - El_loc = zeros(nlats, nlons) - if ϕ[i] > 0 - El_loc .= 0.5 * ϕ[i]^2 * ω^2 * (ηl[i] / k[i]) .* (abs.(d_disp[:,:,1]).^2 .+ abs.(d_disp[:,:,2]).^2 .+ abs.(d_disp[:,:,3]).^2) - end - - # 5. Angular Integration and Volume Averaging - weight = sin.(clats) - - Eμ_tot[i] = sum(weight .* Eμ_loc .* dres^2) * rr^2 * dr / dvol - Eκ_tot[i] = sum(weight .* Eκ_loc .* dres^2) * rr^2 * dr / dvol - El_tot[i] = sum(weight .* El_loc .* dres^2) * rr^2 * dr / dvol - end - - return Eμ_tot, Eκ_tot, El_tot - end - end \ No newline at end of file diff --git a/src/solid1d_relax.jl b/src/solid1d_relax.jl index 1f17710..0c50d7a 100644 --- a/src/solid1d_relax.jl +++ b/src/solid1d_relax.jl @@ -17,14 +17,14 @@ module solid1d_relax const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 - clats = 0.0 - lons = 0.0 - Y = 0.0 - dYdθ = 0.0 - dYdϕ = 0.0 - Z = 0.0 - X = 0.0 - res = 20.0 + # clats = 0.0 + # lons = 0.0 + # Y = 0.0 + # dYdθ = 0.0 + # dYdϕ = 0.0 + # Z = 0.0 + # X = 0.0 + # res = 20.0 """ @@ -127,105 +127,6 @@ module solid1d_relax end - """ - Ynm(n, m, theta, phi) - - Compute the spherical harmonic Ynm for given n, m, theta, and phi. - - # Arguments - - `n::Int` : Tidal degree. - - `m::Int` : Tidal order. - - `theta::Array{Float64,1}` : Array of colatitudes in radians. - - `phi::Array{Float64,1}` : Array of longitudes in radians. - - # Returns - - `Ynm::Array{ComplexF64,2}` : 2D array of spherical harmonic values for each combination of theta and phi. - """ - function Ynm(n, m, theta, phi) - return Plm.(n, m, cos.(theta)) .* exp.(1im * m .* phi) - end - - - """ - define_spherical_grid(res) - - Create the spherical grid of angular resolution `res` in degrees. This is used for - numerical integrations over solid angle. A new grid can easily be defined by - recalling the function with a new `res`. - - # Arguments - - `res::Float64` : Desired angular resolution in degrees. - - `n::Int` : Tidal degree. - - `m::Int` : Tidal order. - - # Notes - The grid is internal to solid1d_relax, but can be accessed with - - solid1d_relax.clats[:] # colatitude grid - solid1d_relax.lons[:] # longitude grid - """ - function define_spherical_grid(res, n, m) - solid1d_relax.res = res - - # θ and φ grids - lons = deg2rad.(collect(0:res:360-0.001))' - clats = deg2rad.(collect(0:res:180)) - clats[1] += 1e-6 - clats[end] -= 1e-6 - - # allocate arrays - solid1d_relax.Y = zeros(ComplexF64, 1, length(clats), length(lons)) - solid1d_relax.dYdθ = similar(solid1d_relax.Y) - solid1d_relax.dYdϕ = similar(solid1d_relax.Y) - solid1d_relax.Z = similar(solid1d_relax.Y) - solid1d_relax.X = similar(solid1d_relax.Y) - - sinθ = sin.(clats) - cosθ = cos.(clats) - cotθ = cosθ ./ sinθ - cscθ = csc.(clats) - - # Normalization factor for spherical harmonics - norm = sqrt((2*n+1) * factorial(n-m) / (4π * factorial(n+m))) - - i = 1 - - # Y - solid1d_relax.Y[i,:,:] = Ynm(n,m,clats,lons) - - # ∂Y/∂θ - Pn = Plm.(n, m, cosθ) - if n > m - Pn_1 = Plm.(n-1, m, cosθ) - dPdθ = (n .* cosθ .* Pn .- (n + m) .* Pn_1) ./ (sinθ) - else - # m == n -> P_{n-1}^m = 0 - dPdθ = (n .* cosθ .* Pn) ./ (sinθ) - end - solid1d_relax.dYdθ[i,:,:] .= dPdθ .* exp.(1im .* m .* lons) - - # ∂Y/∂ϕ - solid1d_relax.dYdϕ[i,:,:] .= 1im * m .* solid1d_relax.Y[i,:,:] - - # Z = 2 ((1/sinθ) ∂²Y/∂θ∂ϕ - cotθ cscθ ∂Y/∂ϕ) - solid1d_relax.Z[i,:,:] .= 2 .* (1im * m ./ sinθ .* solid1d_relax.dYdθ[i,:,:] .- cotθ .* cscθ .* solid1d_relax.dYdϕ[i,:,:]) - - # X = -2 (cotθ ∂Y/∂θ + csc²θ ∂²Y/∂ϕ²) - n(n+1)) Y - solid1d_relax.X[i,:,:] .= -2 .* (cotθ .* solid1d_relax.dYdθ[i,:,:] .- cscθ.^2 .* m^2 .* solid1d_relax.Y[i,:,:]) .- n*(n+1) .* solid1d_relax.Y[i,:,:] - - # Normalize - solid1d_relax.Y[i,:,:] .*= norm - solid1d_relax.dYdθ[i,:,:] .*= norm - solid1d_relax.dYdϕ[i,:,:] .*= norm - solid1d_relax.Z[i,:,:] .*= norm - solid1d_relax.X[i,:,:] .*= norm - - # save grids - solid1d_relax.clats = clats - solid1d_relax.lons = lons - end - - """ doublefactorial(n) @@ -251,7 +152,7 @@ module solid1d_relax """ - solve_radial_system(r, ρ, g, μ, K, ω, n, R_planet, ρ_core; core="liquid") + solve_radial_system(r, ρ, g, μ, K, ω, n, R_planet, ρ_core, μ_core, κ_core, M_tot; core="liquid") Solve the radial system of ODEs for the solid-body problem using a relaxation method. This function implements the forward-backward relaxation scheme described in the main text of N. Kobayashi (2006). @@ -265,6 +166,8 @@ module solid1d_relax - `ω::prec` : Angular frequency of the tidal forcing. - `n::Int` : Tidal degree. - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `μ_core::prec` : Shear modulus of the core, used for core boundary conditions. + - `κ_core::prec` : Bulk modulus of the core, used for core boundary conditions. - `M_tot::prec` : Total mass of the planet, used for gravity calculations. # Keyword Arguments @@ -276,7 +179,7 @@ module solid1d_relax - `R::Vector{Matrix{precc}}` : Vector of 6x6 matrices representing the coefficients of the ODE system at each radial layer. - `S::Vector{Matrix{precc}}` : Vector of 6x6 matrices representing the normalization. """ - function solve_radial_system(r, ρ, g, μ, K, ω, n, ρ_core, M_tot; core="liquid") + function solve_radial_system(r, ρ, g, μ, K, ω, n, ρ_core, μ_core, κ_core, M_tot; core="liquid") Nr = length(r) @@ -301,7 +204,7 @@ module solid1d_relax R = Vector{Matrix{precc}}(undef, Nr) # component 1: apply core boundary condition and get first solution - C1l, D2l = core_boundary(R, ids[1], rs, ρs, gs, μs, Ks, ωs, ρ_core/ρ0, core, n; G0=G0) + C1l, D2l = core_boundary(R, ids[1], rs, ρs, gs, μs, Ks, ωs, ρ_core/ρ0, μ_core/μ0, κ_core/μ0, core, n; G0=G0) # component 2: propagate the solution up to the surface (6x6) C1l, D2l = propagate_solid(R, C1l, D2l, ids[2], rs, ρs, gs, μs, Ks, ωs, n; G0=G0) @@ -315,7 +218,7 @@ module solid1d_relax """ - core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n) + core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, μ_core, κ_core, core, n) Perform the forward-backward relaxation step at the core boundary. This function implements the recursion described in N. Kobayashi (2007) for the initial step of the relaxation scheme, where we apply the core boundary condition and @@ -331,6 +234,8 @@ module solid1d_relax - `K::Vector{prec}` : Vector of bulk moduli at the layer centers. - `ω::prec` : Angular frequency of the tidal forcing. - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `μ_core::prec` : Shear modulus of the core, used for core boundary conditions. + - `κ_core::prec` : Bulk modulus of the core, used for core boundary conditions. - `core::String` : Type of core boundary condition to apply. - `n::Int` : Tidal degree. @@ -341,12 +246,12 @@ module solid1d_relax - `C1l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the C1 matrix for the next iteration. - `D2l::Matrix{precc}` : 3x6 matrix representing the "stored" lower half of the D2 matrix for the next iteration. """ - function core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, core, n; G0=1) + function core_boundary(R, ids, r, ρ, g, μ, K, ω, ρ_core, μ_core, κ_core, core, n; G0=1) start_id, end_id = ids # boundary conditions - B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ[start_id], K[start_id], core, n; G0=G0) + B1 = get_core_bc!(ω, r[start_id], ρ_core, g[start_id], μ_core, κ_core, core, n; G0=G0) # first layer (n = 1) dr = r[end_id] - r[start_id] @@ -619,7 +524,7 @@ module solid1d_relax """ - compute_y(r, ρ, g, μ, K, ω, n, R, ρ_core; core="liquid") + compute_y(r, ρ, g, μ, K, ω, n, R, ρ_core, μ_core, κ_core, M_tot; core="liquid") Compute the solution `y` to the solid-body problem using a relaxation method. This function performs the forward-backward relaxation scheme described in the main text of N. Kobayashi (2006), where we first solve @@ -635,6 +540,8 @@ module solid1d_relax - `ω::prec` : Angular frequency of the tidal forcing. - `n::Int` : Tidal degree. - `ρ_core::prec` : Density of the core, used for core boundary conditions. + - `μ_core::prec` : Shear modulus of the core, used for core boundary conditions. + - `κ_core::prec` : Bulk modulus of the core, used for core boundary conditions. - `M_tot::prec` : Total mass of the planet, used for non-dimensionalization. # Keyword Arguments @@ -643,10 +550,10 @@ module solid1d_relax # Returns - `y::Matrix{precc}` : 6xN matrix of the solution at all radial grid points, where N is the number of radial layers. Each column corresponds to a radial grid point, and each row corresponds to a state variable (displacements, stresses, potential). """ - function compute_y(r, ρ, g, μ, K, ω, n, ρ_core, M_tot; core="liquid") + function compute_y(r, ρ, g, μ, K, ω, n, ρ_core, μ_core, κ_core, M_tot; core="liquid") # solve radial system to get surface solution and recursion matrices - yN_t, yN_l, R, S = solve_radial_system(r, ρ, g, μ, K, ω, n, ρ_core, M_tot; core=core) + yN_t, yN_l, R, S = solve_radial_system(r, ρ, g, μ, K, ω, n, ρ_core, μ_core, κ_core, M_tot; core=core) Nr = length(r) T = eltype(yN_t) @@ -668,119 +575,4 @@ module solid1d_relax return y_t, y_l end - - """ - compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr) - - Calculate the strain tensor ϵ at a particular radial level. - - # Arguments - - `ϵ::Array{ComplexF64,3}` : 3D array to store the strain tensor at a particular radial level, with dimensions corresponding to latitude, longitude, and the 6 independent components of the strain tensor. - - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 6 components. - - `n::Int` : Tidal degree. - - `rr::prec` : Radius at which to compute the strain tensor. - - `ρr::prec` : Density at radius rr. - - `gr::prec` : Gravity at radius rr. - - `μr::prec` : Shear modulus at radius rr. - - `Ksr::prec` : Bulk modulus at radius rr. - """ - function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr) - - i = 1 - - @views Y = solid1d_relax.Y[i,:,:] - @views dYdθ = solid1d_relax.dYdθ[i,:,:] - @views dYdϕ = solid1d_relax.dYdϕ[i,:,:] - @views Z = solid1d_relax.Z[i,:,:] - @views X = solid1d_relax.X[i,:,:] - - y1 = y[1] - y2 = y[2] - y3 = y[3] - y4 = y[4] - - λr = Ksr .- 2μr/3 - βr = λr + 2μr - - # Compute strain tensor - ϵ[:,:,1] = (-2λr*y1 + n*(n+1)λr*y2 + rr*y3)/(βr*rr) * Y - ϵ[:,:,2] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y + 0.5y2*X) - ϵ[:,:,3] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y - 0.5y2*X) - ϵ[:,:,4] = 0.5/μr * y4 * dYdθ - ϵ[:,:,5] = 0.5/μr * y4 * dYdϕ .* 1.0 ./ sin.(clats) - ϵ[:,:,6] = 0.5 * y2/rr * Z - end - - - """ - function get_heating_profile(y, r, ρ, g, μ, κ, n, ω) - - Get the radial volumetric heating for solid-body tides and eccentricity forcing, - assuming synchronous rotation. Heating rate is computed with numerical integration - using the solution `y`, using Eq. 2.39a/b integrated over solid angle. - - # Arguments - - `y::Array{ComplexF64,6}` : 6D array of the solution vector y across the interior, returned by `compute_y`. - - `r::AbstractVector` : 1D vector of radial coordinates or shell boundaries. - - `ρ::AbstractVector` : 1D vector of densities at each radial shell. - - `g::AbstractVector` : 1D vector of gravitational acceleration values at each radial shell. - - `μ::AbstractVector` : 1D vector of complex shear moduli at each radial shell. - - `κ::AbstractVector` : 1D vector of complex bulk moduli at each radial shell. - - `n::Int` : Tidal degree. - - `ω` : Tidal frequency in radians per second. - - # Returns - - `Eμ_tot::Array{Float64,1}` : 1D array of total power dissipated in each radial shell due to shear, in W. - - `Eκ_tot::Array{Float64,1}` : 1D array of total power dissipated in each radial shell due to compaction, in W. - """ - function get_heating_profile(y, r, ρ, g, μ, κ, n, ω) - - dres = deg2rad(solid1d_relax.res) - - clats = solid1d_relax.clats - lons = solid1d_relax.lons - - Nr = length(r) - nlats = length(clats) - nlons = length(lons) - - # strain tensor per radius - ϵ = zeros(ComplexF64, nlats, nlons, 6) - - # outputs - Eμ_tot = zeros(Float64, Nr-1) - Eκ_tot = zeros(Float64, Nr-1) - - for i in 1:Nr-1 - - rr = r[i] - dr = r[i+1] - r[i] - - dvol = 4π/3 * (r[i+1]^3 - r[i]^3) - - yrr = y[:, i] - - compute_strain_ten!(ϵ, yrr, n, rr, ρ[i], g[i], μ[i], κ[i]) - - # shear heating - Eμ_loc = sum(abs.(ϵ[:,:,1:3]).^2, dims=3) .+ - 2sum(abs.(ϵ[:,:,4:6]).^2, dims=3) .- - 1/3 .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 - - Eμ_loc .*= ω * imag(μ[i]) - - # bulk heating - Eκ_loc = ω/2 * imag(κ[i]) .* abs.(sum(ϵ[:,:,1:3], dims=3)).^2 - - # angular integration - weight = sin.(clats) - - Eμ_tot[i] = sum(weight .* Eμ_loc * dres^2) * rr^2 * dr / dvol - Eκ_tot[i] = sum(weight .* Eκ_loc * dres^2) * rr^2 * dr / dvol - - end - - return Eμ_tot, Eκ_tot - end - end \ No newline at end of file From 5fbb18980e9018d33df91ee1455fa0642e79cafa Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 3 May 2026 22:15:41 +0000 Subject: [PATCH 19/36] Added safety to read file in Hansen.jl --- src/Hansen.jl | 51 ++++++++++++++++++++------------------------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/Hansen.jl b/src/Hansen.jl index d83e015..babe962 100644 --- a/src/Hansen.jl +++ b/src/Hansen.jl @@ -33,46 +33,35 @@ module Hansen - `k_max::Int` : Maximum k-index for Hansen coefficients. """ function get_k_range(e, n::Int, m::Int)::Tuple{Int, Int} - path = joinpath(RES_DIR, "hansen_k_table.nc") - ds = NCDataset(path, "r") - - n_vals = ds["n"][:] - m_vals = ds["m"][:] - ecc_vals = ds["ecc"][:] - kmin_vals = ds["k_min"][:] - kmax_vals = ds["k_max"][:] - - close(ds) - - # filter matching (n, m) - mask_nm = (n_vals .== n) .& (m_vals .== m) + # Use a do-block to ensure the file handle is closed automatically + k_min, k_max = NCDataset(path, "r") do ds + n_vals = ds["n"][:] + m_vals = ds["m"][:] + + # Filter matching (n, m) indices first to minimize work + mask_nm = (n_vals .== n) .& (m_vals .== m) - if !any(mask_nm) - error("No entries found for n=$n, m=$m in Hansen table.") - end + if !any(mask_nm) + error("No entries found for n=$n, m=$m in Hansen table.") + end - ecc_sub = ecc_vals[mask_nm] - kmin_sub = kmin_vals[mask_nm] - kmax_sub = kmax_vals[mask_nm] + # Pull sub-arrays only for valid (n, m) entries + ecc_sub = ds["ecc"][mask_nm] + kmin_sub = ds["k_min"][mask_nm] + kmax_sub = ds["k_max"][mask_nm] - # find smallest ecc >= e - # convert e to Float64 for comparison (table is Float64) - e_val = Float64(e) + # Find smallest eccentricity >= e + e_val = Float64(e) + valid = ecc_sub .>= e_val - valid = ecc_sub .>= e_val + idx_local = any(valid) ? findfirst(valid) : argmax(ecc_sub) - if any(valid) - idx_local = findfirst(valid) # assumes ecc_list was sorted ascending - else - # if e is larger than any tabulated value → fallback to largest available - idx_local = argmax(ecc_sub) + return Int(kmin_sub[idx_local]), Int(kmax_sub[idx_local]) end - k_max = kmax_sub[idx_local] - k_min = kmin_sub[idx_local] - + # Handle the specific m=0 case logic outside the block if m == 0 k_min = 0 end From e55752649bdd051c0b6cc23d1b918e7b54ba52fd Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 3 May 2026 22:16:10 +0000 Subject: [PATCH 20/36] Updated dependencies. --- Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Project.toml b/Project.toml index 63f9b04..6ad621c 100644 --- a/Project.toml +++ b/Project.toml @@ -31,7 +31,6 @@ LoggingExtras = "1.2.0" NCDatasets = "0.14.15" Plots = "1.41.2" Printf = "1.11.0" -PythonPlot = "1.0.6" SparseArrays = "1.11.0" SpecialFunctions = "2.7" Statistics = "1.11.1" From 5f5d2f20996e475167b45ab8cc38f269429aa5a7 Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 4 May 2026 19:17:44 +0000 Subject: [PATCH 21/36] Added fix. output is now internally consistent and matches closely with soli1d_relax, but shows interesting new features. --- src/solid1d_mush_relax.jl | 138 +++++--------------------------------- 1 file changed, 16 insertions(+), 122 deletions(-) diff --git a/src/solid1d_mush_relax.jl b/src/solid1d_mush_relax.jl index e54b466..6ef8b77 100644 --- a/src/solid1d_mush_relax.jl +++ b/src/solid1d_mush_relax.jl @@ -188,7 +188,7 @@ module solid1d_mush_relax ρₗs, Kls, Kds, ηₗs, ks = ρₗ./ρ0, Kl./μ0, Kd./μ0, ηₗ./(μ0*s0), k./R0^2 # 3. Initialize Matrices - R = [Matrix{precc}(I, 8, 8) for _ in 1:Nr] + R = [zeros(precc, 8, 8) for _ in 1:Nr] B = [zeros(precc, 8, 1) for _ in 1:Nr] idx_6 = [1, 2, 3, 5, 6, 7] R6_view = [view(R[i], idx_6, idx_6) for i in 1:Nr] @@ -379,7 +379,6 @@ module solid1d_mush_relax Cn = I8 Cn[Y[7], Y[7]] = 0.0 - Cn[Y[8], Y[8]] = 0.0 Dnp = -I8 # forward recursion @@ -414,18 +413,29 @@ module solid1d_mush_relax Sn = [Sn_u; Cn_u] Qn = [Qn_u; Dnp_u] Kn = zeros(precc, 8, 8) - Kn[Y[8], Y[8]] = 1.0 + Kn[Y[8], Y[8]] = -1.0 # 4. Perform recursion - Xn = Pn * R[i-1] + Sn + Kn + R_prev = R[i-1] + Xn = Pn * R_prev + Sn + Kn if i == start_id # For the first step into the mush, we may need to use pinv if the system is not yet fully constrained by the solid boundary conditions. - R[i] .= -pinv(Xn) * Qn + R_ifc = -pinv(Xn) * Qn else - R[i] .= -Xn \ Qn + R_ifc = -Xn \ Qn end + # this line makes it such that y8 is zero at the interface + # its exact derivation is a wip, but it produces the expected bahavior. + # More generally, it seems like for the mush, the lower bound has zeros on + # the rows of y7 and y8 in R, and for the upper bound the columns of y7 and y8 + # should be zero in R. The added K term is still required so this probably all + # has to do with the way the system is rearranged in the interface step, but + # its exact form is still being worked out. + R[i][1:7, 1:8] .= R_ifc[1:7, 1:8] # 7 and 8 here are only valid for the current Y ordering! (i.e. y8 at 8th position in order) + # Note the current implementation already sets the y7 row to zero. + # 5. Update the "stored" lower halves for the next iteration Cn_l = Cn[5:8, :] Dnp_l = Dnp[5:8, :] @@ -1061,7 +1071,6 @@ module solid1d_mush_relax # back-substitution for i in Nr-1:-1:1 - # if this is not a transition point, we perform the recursion step as normal y_t[:, i] = R[i] * y_t[:, i+1] end @@ -1072,119 +1081,4 @@ module solid1d_mush_relax return y_t, y_l end - - """ - compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr, SphericalGrid) - - Calculate the strain tensor ϵ at a particular radial level. - - # Arguments - - `ϵ::Array{ComplexF64,4}` : 4D array to store the strain tensor at a particular radial level, with dimensions corresponding to latitude, longitude, and the 6 independent components of the strain tensor. - - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 6 components. - - `n::Int` : Tidal degree. - - `rr::prec` : Radius at which to compute the strain tensor. - - `ρr::prec` : Density at radius rr. - - `gr::prec` : Gravity at radius rr. - - `μr::prec` : Shear modulus at radius rr. - - `Ksr::prec` : Bulk modulus at radius rr. - - `ω::prec` : Forcing frequency. - - `ρlr::prec` : Liquid density at radius rr. - - `Klr::prec` : Liquid bulk modulus at radius rr. - - `Kdr::prec` : Drained bulk modulus at radius rr. - - `αr::prec` : Biot coefficient at radius rr. - - `ηlr::prec` : Liquid viscosity at radius rr. - - `ϕr::prec` : Porosity at radius rr. - - `kr::prec` : Permeability at radius rr. - - `SphericalGrid` : A struct containing the spherical grid information (Y, dYdθ, dYdϕ) for the current radial level. - """ - function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr, SphericalGrid) - i = 1 - - @views clats = SphericalGrid.clats - @views Y = SphericalGrid.Y[i,:,:] - @views dYdθ = SphericalGrid.dYdθ[i,:,:] - @views dYdϕ = SphericalGrid.dYdϕ[i,:,:] - @views Z = SphericalGrid.Z[i,:,:] - @views X = SphericalGrid.X[i,:,:] - - y1 = y[1] - y2 = y[2] - y3 = y[3] - y4 = y[4] - y7 = y[7] - - λr = Kdr - 2μr/3 - βr = λr + 2μr - - # Compute strain tensor - ϵ[:,:,1] = (-2λr*y1 + n*(n+1)λr*y2 + rr*y3 + rr*αr*y7)/(βr*rr) * Y # e_rr - ϵ[:,:,2] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y + 0.5y2*X) # e_ - ϵ[:,:,3] = 1/rr * ((y1 - 0.5n*(n+1)y2)Y - 0.5y2*X) # e_ - ϵ[:,:,4] = 0.5/μr * y4 * dYdθ # e_rθ - ϵ[:,:,5] = 0.5/μr * y4 * dYdϕ .* 1.0 ./ sin.(clats) # e_rϕ - ϵ[:,:,6] = 0.5 * y2/rr * Z # e_ - end - - - """ - compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl, SphericalGrid) - - Calculate the Darcy displacement vector at a particular radial level. - - # Arguments - - `dis_rel::Array{ComplexF64,4}` : 4D array to store the Darcy displacement vector at a particular radial level, with dimensions corresponding to latitude, longitude, and the 3 components of the Darcy displacement vector. - - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 8 components. - - `n::Int` : Tidal degree. - - `r::prec` : Radius at which to compute the Darcy displacement vector. - - `ω::prec` : Forcing frequency. - - `ϕ::prec` : Porosity at radius r. - - `ηl::prec` : Liquid viscosity at radius r. - - `k::prec` : Permeability at radius r. - - `g::prec` : Gravity at radius r. - - `ρl::prec` : Liquid density at radius r. - - `SphericalGrid` : A struct containing the spherical grid information (Y, dYdθ, dYdϕ) for the current radial level. - """ - function compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl, SphericalGrid) - i = 1 - - @views clats = SphericalGrid.clats - @views Y = SphericalGrid.Y[i,:,:] - @views dYdθ = SphericalGrid.dYdθ[i,:,:] - @views dYdϕ = SphericalGrid.dYdϕ[i,:,:] - - y1 = y[1] - y5 = y[5] - y7 = y[7] - y8 = y[8] - y9 = 1im * k / (ω*ϕ*ηl*r) * (ρl*g*y1 - ρl * y5 + ρl*g*y8 + y7) - - dis_rel[:,:,1] = Y * y8 - dis_rel[:,:,2] = dYdθ * y9 - dis_rel[:,:,3] = dYdϕ * y9 ./ sin.(clats) - end - - - """ - compute_pore_pressure!(p, y, n, SphericalGrid) - - Calculate the fluid pore pressur at a particular radial level. - - # Arguments - - `p::Array{ComplexF64,4}` : 4D array to store the pore pressure at a particular radial level, with dimensions corresponding to latitude and longitude. - - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 8 components. - - `n::Int` : Tidal degree. - - `SphericalGrid` : A struct containing the spherical grid information (Y, dYdθ, dYdϕ) for the current radial level. - """ - function compute_pore_pressure!(p, y, n, SphericalGrid) - i = 1 - - @views Y = SphericalGrid.Y[i,:,:] - @views dYdθ = SphericalGrid.dYdθ[i,:,:] - @views dYdϕ = SphericalGrid.dYdϕ[i,:,:] - - y7 = y[7] - - p[:,:] = Y * y7 - end - end \ No newline at end of file From ae2cd7c4b26b5d05c057932fb59137772af485fc Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 4 May 2026 19:23:33 +0000 Subject: [PATCH 22/36] Added surface heatmap plot functionality. Left as commented out for now, should be made a config option. Can be properly included in plotting overhaul? --- src/Obliqua.jl | 47 +++++++++++++++++++++++-------------- src/plotting.jl | 62 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 29 deletions(-) diff --git a/src/Obliqua.jl b/src/Obliqua.jl index da681e5..19b446a 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -63,7 +63,7 @@ module Obliqua const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 const M_Earth::prec = prec(5.9724e24) # kg - const res::Float64 = 10.0 # angular resolution in degrees + const res::Float64 = 20.0 # angular resolution in degrees """ @@ -530,7 +530,8 @@ module Obliqua ) # elseif 1D interior and heating profile from strain tensor elseif module_solid=="solid1d-relax" - prf_seg[iss,:], map_seg[iss,:, :], kT, kL = run_solid1d_relax( + prf_seg[iss,:], kT, kL = run_solid1d_relax( + # prf_seg[iss,:], map_seg[iss,:, :], kT, kL = run_solid1d_relax( σ, ρ_seg, r_seg, η_seg, μc_seg[:, iss], κ_seg, R, @@ -551,7 +552,8 @@ module Obliqua permea=permea, porosity_thresh=porosity_thresh ) elseif module_solid=="solid1d-mush-relax" - prf_seg[iss,:], map_seg[iss,:, :], kT, kL = run_solid1d_mush_relax( + # prf_seg[iss,:], map_seg[iss,:, :], kT, kL = run_solid1d_mush_relax( + prf_seg[iss,:], kT, kL = run_solid1d_mush_relax( σ, ρ_seg, r_seg, η_seg, μc_seg[:, iss], κ_seg, ϕ_seg, R, @@ -1286,7 +1288,8 @@ module Obliqua n::Int=2, m::Int=2, core::String="liquid" - )::Tuple{Array{prec,1},Matrix{prec},precc,precc} + )::Tuple{Array{prec,1},precc,precc} + # )::Tuple{Array{prec,1},Matrix{prec},precc,precc} # convert inputs ρ = convert(Vector{prec}, rho) @@ -1308,8 +1311,6 @@ module Obliqua y_t, y_l = solid1d_relax.compute_y(r_centers, ρ, g, μ, κ, omega, n, ρ_core, μ_core, κ_core, M_tot; core=core) # for debugging: plot y-function relaxation solution - # in particular, observe the oscillating behavior near transition zones - # and also near the surface for high frequencies # plotting.plot_relaxation_solution(y_t, r_centers, # filename="$OUT_DIR/relaxation_solution.png") @@ -1337,7 +1338,8 @@ module Obliqua power_prf = itp.(r_orig_centers) - return power_prf, power_map, k2_T, k2_L + return power_prf, k2_T, k2_L + # return power_prf, power_map, k2_T, k2_L end @@ -1531,7 +1533,8 @@ module Obliqua bulk_l::Float64=1e9, permea::Float64=1e-7, porosity_thresh::Float64=1e-5 - )::Tuple{Array{prec,1},Matrix{prec},precc,precc} + )::Tuple{Array{prec,1},precc,precc} + # )::Tuple{Array{prec,1},Matrix{prec},precc,precc} # internal structure arrays. # first element is the innermost layer, last element is the outermost layer @@ -1550,13 +1553,13 @@ module Obliqua ηl = zeros(prec, length(r)) k = zeros(prec, length(r)) - # add small non-zero porosity to avoid numerical issues with zero porosity layers - ϕ[ϕ .< prec(porosity_thresh)] .= prec(10*porosity_thresh) - # find where the mush interface occurs (excluding the CMB layer) # get all indices above the threshold - ii_all = findall(ϕ .> porosity_thresh) + ii_all = findall(ϕ .>= porosity_thresh) + # set all porosity values below the threshold to zero (otherwise code cannot solve system) + ϕ[ϕ .< porosity_thresh] .= 0.0 + # update the liquid arrays κl .= prec(bulk_l) # liquid bulk modulus ηl .= prec(visc_l) # liquid viscosity @@ -1589,12 +1592,21 @@ module Obliqua y_t, r_grid, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, SphericalGrid ) - Eμ_map, Eκ_map, El_map = solid1d_mush_relax.get_heating_map( - y_t, r_grid, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, SphericalGrid - ) + # check if nan in heating profile, if so, set to zero + # warn user if nan in heating profile + if any(isnan.(Eμ_tot)) || any(isnan.(Eκ_tot)) || any(isnan.(El_tot)) + @info "NaN values found in heating profile, setting to zero. This can occur when the solution is unstable, e.g. for high frequencies or high porosities." + end + Eμ_tot[isnan.(Eμ_tot)] .= 0.0 + Eκ_tot[isnan.(Eκ_tot)] .= 0.0 + El_tot[isnan.(El_tot)] .= 0.0 + + # Eμ_map, Eκ_map, El_map = solid1d_mush_relax.get_heating_map( + # y_t, r_grid, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, SphericalGrid + # ) power_prf = abs.(Eμ_tot .+ Eκ_tot .+ El_tot) - power_map = abs.(Eμ_map .+ Eκ_map .+ El_map) + # power_map = abs.(Eμ_map .+ Eκ_map .+ El_map) # interpolate from grid back to original radius points itp = linear_interpolation(r_centers, power_prf, extrapolation_bc=Line()) @@ -1604,7 +1616,8 @@ module Obliqua power_prf = itp.(r_orig_centers) - return power_prf, power_map, k2_T, k2_L + return power_prf, k2_T, k2_L + # return power_prf, power_map, k2_T, k2_L end diff --git a/src/plotting.jl b/src/plotting.jl index 6156a60..678551c 100644 --- a/src/plotting.jl +++ b/src/plotting.jl @@ -327,6 +327,22 @@ module plotting end + """ + plot_surface_heating(H, res; filename="tidal_heating_map_surface.png", title_str="Surface heating map") + + Create and save a heatmap of the surface tidal heating as function of latitude and longitude. + + # Arguments + - `H::AbstractMatrix` : Surface tidal heating values for each lat-lon grid point. + - `res::Float64` : Angular resolution of the lat-lon grid in degrees. + + # Keyword Arguments + - `filename::String="tidal_heating_map_surface.png"` : Path to save the heatmap figure. + - `title_str::String="Surface heating map"` : Title for the heatmap figure. + + # Returns + - `plt` : The `Plots.Plot` object. + """ function plot_surface_heating(H, res; filename="tidal_heating_map_surface.png", title_str="Surface heating map") # convert to Float64 @@ -353,8 +369,23 @@ module plotting return plt end + + """ + plot_relaxation_solution(y, r; filename="relaxation_solution.png") + + Create and save a plot of the y-function relaxation solution as function of radius. + # Arguments + - `y::AbstractMatrix` : y-function values for each radius and function index. + - `r::AbstractVector` : Radius values corresponding to rows of y. + + # Keyword Arguments + - `filename::String="relaxation_solution.png"` : Path to save the figure. + + # Returns + - `plt` : The `Plots.Plot` object. + """ function plot_relaxation_solution(y, r; filename="relaxation_solution.png") R = maximum(r) @@ -362,40 +393,47 @@ module plotting N = Int(size(y, 1)) - plt = plot(layout=(2,Int(N/2)), size=(1000,600)) + # layout remains the same + plt = plot(layout=(2, Int(N/2)), size=(1200, 800)) for i in 1:N yi = y[i, :] - # plot directly into subplot - plot!( + # 1. Scatter for Real part + scatter!( plt[i], real(yi), rnorm, label = "Re(y$i)", - lw = 2 + markersize = 3, + markerstrokewidth = 0, + color = :blue, + alpha = 0.7 ) - plot!( + # 2. Scatter for Imaginary part + scatter!( plt[i], imag(yi), rnorm, label = "Im(y$i)", - lw = 2, - ls = :dash + markersize = 3, + markerstrokewidth = 0, + marker = :diamond, # Different shape to distinguish from Real + color = :red, + alpha = 0.7 ) xlabel!(plt[i], "y$i") ylabel!(plt[i], "r / R") title!(plt[i], "y$i") - # fix axis direction explicitly - ylims!(plt[i], minimum(rnorm), 1) - - # zero reference line + # Keep vertical reference line as a dotted line vline!(plt[i], [0], color=:black, lw=1, ls=:dot, label=false) + + ylims!(plt[i], minimum(rnorm), 1) end savefig(plt, filename) - @info "Saved relaxation solution plot to $filename" + @info "Saved relaxation scatter plot to $filename" return plt end From 4d54e48761d2d92fb0bf8fa26ee749336c532a3a Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 4 May 2026 19:25:32 +0000 Subject: [PATCH 23/36] Updated config, it now includes the core parameters. Changed default porosity threshold. --- res/config/all_options.toml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/res/config/all_options.toml b/res/config/all_options.toml index 981a1af..4d3ddf6 100644 --- a/res/config/all_options.toml +++ b/res/config/all_options.toml @@ -18,7 +18,7 @@ version = "1.0" # Version of this configuration file # output files [params.out] path = "all_options" - logging = "INFO" + logging = "DEBUG" plot_fmt = "png" # Plotting image file format, "png" or "pdf" recommended # ---------------------------------------------------- @@ -46,12 +46,12 @@ version = "1.0" # Version of this configuration file m = 2 # Harmonic of the true anomaly. m=2 corresponds to the semidiurnal tide, m=1 diurnal tide spectrum = "adaptive"# Obtain k2 spectrum for whole spectrum (<"full">) or region of interest (<"adaptive">) N_sigma = 10 # Number probe frequencies to evaluate k2 at - p_min = -6.7 # Minimum period for orbital and axial frequencies [log(kyr)] i.e. forcing occurion over minimal 1e-17 yr interval + p_min = -8 # Minimum period for orbital and axial frequencies [log(kyr)] i.e. forcing occurion over minimal 1e-17 yr interval p_max = 4 # Maximum period for orbital and axial frequencies [log(kyr)] i.e. forcing occurion over maximal 1e9 yr interval s_min = "none" # Minimum tidal mode range (k is the Fourier index in mean anomaly), set to obtain tidal mode range from eccentricity relation s_max = "none" # Maximum tidal mode range (k is the Fourier index in mean anomaly), set to obtain tidal mode range from eccentricity relation - material = "andrade" # Material type for complex shear modulus ("andrade", "maxwell") + material = "andrade" #"andrade" # Material type for complex shear modulus ("andrade", "maxwell") alpha = 0.3 # Power-law exponent, only for Andrade rheology (free parameter) # Solid interior model @@ -93,7 +93,7 @@ version = "1.0" # Version of this configuration file core = "liquid" # Core solution to use as CMB boundary condition, options: "liquid", "solid", "inertial" bulk_l = 1e9 # Liquid bulk modulus [Pa] permea = 1e-7 # Permeability [m^2] - porosity_thresh = 1e-5 # Porosity threshold, below this value no mush. [dimensionless] + porosity_thresh = 3e-2 # Porosity threshold, below this value no mush. [dimensionless] [orbit.obliqua.fluid] sigma_R = 1e-4 # Rayleigh drag coefficient at interface [dimensionless] @@ -110,3 +110,5 @@ version = "1.0" # Version of this configuration file [struct] mass_tot = 1.0 # M_earth core_density = 10738.33 # Core density [kg m-3] + core_shear = 0.0 # Core viscosity [Pa s] + core_bulk = 5e11 # Core bulk modulus [Pa] From 9358a01aa317911b8d174981aa7376daf47fe9c4 Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 4 May 2026 20:30:06 +0000 Subject: [PATCH 24/36] Updated reference docs. --- docs/src/reference/forcing-frequency.md | 9 ++- docs/src/reference/index.md | 2 +- docs/src/reference/liquid-phase.md | 2 +- docs/src/reference/mush-layer.md | 44 +++++++++++- docs/src/reference/solid/solid0d.md | 43 ++++++++++++ docs/src/reference/solid/solid1d.md | 66 +++++++++++++++++ docs/src/reference/solid/solid1d_mush.md | 51 ++++++++++++++ .../src/reference/solid/solid1d_mush_relax.md | 6 ++ docs/src/reference/solid/solid1d_relax.md | 70 +++++++++++++++++++ docs/src/reference/surface-loading.md | 51 ++++++++++++-- 10 files changed, 331 insertions(+), 13 deletions(-) create mode 100644 docs/src/reference/solid/solid0d.md create mode 100644 docs/src/reference/solid/solid1d.md create mode 100644 docs/src/reference/solid/solid1d_mush.md create mode 100644 docs/src/reference/solid/solid1d_mush_relax.md create mode 100644 docs/src/reference/solid/solid1d_relax.md diff --git a/docs/src/reference/forcing-frequency.md b/docs/src/reference/forcing-frequency.md index 85dce11..777a3b4 100644 --- a/docs/src/reference/forcing-frequency.md +++ b/docs/src/reference/forcing-frequency.md @@ -1,5 +1,5 @@ -### Reference (5) +### Reference (1) # Forcing Frequency @@ -12,13 +12,16 @@ Given the fact that the tidal forcing magnitude decreases exponentially with har where ``\Omega`` is spin rate and ``n_{\mathrm{orb}}`` orbital mean motion, and for integer values of order ``-2 \leq m \leq 2`` and harmonic ``\infty \leq k \leq \infty``. In our formalism tides are occuring over a large time interval, a time step ``\Delta t``. As such, we must account for tidal excitations that occur over a wide range of frequencies. We calculate the imaginary part of the ``n``th harmonic degree (``k_n``) Love number (``\Im[k_{n}(\sigma)]``) for all relevant harmonnic frequencies for which the Hansen coefficient ```math -X_{nmk}(e) = +X^{-(n+1), m}_k(e) = \frac{1}{2\pi} \int_0^{2\pi} \left(\frac{r}{a}\right)^n e^{im\Omega - ikn_{\mathrm{orb}}}\,dn_{\mathrm{orb}} \geq 0.01, ``` -(i.e. ~1% corrections). +(i.e. ~1% corrections). This implies that we are considering the following set $K$ of harmonnic frequencies $k$: +```math +\{k \in K \, \forall \, k : X^{-(n+1), m}_k \geq 0.01\ | k \in Z\} +``` --- \ No newline at end of file diff --git a/docs/src/reference/index.md b/docs/src/reference/index.md index 894ac30..4c214da 100644 --- a/docs/src/reference/index.md +++ b/docs/src/reference/index.md @@ -4,7 +4,7 @@ In this component you will be guided through the employed formalism and theoreti - [Rheology](@ref) - [Solid-Phase](@ref) -- [Mush layer](@ref) +- [Mush layer - interp](@ref) - [Liquid-Phase](@ref) - [Forcing Frequency](@ref) - [Surface Loading](@ref) diff --git a/docs/src/reference/liquid-phase.md b/docs/src/reference/liquid-phase.md index 003c8fb..a92889d 100644 --- a/docs/src/reference/liquid-phase.md +++ b/docs/src/reference/liquid-phase.md @@ -1,5 +1,5 @@ -### Reference (4) +### Reference (5) # Liquid-Phase diff --git a/docs/src/reference/mush-layer.md b/docs/src/reference/mush-layer.md index ba31b60..31a1e3e 100644 --- a/docs/src/reference/mush-layer.md +++ b/docs/src/reference/mush-layer.md @@ -1,4 +1,44 @@ -### Reference (3) +### Reference (4) -# Mush layer \ No newline at end of file +# Mush layer - interp + +The `interp` model is a simplified approach to modeling dissipation within a planetary region where active tidal equations (like those in `solid1d`) are not explicitly solved, such as a thin mushy layer or a transition zone. Instead of solving the full poro-viscoelastic system, it approximates the heating profile and subsequent Love numbers using exponential decay from the layer boundaries. + +### Methodology + +The model assumes that tidal dissipation peaks at the interfaces (upper and lower) and decays exponentially into the interior of the segment. + +#### 1. Heating Profile Generation +The heating profile $P(r)$ is constructed as a superposition of two exponential decay functions originating from the top ($r_{top}$) and bottom ($r_{bot}$) interfaces: + +$$P(r) = P_t \exp\left( -\frac{|r - r_{top}|}{l_t} \right) + P_b \exp\left( -\frac{|r - r_{bot}|}{l_b} \right)$$ + +where: +* **$P_t, P_b$**: The heating intensities at the top and bottom interfaces. +* **$l_t, l_b$**: The characteristic decay lengths, defined as a fraction of the total segment thickness ($l = \text{width} \cdot \Delta R$). + + + +#### 2. Inferring Love Numbers +To remain consistent with the energy dissipation within the global model, the imaginary part of the Tidal Love number $k_2^T$ is inferred directly from the generated power profile. For a shell with volume $V_s$, the contribution to the complex Love number is: + +$$\text{Im}(k_{2,s}^T) = -\frac{5 R \omega}{8\pi G} (P_s V_s)$$ + +The total $k_2^T$ for the segment is the sum of these contributions across all radial shells. Note that in this interpolation mode, the load Love number $k_2^L$ is typically returned as zero (or ignored) as the model focuses purely on energy dissipation rather than structural loading response. + +--- + +### Function Documentation + +```@docs +Obliqua.run_interp +``` + +### Configuration Parameters +When using this model via the TOML configuration, the following parameters in `[orbit.obliqua.mushy]` are relevant: + +* `t_width`: Controls the decay length from the top interface ($l_t$). +* `b_width`: Controls the decay length from the bottom interface ($l_b$). + +This model is particularly useful for representing "mushy" regions where the physical properties are highly uncertain, but the dissipation is expected to be concentrated near the boundaries of solid or liquid layers. \ No newline at end of file diff --git a/docs/src/reference/solid/solid0d.md b/docs/src/reference/solid/solid0d.md new file mode 100644 index 0000000..fcdad4c --- /dev/null +++ b/docs/src/reference/solid/solid0d.md @@ -0,0 +1,43 @@ + +### Reference (3) + +# Solid-Phase - solid0d + +The mechanical response of a planetary mantle with radially varying properties is approximated by a single effective complex shear modulus, $\bar{\mu}^*$. To account for the competition between uniform strain (Voigt limit) and uniform stress (Reuss limit) within a spherical segment, we employ the **Hill Average**. + +### The Hill Average +Given a radial profile of the complex shear modulus $\mu^*(r)$ and radial shells defined by $r_i$, the volume of each shell $V_i$ and its corresponding volume fraction $f_i$ are given by: + +$$V_i = \frac{4}{3}\pi \left( r_{i+1}^3 - r_i^3 \right), \quad f_i = \frac{V_i}{\sum V_i}$$ + +The effective modulus $\bar{\mu}^*$ is the arithmetic mean of the Voigt average ($\mu_V$) and the Reuss average ($\mu_R$): + +$$\mu_V = \sum_{i} f_i \mu_i^*, \quad \mu_R = \left( \sum_{i} \frac{f_i}{\mu_i^*} \right)^{-1}$$ + +$$\bar{\mu}^* = \frac{1}{2} \left( \mu_V + \mu_R \right)$$ + +--- + +### Dimensionless Rigidity +For a homogeneous incompressible sphere of radius $R$ and total mass $M$, the tidal ($k_n^T$) and load ($k_n^L$) Love numbers of degree $n$ are determined by the dimensionless rigidity $\bar{\mu}_n$. The scaling parameter $A_n$, which relates the elastic restoring force to the self-gravitational stability of the body, is defined as: + +$$A_n = \frac{4(2n^2 + 4n + 3)}{3n G M^2} \pi R^4$$ + +We define the effective dimensionless complex rigidity for the segment as: + +$$\mu^*_n = A_n \bar{\mu}^*$$ + +--- + +### Complex Love Numbers +The complex Love numbers are calculated using the transfer function factor $\mathcal{F} = (1 + \mu^*_n)^{-1}$. + +#### Tidal Love Number ($k_n^T$) +The tidal Love number represents the potential modification due to an external body. For $n > 1$: + +$$k_n^T = \frac{1}{1 + \mu^*_n} \left( \frac{3}{2(n - 1)} \right)$$ + +#### Load Love Number ($k_n^L$) +The load Love number represents the response to a surface mass load: + +$$k_n^L = -\frac{1}{1 + \mu^*_n}$$ \ No newline at end of file diff --git a/docs/src/reference/solid/solid1d.md b/docs/src/reference/solid/solid1d.md new file mode 100644 index 0000000..b82d004 --- /dev/null +++ b/docs/src/reference/solid/solid1d.md @@ -0,0 +1,66 @@ + +### Reference (3) + +# Solid-Phase - solid1d + +The `solid1d` model uses the shooting method approach and is in many regards identical to the original **Lovepy** model (now called **Love.jl**). + +The structure of the model is as follows: +1. We impose the Core-Mantle Boundary (CMB) condition (three elementary solutions). +2. We solve the system at each radius: + $$\frac{d\pmb{y}_{n,m}(r)}{dr} = \pmb{A}_n (r) \pmb{y}_{n,m}(r) - \pmb{f}_{n,m}(r)$$ + stepping (shooting) from the CMB to the surface. + +For this model, we assume $\pmb{f}_{n,m}(r) = \pmb{0}$, as porosity effects are not included. + +### The Propagator Matrix +A convenient way to treat the state vectors $\pmb{y}_{n,m}(r)$ is to define the propagator matrix $\pmb\Pi_n(r)$ (a $6 \times 6$ matrix) that takes the state vector at $r_i$ to $r_{i+1}$: + +$$\pmb{y}_{n,m}(r_{i+1}) = \pmb\Pi_n(r_{i}) \pmb{y}_{n,m}(r_{i})$$ + +Using the **RK4 (Runge-Kutta 4th Order)** method, we have: + +$$\pmb\Pi_n(r_{i}) = \pmb{1} + \frac{1}{6} \left(\pmb{K}_1 + \pmb{K}_2 + \pmb{K}_3 + \pmb{K}_4 \right)$$ + +where: +$$\begin{aligned} + \pmb{K}_1 &= \Delta r_i \pmb{A}_n (r_i) \\ + \pmb{K}_2 &= \Delta r_i \pmb{A}_n (r_i + \Delta r_i / 2) \left[ \pmb{1} + \frac{1}{2} \pmb{K}_1 \right] \\ + \pmb{K}_3 &= \Delta r_i \pmb{A}_n (r_i + \Delta r_i / 2) \left[ \pmb{1} + \frac{1}{2} \pmb{K}_2 \right] \\ + \pmb{K}_4 &= \Delta r_i \pmb{A}_n (r_i + \Delta r_i) \left[ \pmb{1} + \pmb{K}_3 \right] +\end{aligned}$$ + +--- + +### Shooting vs. Relaxation +It is worth highlighting that this method purely propagates the solution vectors to the surface. Effectively, we attempt to solve the two-point **Boundary Value Problem (BVP)** by rewriting it as an initial value problem and imposing a constraint at the end. + +* **Shooting Method:** The propagator matrix $\pmb\Pi_n(r)$ has no knowledge of the surface boundary until it arrives there, which means it may diverge before the boundary conditions can be applied. +* **Relaxation Method:** Solves the state vectors globally based on both boundary conditions simultaneously, making it significantly more stable. + +--- + +### Numerical Integration +Imposing continuity of the propagator (implying the solution itself is continuous): +$$\pmb \Pi_n (r_i^+,r') = \pmb \Pi_n (r_i^-,r')$$ + +And imposing CMB conditions in the general solution: +$$\pmb y_{n,m}(r_C^+) = \pmb y_0 = \pmb I_C \pmb C$$ + +We find: +$$\pmb y_{n,m}(r) = \pmb \Pi_n (r,r_C^+) \pmb I_C \pmb C$$ + +We iterate through the mantle, repeatedly multiplying the elementary solutions with the propagator matrix until we reach the surface ($R = a^-$). + +### Surface Boundary Conditions +At the surface, we impose the boundary conditions on the upper components of the $y$-functions using a projector $\pmb P_1$: + +$$\pmb P_1 \pmb y (a^-) = +\begin{pmatrix} +y_3(a^-) \\[1.2em] +y_4(a^-) \\[1.2em] +\frac{n+1}{a^-} y_5(a^-) + y_6(a^-) +\end{pmatrix} = +\pmb P_1 \left( \pmb\Pi_n (a^-, r_C^+) \pmb I_C \pmb C \right) = \pmb b$$ + +where $\pmb b$ is the 3-vector composed of the RHS of the surface boundary conditions. This allows for the determination of the coefficient vector $\pmb C$, which is used to combine the three elementary solutions into the true modal solution. \ No newline at end of file diff --git a/docs/src/reference/solid/solid1d_mush.md b/docs/src/reference/solid/solid1d_mush.md new file mode 100644 index 0000000..602df3b --- /dev/null +++ b/docs/src/reference/solid/solid1d_mush.md @@ -0,0 +1,51 @@ + +### Reference (3) + +# Solid-Phase - solid1d_mush + +The `solid1d-mush` model is also based on a forward propagation scheme, but it includes the effects of porosity as a **poro-viscoelastic model**. + +The governing equations are an extension of the `solid1d` model. The additional complexity is handled through two extra variables: the 7th and 8th $y$-functions, representing **pore pressure** and **Darcy flux** (relative tangential displacement), respectively. + +### Porous Transitions and Elementary Solutions +The solver initially treats the system as a $6 \times 6$ problem. When it encounters a porous layer, it updates the elementary solutions by embedding the $3 \times 1$ coefficient set into a $4 \times 1$ matrix and adding a fourth elementary solution vector, $\pmb{y}^{(4)}_n(r_i)$. + +At the initial solid-to-porous transition ($r_i$), the new elementary vector is defined as: +$$\pmb{y}^{(4)}_{n,m}(r_i) = (0, 0, 0, 0, 0, 0, 1, 0)^T$$ + +### Continuity and Source Terms +By turning back to the governing differential equation: +$$\frac{d\pmb{y}_{n,m}(r)}{dr} = \pmb{A}_n (r) \pmb{y}_{n,m}(r) - \pmb{f}_{n,m}(r)$$ + +We impose continuity across an infinitesimally thin layer ($\Delta r = 0$) where $\pmb\Pi_n(r_{i}) = \pmb{1}$. We allow $\pmb{y}_{n,m}(r_i^+)$ to be an $8 \times 1$ vector and $\pmb{y}_{n,m}(r_i^-)$ to be $6 \times 1$. To preserve continuity while introducing pore pressure, we include a source term $\pmb{f}_{n,m}(r_i^+)$: + +$$\begin{pmatrix} \pmb{1}_{(8\times8)} \end{pmatrix} \pmb{y}_{n,m}(r_i^+) - \pmb{f}_{n,m}(r_i^+) = \begin{pmatrix} \pmb{1}_{(8\times6)} \\ \pmb{0}_{(2\times6)} \end{pmatrix} \pmb{y}_{n,m}(r_i^-)$$ + +Since the elementary solutions are linearly independent, the modal solution at the interface is: +$$\pmb{y}_n(r_i^+) = a_1 \mathbf{y}^{(1)}(r_i^+) + a_2 \mathbf{y}^{(2)}(r_i^+) + a_3 \mathbf{y}^{(3)}(r_i^+) + a_4 \mathbf{y}^{(4)}(r_i^+)$$ +$$\pmb{y}_{n,m}(r_i^+) = \{ \text{solution of } 6\times 6 \text{ system} \} + (0, 0, 0, 0, 0, 0, a_4, 0)^T$$ + +To preserve continuity, the source term must exactly cancel the introduced pressure: +$$\pmb{f}_{n,m}(r_i^+) = (0, 0, 0, 0, 0, 0, a_4, 0)^T$$ + +Understanding these source/sink steps is key for including effects like porosity or seismic activity in the relaxation-based solver. + +--- + +### Removing Porous Functions +If the medium becomes solid again before the surface, we reverse the process. We use a sink term $\pmb{f}_{n,m}(r_i^-)$ at the upper interface to remove excess pore pressure: + +$$\begin{pmatrix} \pmb{1}_{(8\times6)} \\ \pmb{0}_{(2\times6)} \end{pmatrix}^T \pmb{y}_{n,m}(r_i^+) = \begin{pmatrix} \pmb{1}_{(8\times8)} \end{pmatrix} \pmb{y}_{n,m}(r_i^-) - \pmb{f}_{n,m}(r_i^-)$$ + +We must constrain the **Darcy flux to be zero** at the interface (upper boundary condition). While a sink term can remove excess pore pressure, it cannot "force" a boundary condition like zero Darcy flux if the propagation has already diverged. Instead, we find: +$$\pmb{f}_{n,m}(r_i^-) = (0, 0, 0, 0, 0, 0, y_7(r_i^-), 0)^T$$ + +A simpler numerical approach is to manually set $y_7 = 0$ for all $r > r_i^-$, as it no longer interacts with the $6 \times 6$ system. + +--- + +### Final Solution +The remaining steps are identical to the `solid1d` model: +1. Propagate the elementary solution vectors ($8 \times 1$ or $6 \times 1$) to the surface. +2. Determine the coefficients $\pmb{C}$ (now including $a_4$ for the porous component). +3. Apply $\pmb{C}$ to obtain the final modal solution. \ No newline at end of file diff --git a/docs/src/reference/solid/solid1d_mush_relax.md b/docs/src/reference/solid/solid1d_mush_relax.md new file mode 100644 index 0000000..1c8a87b --- /dev/null +++ b/docs/src/reference/solid/solid1d_mush_relax.md @@ -0,0 +1,6 @@ + +### Reference (3) + +# Solid-Phase - solid1d-mush-relax + +No spoilers (still a WIP), but this module combines the features of `solid1d_mush` and `solid1d_relax`, providing a radially resolved structure with partially molten regions, solved using a relaxation-based method for improved stability. \ No newline at end of file diff --git a/docs/src/reference/solid/solid1d_relax.md b/docs/src/reference/solid/solid1d_relax.md new file mode 100644 index 0000000..f8df9c2 --- /dev/null +++ b/docs/src/reference/solid/solid1d_relax.md @@ -0,0 +1,70 @@ + +### Reference (3) + +# Solid-Phase - solid1d_relax + +A significant advantage of the `solid1d-relax` model is that it eliminates the need for the cumbersome imposition of internal boundary conditions required by shooting methods. Instead, the relaxation method solves for the modal solution across the entire domain simultaneously. + +### Numerical Formulation +We begin with the standard problem statement: +$$\frac{d\pmb{y}_{n,m}(r)}{dr} = \pmb{A}_n (r) \pmb{y}_{n,m}(r) - \pmb{f}_{n,m}(r)$$ + +Assuming no internal sources ($\pmb{f}_{n,m}(r) = \pmb{0}$), we approximate the system using second-order finite differences: +$$\pmb{y}_{n+1} − \pmb{y}_{n} = \frac{\Delta r}{2} (\pmb{A}_{n+1} \pmb{y}_{n+1} + \pmb{A}_n \pmb{y}_n)$$ + +Rearranging terms yields the fundamental relaxation equation: +$$\pmb{C}_n \pmb{y}_n + \pmb{D}_{n+1} \pmb{y}_{n+1} = \pmb{0}$$ +where: +* $\pmb{C}_n = \pmb{I} + \frac{\Delta r}{2} \pmb{A}_n$ +* $\pmb{D}_{n+1} = −\pmb{I} + \frac{\Delta r}{2} \pmb{A}_{n+1}$ + +--- + +### Boundary Conditions as Constraints +Unlike the shooting method, where we track elementary solutions, the relaxation scheme treats the Core-Mantle Boundary (CMB) and the surface as direct constraints on a single modal solution vector. + +#### Lower Boundary (CMB) +At the CMB ($r = r_C^+$), we eliminate unknown coefficients to write the boundary condition as: +$$B_1 \mathbf{y}(r_C^+) = 0$$ +where $B_1$ is a $3 \times 6$ matrix that enforces continuity. By setting the RHS to zero, we ensure the solution remains physically consistent at the interface without needing to manually restart the integration. + +#### Upper Boundary (Surface) +Similarly, at the surface ($r = a^-$), we define the $3 \times 6$ matrix $B_N$: +$$B_N = \begin{pmatrix} 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & (n+1)/a & 1 \end{pmatrix}$$ +This satisfies $B_N \mathbf{y} = b$, where $b$ contains the surface forcing terms. + +--- + +### The Global Matrix and Henyey Relaxation +Combining the difference equations and boundary conditions results in a large, banded global system: +$$\mathcal{H} \pmb{y} = \pmb{b}$$ + +Because $\mathcal{H}$ is typically $6N \times 6N$ (with $N \approx 2000$), direct inversion is too expensive. We use a **Henyey-type relaxation** to solve the system recursively. We define square $6 \times 6$ submatrices $P_n, S_n,$ and $Q_n$: + +* **$P_n$**: Lower half of $C_{n-1}$ +* **$S_n$**: Combined lower half of $D_n$ and upper half of $C_n$ +* **$Q_n$**: Upper half of $D_{n+1}$ + +The system is then solved in two passes: + +1. **Forward Sweep (Recursive Relations):** + Calculate the response matrices $R_n$ from the CMB to the surface: + $$R_n = -X_n^{-1} Q_n, \quad X_n = P_n R_{n-1} + S_n$$ + starting with $R_1 = -S_1^{-1} Q_1$. + +2. **Backward Sweep (Solution):** + Determine the surface solution: + $$y_N = X_N^{-1} b$$ + Then propagate the solution back down to the CMB: + $$y_n = R_n y_{n+1}$$ + +> **Conceptual Analogy:** Imagine a string in a river tied to two endpoints. The river's flow (the physics/differential equations) dictates the string's curve, while the endpoints (boundaries) fix its absolute position. If a rock (internal boundary) is in the river, the string naturally bends around it. + +--- + +### The Grid +To resolve high gradients near the surface, we use an uneven exponential grid. The total number of nodes $N$ and their positions $r_i$ are determined by the ratio between the maximum interval $\Delta r_{\max}$ (at the CMB) and the minimum interval $\Delta r_{\min}$ (at the surface): + +$$\alpha = \ln\left(\frac{\Delta r_{\max}}{\Delta r_{\min}}\right)$$ + +$$r_i = R_E + (R_c - R_E) \left( \frac{\exp\left( \alpha \frac{N - i}{N - 1} \right) - 1}{\exp(\alpha) - 1} \right), \quad i = 1, \ldots, N$$ \ No newline at end of file diff --git a/docs/src/reference/surface-loading.md b/docs/src/reference/surface-loading.md index fff1750..396cc7c 100644 --- a/docs/src/reference/surface-loading.md +++ b/docs/src/reference/surface-loading.md @@ -5,14 +5,53 @@ ### Total ``k_{2}`` -To include a surface magma ocean, the full Love number is +To calculate the response of a planet to various external forcings, we define general boundary conditions at the surface ($r = R$). These conditions account for external gravitational potentials, mass loading, tangential tractions, and surface pressure. -```math -k_n(\sigma) = k_{T,n}^{(\mathrm{solid})}(\sigma) -+ \bigl[1 + k_{L,n}(\sigma)\bigr]\,k_{n}^{(\mathrm{fluid})}(\sigma). -``` +### General Boundary Conditions +For a given degree $n$, the boundary conditions for the state vector components $y_i$ are summarized in the following table: -Sub-surface magma oceans are included only through the solid formalism, based on viscous disipation and compaction. +| State Function | External Potential ($U$) | Mass Load ($\zeta_n$) | Traction ($\tau$) | Pressure ($P$) | +| :--- | :--- | :--- | :--- | :--- | +| **$y_3(R)$** (Normal Stress) | $0$ | $-g_e \zeta_n$ | $0$ | $-P_n$ | +| **$y_4(R)$** (Tangential Stress) | $0$ | $0$ | $\tau_n$ | $0$ | +| **$\frac{n+1}{R} y_5(R) + y_6(R)$** | $\frac{2n+1}{R} U_n$ | $4\pi G \zeta_n$ | $0$ | $0$ | +By expressing a surface mass load $\zeta_n$ as an equivalent external potential $U'$, where $\zeta_n = \frac{2n + 1}{4 \pi G R} U'_n$, the system simplifies to: + +$$\begin{aligned} +y_{3}(R) &= - \frac{(2n + 1)g_e}{4 \pi G R} U'_n - P_n \\ +y_{4}(R) &= \tau_n \\ +\frac{n+1}{R} y_5(R) + y_6(R) &= \frac{2n+1}{R} (U_n + U'_n) +\end{aligned}$$ + +### Calculation of Love Numbers +In `Obliqua`, Love numbers are non-dimensionalized by setting the forcing terms to either $1$ (present) or $0$ (absent). +* **Tidal Love Number (TLN):** Calculated by setting $(U, U', \tau, P) = (1, 0, 0, 0)$. +* **Load Love Number (LLN):** Calculated by setting $(U, U', \tau, P) = (0, 1, 0, 0)$. + +Setting $U=1$ allows us to determine the intrinsic response of the planet; the final physical solution is obtained by scaling these non-dimensional Love numbers by the actual tidal potential $U_{n,m,k}$ of the system. + +--- + +### Global Potential and Layer Coupling +The total distortion potential of the planet, $U_n^D$, can be constructed by combining the responses to different forcings: + +$$U_n^D = k_n^T + (1+k_n^L)U_n^L + K_n^P U_n^P$$ + +Where: +* $U_n^L$: Potential due to surface mass-loading (e.g., a magma ocean or ice sheet). +* $U_n^P$: Potential due to surface pressure (e.g., atmospheric loading). + +#### Current Implementation +Currently, `Obliqua` couples the solid and fluid responses to calculate a **Global Tidal Love Number** $k_n$ using the following recursive form: + +$$k_n = k^T_n + (1 + k^L_n) k_n^{T, \text{(fluid)}}$$ + +In this formulation: +1. $k^T_n$ and $k^L_n$ are the tidal and load Love numbers of the **solid mantle** only. +2. $k_n^{T, \text{(fluid)}}$ represents the response of the fluid layer. +3. The term $(1 + k^L_n)$ accounts for how the solid interior deforms under the weight of the fluid's own tidal response. + +While additional corrections for atmospheric pressure or specialized crustal rheologies (Andrade/Maxwell) are not yet active, the framework is designed to incorporate these by calculating the respective pressure and loading Love numbers which are already implemented in the solver. --- \ No newline at end of file From 877c9102a1d4071b77d24d033b2311ecb144479e Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 4 May 2026 20:31:35 +0000 Subject: [PATCH 25/36] Updated tutorials docs. --- docs/src/tutorials/configuration-file.md | 118 +++++++++++++++++++++++ docs/src/tutorials/loading-data.md | 19 +++- docs/src/tutorials/plotting.md | 11 ++- docs/src/tutorials/running-model.md | 21 ++-- 4 files changed, 147 insertions(+), 22 deletions(-) create mode 100644 docs/src/tutorials/configuration-file.md diff --git a/docs/src/tutorials/configuration-file.md b/docs/src/tutorials/configuration-file.md new file mode 100644 index 0000000..9037a5d --- /dev/null +++ b/docs/src/tutorials/configuration-file.md @@ -0,0 +1,118 @@ +```@meta +CollapsedDocStrings = true +``` + +### Tutorials (2) + +# Configuration file + +You can read the configuration from a TOML file using the following command: + +```julia +cfg = Obliqua.open_config("$RES_DIR/config/all_options.toml") +``` + +The configuration files follow the conventions used within PROTEUS. The default `all_options.toml` file contains all available parameters. + +### Global Parameters +These define metadata and output behavior: +* **`title`**: Identifier for the simulation setup. +* **`version`**: Configuration file version for reproducibility. + +--- + +### Execution Parameters (`[params]`) +This block controls output and logging: +* **`path`**: Directory where output files are stored. +* **`logging`**: Logging level (e.g., `INFO`, `DEBUG`). +* **`plot_fmt`**: Output format for generated plots. + +--- + +### Stellar Parameters (`[star]`) +Defines the host star: +* **`mass`**: Stellar mass in solar masses ($M_\odot$). + +--- + +### Orbital Parameters (`[orbit]`) +Describes the planetary orbit and optional satellite: +* **`semimajoraxis`**: Orbital semi-major axis [AU]. +* **`eccentricity`**: Orbital eccentricity. +* **`satellite`**: Boolean flag to include a moon. +* **`mass_sat`**: Satellite mass [kg]. +* **`semimajoraxis_sat`**: Satellite orbital radius [m]. + +--- + +### Tidal Model Parameters (`[orbit.obliqua]`) +Controls the tidal response model. + +#### Rheology and Viscosity +* **`visc_l`**, **`visc_lus`**: Liquid and liquidus viscosities. +* **`visc_s`**, **`visc_sus`**: Solid and solidus viscosities. + +#### Spectral and Forcing Parameters +* **`n`**: Radial dependence exponent in $(r/a)^n$. +* **`m`**: Tidal harmonic (e.g., $m=2$ for semidiurnal tides). +* **`spectrum`**: Frequency sampling strategy (`"full"` or `"adaptive"`). +* **`N_sigma`**: Number of sampled forcing frequencies. +* **`p_min`**, **`p_max`**: Period range ($\log_{10}$ kyr). +* **`s_min`**, **`s_max`**: Fourier mode range. + +#### Material Model +* **`material`**: Rheological model (`"andrade"` or `"maxwell"`). +* **`alpha`**: Andrade power-law exponent. + +--- + +### Interior Structure Models + +#### Solid Model (`module_solid`) +* **`solid0d`**: Homogeneous solid approximation. +* **`solid1d`**: Radially resolved structure. +* **`solid1d-relax`**: Relaxation-based solver (more stable). +* **`solid1d-mush`**: Includes partially molten regions. +* **`solid1d-mush-relax`**: Relaxation-based solver (more stable), includes partially molten regions. + +#### Mushy Layer (`module_mushy`) +* **`none`**: No explicit treatment. +* **`interp`**: Smooth transition between solid and liquid. + +#### Fluid Model (`module_fluid`) +* **`none`**: No fluid layer. +* **`fluid0d`**: Bulk fluid approximation. +* **`fluid1d`**: Radially structured fluid with heating. + +--- + +### Solid Interior Parameters (`[orbit.obliqua.solid]`) +* **`ncalc`**: Number of radial layers (shooting method). +* **`dr_min`**, **`dr_max`**: Grid spacing for relaxation solver [m]. +* **`core`**: Core boundary condition (`"liquid"`, `"solid"`, `"inertial"`). +* **`bulk_l`**: Liquid bulk modulus [Pa]. +* **`permea`**: Permeability [$m^2$]. +* **`porosity_thresh`**: Threshold below which no mush is formed. + +--- + +### Fluid Parameters (`[orbit.obliqua.fluid]`) +* **`sigma_R`**: Rayleigh drag at the interface. +* **`sigma_R_inf`**: Drag in the bulk fluid. +* **`sigma_R_prf`**: Vertical drag profile. +* **`H_R`**: Scale height [m]. +* **`efficiency`**: Drag efficiency factor. + +--- + +### Mushy Layer Parameters (`[orbit.obliqua.mushy]`) +* **`b_width`**, **`t_width`**: Width of dissipation peaks as fraction of layer thickness. + +--- + +### Planetary Structure (`[struct]`) +Defines bulk planetary properties: +* **`mass_tot`**: Total planetary mass [$M_\oplus$]. +* **`core_density`**: Core density [kg m$^{-3}$]. +* **`core_shear`**: Core shear [Pa s]. +* **`core_bulk`**: Core bulk modulus [Pa ]. \ No newline at end of file diff --git a/docs/src/tutorials/loading-data.md b/docs/src/tutorials/loading-data.md index 3c9b8ee..282bbec 100644 --- a/docs/src/tutorials/loading-data.md +++ b/docs/src/tutorials/loading-data.md @@ -31,13 +31,13 @@ Depending on which module is being used the following parameters need to be prov ##### Table of inputs -| Input | solid-phase | solid-phase + mush interface | liquid-phase | Description | Symbol | +| Input | solid1d | solid1d\_mush | fluid1d | Description | Symbol | |:--------|:----------------:|:----------------:|:--------:|:---------------------------------|-------------:| | omega | ✔️ | ✔️ | ✔️ | Orbital Frequency | ``\omega`` | -| axial | ❌ | ❌ | ✔️ | Axial Frequency | ``\Omega`` | +| axial | ✔️ | ✔️ | ✔️ | Axial Frequency | ``\Omega`` | | ecc | ✔️ | ✔️ | ✔️ | Eccentricity | ``\epsilon`` | -| sma | ❌ | ❌ | ✔️ | Semi major axis | ``a`` | -| S_mass | ❌ | ❌ | ✔️ | Stellar mass | ``M_\star `` | +| sma | ✔️ | ✔️ | ✔️ | Semi major axis | ``a`` | +| S_mass | ✔️ | ✔️ | ✔️ | Stellar mass | ``M_\star `` | | density | ✔️ | ✔️ | ✔️ | Density profile | ``\rho`` | | radius | ✔️ | ✔️ | ✔️ | Radii | ``r`` | | visc | ✔️ | ✔️ | ✔️ | Viscosity profile | ``\eta`` | @@ -48,7 +48,7 @@ Depending on which module is being used the following parameters need to be prov It is important to note that the `radius` array contains the radial values at the boundaries of the spherical shells that make up the planetary mantle, whilst the `density`, `visc`, `shear`, `bulk`, and `phi` arrays contain the mean of these in the spherical shells. As such it follows that there are ``N+1`` values in the `radius` array, and ``N`` values in the`density`, `visc`, `shear`, `bulk`, and `phi` arrays, where ``N`` is the number of spherical shells. Please do not confuse `ncalc` with ``N``, since the former expands the number of layers through interpolation, whereas the latter is the initial resolution of the data. -For now the user is provided with four different functions, they are given below. The most simple way to use these functions is as follows. First let's test if the provided data is compatible. +For now, the user is provided with four different functions, they are given below. The most simple way to use these functions is as follows. First let's test if the provided data is compatible. ```julia using Obliqua @@ -75,6 +75,15 @@ omega, ecc, rho, radius, visc, shear, bulk, ncalc = load.load_interior("$RES_DIR ``` +Obliqua still expects the user to provide all quantities, in this case one can simply pass an array of zeros (e.g. for the `phi` array, since this is not used in the `solid1d` module.) Recommnende is to use + +```julia +omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = + load.load_interior_mush_full("$RES_DIR/interior_data/test_mantle_mush_full_test.json", false) +``` + +to get started, as it provides all the necessary parameters for all modules. + --- --- diff --git a/docs/src/tutorials/plotting.md b/docs/src/tutorials/plotting.md index 0395f52..e8b0e06 100644 --- a/docs/src/tutorials/plotting.md +++ b/docs/src/tutorials/plotting.md @@ -2,11 +2,18 @@ CollapsedDocStrings = true ``` -### Tutorials (3) +### Tutorials (4) # Plotting -Currently, there are two plotting scripts included with `Obliqua`. More will be added. +While this is still a work in progress, `Obliqua` currently includes basic plotting functionality. These include the following functions: +- `Obliqua.plotting.plot_imagk2_spectrum`: Plots the imaginary part of the Love number $k_n$ as a function of the forcing frequency. +- `Obliqua.plotting.plot_imagk2_spectra`: Plots the imaginary part of the Love number $k_n$ as a function of the forcing frequency for each segment individually. +- `Obliqua.plotting.save_heat_profile`: Plots the tidal power profile as a function of the radius. +- `Obliqua.plotting.plot_segment_heating`: Plots the tidal power block as a function of the radius and forcing frequency. +- `Obliqua.plotting.plot_surface_heating`: Plots the surface tidal power as a function of latitude and longitude. +- `Obliqua.plotting.plot_relaxation_solution`: Plots the $y$-functions as a function of the radius. (Specifically useful for debugging the relaxation models, but can be used for the other models as well). + --- --- diff --git a/docs/src/tutorials/running-model.md b/docs/src/tutorials/running-model.md index 2125ae0..9f3c525 100644 --- a/docs/src/tutorials/running-model.md +++ b/docs/src/tutorials/running-model.md @@ -6,10 +6,7 @@ CollapsedDocStrings = true # Running Model -Now that you are able to validate and load data files using the `Obliqua.load` module, we can start using the tidal models `Obliqua.Solid`, `Obliqua.Love` and `Obliqua.Fluid`. In principle, this is rather simple, since you only have to call one function depending on your use case. For example, let us proceed with the data file from [Loading data](@ref). In this case, following the [Table of inputs](@ref), we are interested in the solid-phase tides only, so we may use the `Obliqua.calc_solid_tides` function. - -!!! warning - Newer versions of the code will stop using the distinct phase `calc_..._tides` functions, instead all models can be accessed through `run_tides`. +Now that you are able to validate and load data files using the `Obliqua.load` module, we can start using the tidal models `Obliqua.solid0d`, `Obliqua.solid1d`, `Obliqua.solid1d_mush`, `Obliqua.solid1d_relax`, `Obliqua.solid1d_mush_relax`, `Obliqua.fluid0d` and `Obliqua.fluid1d`. In principle, this is rather simple, since you only have to call one function. For example, let us proceed with the data file from [Loading data](@ref). In this case, following the [Table of inputs](@ref), we are interested in the solid-phase tides only, so we may use the `Obliqua.calc_solid_tides` function. ```julia using Obliqua @@ -18,22 +15,16 @@ using Obliqua RES_DIR = "/path/to/Obliqua/res" # use the relevant load function -omega, ecc, rho, radius, visc, shear, bulk, ncalc = Obliqua.load.load_interior("$RES_DIR/interior_data/test_mantle.json", false) +omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = + Obliqua.load.load_interior_mush_full("$RES_DIR/interior_data/test_mantle_mush_full_test.json", false) # call the desired model -power_prf, power_blk, imag_k2 = Obliqua.calc_solid_tides(omega, ecc, rho, radius, visc, shear, bulk; ncalc=ncalc, material="maxwell") +power_prf, power_blk, σ_range, imag_k2 = Obliqua.run_tides( + omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg +) ``` -In general, we can use - -| Tides | Function | -|:------------------------------|:---------------------------------------| -| solid-phase | `Obliqua.calc_solid_tides` | -| solid-phase + mush interface | `Obliqua.calc_solid_tides_mush` | -| liquid-phase | `Obliqua.calc_fluid_tides` | -| mixed-phase | `Obliqua.run_tides` | - --- --- From db9af903834b5210b40e3c784a5ec2fd45243d2a Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 4 May 2026 20:32:23 +0000 Subject: [PATCH 26/36] Updated main docs structure. --- docs/make.jl | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 2ba4784..80a7d8c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -11,6 +11,7 @@ format = Documenter.HTML( # Build the docs makedocs( sitename="Obliqua", + checkdocs = :none, format=format, modules = [Obliqua], pages = [ @@ -19,17 +20,25 @@ makedocs( "Compass" => "compass.md", PageNode("Tutorials" => "tutorials/index.md", [ "1 - Loading Data" => "tutorials/loading-data.md", - "2 - Running Model" => "tutorials/running-model.md", - "3 - Plotting" => "tutorials/plotting.md" + "2 - Configuring Model" => "tutorials/configuration-file.md", + "3 - Running Model" => "tutorials/running-model.md", + "4 - Plotting" => "tutorials/plotting.md" ] ), "How-to guides" => "how-to-guides/index.md", PageNode("Reference" => "reference/index.md", [ - "1 - Rheology" => "reference/rheology.md", - "2 - Solid-phase" => "reference/solid-phase.md", - "3 - Mush layer" => "reference/mush-layer.md", - "4 - Liquid-phase" => "reference/liquid-phase.md", - "5 - Forcing Frequency" => "reference/forcing-frequency.md", + "1 - Forcing Frequency" => "reference/forcing-frequency.md", + "2 - Rheology" => "reference/rheology.md", + PageNode("3 -Solid-phase" => "reference/solid-phase.md", [ + "Solid0d" => "reference/solid/solid0d.md", + "Solid1d" => "reference/solid/solid1d.md", + "Solid1d-mush" => "reference/solid/solid1d_mush.md", + "Solid1d-relax" => "reference/solid/solid1d_relax.md", + "Solid1d-mush-relax" => "reference/solid/solid1d_mush_relax.md" + ] + ), + "4 - Mush layer" => "reference/mush-layer.md", + "5 - Liquid-phase" => "reference/liquid-phase.md", "6 - Surface Loading" => "reference/surface-loading.md", "7 - Tidal Potentials" => "reference/tidal-potentials.md" ] From 351a75250ef136f00801f03091c256d9de853d70 Mon Sep 17 00:00:00 2001 From: Marijn Date: Mon, 4 May 2026 20:55:51 +0000 Subject: [PATCH 27/36] Updated tests, cleaned up code. --- src/Obliqua.jl | 14 +++++++------- src/solid1d.jl | 2 -- test/runtests.jl | 30 +++++++++++++++--------------- test/test.toml | 2 ++ 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Obliqua.jl b/src/Obliqua.jl index 19b446a..a6ba12c 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -888,13 +888,13 @@ module Obliqua P_T_prf = [sum(P_T_s_prf[:,j]) for j in 1:size(P_T_s_prf,2)] P_T_map = sum(P_T_s_map, dims=1) # sum over frequencies to get total surface map - # plot map of surface heating - plt = plotting.plot_surface_heating( - P_T_map[1, :, :], - res; - filename="$OUT_DIR/tidal_heating_map_surface.png", - title_str="Surface heating map" - ) + # # plot map of surface heating + # plt = plotting.plot_surface_heating( + # P_T_map[1, :, :], + # res; + # filename="$OUT_DIR/tidal_heating_map_surface.png", + # title_str="Surface heating map" + # ) # determine the total heat input from heating profile P_T_prf_blk = sum(dv .* P_T_prf) diff --git a/src/solid1d.jl b/src/solid1d.jl index 6d55ba6..ed9cf83 100644 --- a/src/solid1d.jl +++ b/src/solid1d.jl @@ -456,8 +456,6 @@ module solid1d function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr) i = 1 - println(sin.(clats)) - @views Y = solid1d.Y[i,:,:] @views dYdθ = solid1d.dYdθ[i,:,:] @views dYdϕ = solid1d.dYdϕ[i,:,:] diff --git a/test/runtests.jl b/test/runtests.jl index a97f10f..c357ef5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -152,9 +152,9 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 3.4054467488462404e-15, 3.221172119001604e-15, 3.1295744034823437e-15, 3.0848071238085414e-15, 3.0748087696203486e-15, 3.097587698655655e-15, 6.812344431592964e-16, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 49253.31079112116 - imag_k2_expt = [6.748620035044579e-5] + power_prf_expt = [0.0, 0.0, 3.3836853500831803e-15, 3.199107433705875e-15, 3.1066684894960716e-15, 3.0607994210208172e-15, 3.049518217180047e-15, 3.0708441754457593e-15, 6.484028375243808e-16, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 48799.97241357575 + imag_k2_expt = [6.68650424205084e-5] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -190,9 +190,9 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 8.439500899702699e-19, 7.620980552195005e-19, 7.343006660817232e-19, 7.233683944336647e-19, 7.207110036818358e-19, 7.257082672043938e-19, 2.791755081097162e-25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 11.206735073152927 - imag_k2_expt = [1.5355312288113298e-8] + power_prf_expt = [0.0, 0.0, 8.38538695455464e-19, 7.568669610705781e-19, 7.289216195147812e-19, 7.17739811066603e-19, 7.147892586185959e-19, 7.194534263837449e-19, 2.6571851057720503e-25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 11.123362128828322 + imag_k2_expt = [1.5241075841179643e-8] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -320,9 +320,9 @@ if suite > 4 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 4.950568223680489e-14, 4.5676008465905126e-14, 4.282121624939781e-14, 4.0271210724467955e-14, 3.787104918976475e-14, 3.560948831486367e-14, 1.2385369527824588e-11, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 4.805276752962792e8 - imag_k2_expt = [0.6584123269704052] + power_prf_expt = [0.0, 0.0, 4.950610209920039e-14, 4.567639539072659e-14, 4.2821578796970674e-14, 4.0271551813907144e-14, 3.787137048538955e-14, 3.560979144308022e-14, 1.2496829309351557e-11, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 4.8052844153493536e8 + imag_k2_expt = [0.6584133768599382] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -358,9 +358,9 @@ if suite > 4 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 1.2000536007632784e-17, 1.0577415996677013e-17, 9.837740538298375e-18, 9.249727566534158e-18, 8.698383380619814e-18, 8.179031833355627e-18, 1.2370422448018447e-11, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 4.8114259486262393e8 - imag_k2_expt = [0.6592548811944196] + power_prf_expt = [0.0, 0.0, 1.200063876496574e-17, 1.0577506483396584e-17, 9.837824669298708e-18, 9.24980671498098e-18, 8.698457949883253e-18, 8.179102199913946e-18, 1.2481596843673565e-11, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 4.8114338657619107e8 + imag_k2_expt = [0.6592559659893207] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -487,9 +487,9 @@ if suite > 10 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 1.4096131769666246e-12, 1.3333546715265303e-12, 1.2954481329556693e-12, 1.2769257986342845e-12, 1.2727969752559767e-12, 1.2822371915505641e-12, 2.821229930633951e-13, 2.821229930633951e-13, 9.545142176139973e-14, 3.2156073783718976e-14, 1.0787092561209404e-14, 3.603545167880893e-15, 1.1988433881459088e-15, 3.972134541138589e-16, 1.3107984900166746e-16, 4.308432121866323e-17, 1.4105684841104198e-17, 4.600238737276539e-18, 1.4945035280724368e-18, 4.836848113787542e-19, 1.5595341828677605e-19, 5.009700640480185e-20, 1.60335633381245e-20, 5.11288707291592e-21, 1.6245616861252033e-21, 5.143462520828592e-22, 1.6227042273848713e-22, 5.101550239945426e-23, 1.5982994108939882e-23, 4.99023517698882e-24, 1.5527591775302929e-24, 4.815266451502689e-25, 1.488271567429775e-25, 4.5846038922348656e-26, 1.407637996310444e-26, 4.307854866500501e-27, 1.3140839195708988e-27, 3.995653170935623e-28, 1.2110594551788732e-28, 3.659031671526164e-29, 1.1020456979480327e-29, 3.3088356720414075e-30, 9.903813466608894e-31, 2.9552534406743966e-31, 8.79249909254516e-32, 2.611876840721646e-32, 7.866748453820105e-33, 2.807937807861988e-33, 2.4897343541112928e-33, 6.412894297136211e-33, 2.1336786059339796e-32, 7.306547572174945e-32, 2.515035677540206e-31, 8.684120259406213e-31, 3.007289583179626e-30, 1.0444320863283046e-29, 3.637764520833359e-29, 1.270670568627847e-28, 4.451129198392355e-28, 1.5636563114413197e-27, 5.5086033172775255e-27, 1.9461047005488956e-26, 6.894630582425686e-26, 2.4494674298625543e-25, 8.726581495295039e-25, 3.1176298746233385e-24, 1.116886818577161e-23, 4.0123094730131237e-23, 1.4453629793639127e-22, 5.220996833425348e-22, 1.891127615394336e-21, 6.868726096099303e-21, 2.501593333124611e-20, 9.13565540811195e-20, 3.3453585303030626e-19, 1.2283530149639029e-18, 4.5225047381945086e-18, 1.6695830999220718e-17, 6.180280947734556e-17, 2.29391690004367e-16, 8.537184470396599e-16, 3.1857928013568695e-15, 1.1920232695749766e-14, 4.4721346239449414e-14, 1.6823123189920694e-13, 6.345397076604935e-13, 2.3997738290864037e-12, 9.099974217275072e-12, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 2.8144721960568983e-11, 2.1514002847231398e-11, 1.64419993296216e-11, 1.2564368592020458e-11] - power_blk_expt = 1.5125953109300022e9 - imag_k2_expt = [0.006259307269295431, 0.005976347265592152, 0.005693513740336182, 0.005410806145990414, 0.005128224634243445, 0.004845770227394191, 0.004563445045070873, 0.004281252608909295, 0.003999198259291266] + power_prf_expt = [0.0, 0.0, 1.4016031926830564e-12, 1.3252327158389514e-12, 1.2870162668501596e-12, 1.2680881334840961e-12, 1.2634868744830177e-12, 1.2723920449064774e-12, 2.700139333088046e-13, 2.700139333088046e-13, 9.135453140439972e-14, 3.077589624238481e-14, 1.032409751431994e-14, 3.448876655072261e-15, 1.1473875813510601e-15, 3.8016457270591504e-16, 1.2545374349729542e-16, 4.1235090092699916e-17, 1.3500251803670166e-17, 4.402790932153665e-18, 1.430357630822549e-18, 4.629244748327109e-19, 1.4925970913368033e-19, 4.794678235714497e-20, 1.534538342612161e-20, 4.893435781788792e-21, 1.5548335355809905e-21, 4.922698894532402e-22, 1.5530558012141815e-22, 4.882585539387812e-23, 1.5296984689356948e-23, 4.776048253436516e-24, 1.4861128774147332e-24, 4.608589397064881e-25, 1.4243931534608167e-25, 4.387827153552935e-26, 1.347220472644357e-26, 4.122956530401987e-27, 1.2576818499209302e-27, 3.8241549088487843e-28, 1.1590793200304743e-28, 3.501981611078859e-29, 1.0547445654520945e-29, 3.166816462678909e-30, 9.478730353074464e-31, 2.8284121594062167e-31, 8.415173815813368e-32, 2.4999722468286247e-32, 7.535872945365564e-33, 2.7103998397967852e-33, 2.461067331488008e-33, 6.40449392644815e-33, 2.1334331756124745e-32, 7.306476076697219e-32, 2.5150336009252767e-31, 8.68411965800176e-31, 3.007289565812955e-30, 1.0444320858282567e-29, 3.6377645206897917e-29, 1.2706705686237368e-28, 4.451129198391182e-28, 1.5636563114412864e-27, 5.508603317277516e-27, 1.9461047005488956e-26, 6.894630582425686e-26, 2.4494674298625543e-25, 8.726581495295039e-25, 3.1176298746233385e-24, 1.116886818577161e-23, 4.0123094730131237e-23, 1.4453629793639127e-22, 5.220996833425348e-22, 1.891127615394336e-21, 6.868726096099303e-21, 2.501593333124611e-20, 9.13565540811195e-20, 3.3453585303030626e-19, 1.2283530149639029e-18, 4.5225047381945086e-18, 1.6695830999220718e-17, 6.180280947734556e-17, 2.29391690004367e-16, 8.537184470396599e-16, 3.1857928013568695e-15, 1.1920232695749766e-14, 4.4721346239449414e-14, 1.6823123189920694e-13, 6.345397076604935e-13, 2.3997738290864037e-12, 9.099974217275072e-12, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 2.8144721960568983e-11, 2.1514002847231398e-11, 1.64419993296216e-11, 1.2564368592020458e-11] + power_blk_expt = 1.5086896530891235e9 + imag_k2_expt = [0.006235304368310926, 0.0059553773333793384, 0.005675317002289581, 0.005395132786467927, 0.005114835104873023, 0.004834435546425278, 0.004553947086848024, 0.004273384382550748, 0.00399276417566161] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg diff --git a/test/test.toml b/test/test.toml index 567b0a0..4f4363e 100644 --- a/test/test.toml +++ b/test/test.toml @@ -107,3 +107,5 @@ version = "1.0" # Version of this configuration file [struct] mass_tot = 1.0 # M_earth core_density = 10738.33 # Core density [kg m-3] + core_shear = 0.0 # Core viscosity [Pa s] + core_bulk = 5e11 # Core bulk modulus [Pa] From 15abe7f73a848e634fdec6fcc6ae16cb09019db9 Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 10 May 2026 10:05:09 +0000 Subject: [PATCH 28/36] Implemented consistent data types --- src/Obliqua.jl | 279 +++++++++---------------------- src/common.jl | 339 ++++++++++++++++++++++++-------------- src/solid1d.jl | 4 +- src/solid1d_mush.jl | 10 +- src/solid1d_mush_relax.jl | 169 +++++++++++-------- src/solid1d_relax.jl | 82 +++++---- 6 files changed, 442 insertions(+), 441 deletions(-) diff --git a/src/Obliqua.jl b/src/Obliqua.jl index a6ba12c..a9d21b2 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -435,12 +435,12 @@ module Obliqua μc = complex_mu(σ_range, μ, η; material=material, α=alpha) # initiate forcing frequency dependent k2 love and load number arrays (one spectrum for each segment) - knms_T = zeros(precc, N_σ, length(segments)) - knms_L = zeros(precc, N_σ, length(segments)) + knms_T = zeros(ComplexF64, N_σ, length(segments)) + knms_L = zeros(ComplexF64, N_σ, length(segments)) # initiate forcing frequency dependent heating profile - prf_total = zeros(prec, N_σ, N_layers) - map_total = zeros(prec, N_σ, length(segments), length(collect(0:res:180)), length(collect(0:res:360-0.001))) + prf_total = zeros(Float64, N_σ, N_layers) + map_total = zeros(Float64, N_σ, length(segments), length(collect(0:res:180)), length(collect(0:res:360-0.001))) # core density for bottom boundary ρ_mean_lower = ρ_core @@ -461,10 +461,10 @@ module Obliqua # preallocate (complex for viscoelastic) # (T)idal love number - knms_T_seg = zeros(precc, N_σ) + knms_T_seg = zeros(ComplexF64, N_σ) # (L)oad love number - knms_L_seg = zeros(precc, N_σ) + knms_L_seg = zeros(ComplexF64, N_σ) # get start and stop index for segment i_start, i_end = is_seg[iseg] @@ -479,8 +479,8 @@ module Obliqua g_seg = g[i_start:i_end] # preallocate heating profile for segment - prf_seg = zeros(prec, N_σ, length(r_seg)-1) - map_seg = zeros(prec, N_σ, length(collect(0:res:180)), length(collect(0:res:360-0.001))) + prf_seg = zeros(Float64, N_σ, length(r_seg)-1) + map_seg = zeros(Float64, N_σ, length(collect(0:res:180)), length(collect(0:res:360-0.001))) # mean density in current segment if length(ρ_seg) == 1 @@ -501,8 +501,8 @@ module Obliqua iszero(σ) && continue # preallocate k2 for segment - kT = zero(precc) - kL = zero(precc) + kT = zero(ComplexF64) + kL = zero(ComplexF64) # if segment is solid if seg == "solid" @@ -823,15 +823,15 @@ module Obliqua # calculate tidal heating # initialize frequency dependent quentities - A_nms_e = zeros(prec, length(s_range)) - U_nms_e = zeros(precc, length(s_range)) + A_nms_e = zeros(Float64, length(s_range)) + U_nms_e = zeros(ComplexF64, length(s_range)) # initialize frequency dependent total heating - P_T_s_blk = zeros(prec, length(s_range)) + P_T_s_blk = zeros(Float64, length(s_range)) # initialize frequency dependent heating profile - P_T_s_prf = zeros(prec, length(s_range), length(shear)) - P_T_s_map = zeros(prec, length(s_range), length(collect(0:res:180)), length(collect(0:res:360-0.001))) + P_T_s_prf = zeros(Float64, length(s_range), length(shear)) + P_T_s_map = zeros(Float64, length(s_range), length(collect(0:res:180)), length(collect(0:res:360-0.001))) # loop over tidal modes for (iss, ss) in pairs(s_range) @@ -905,7 +905,7 @@ module Obliqua P_T_prf ./ ρ # convert to mass heating rate (W/kg) # convert everything to Float64 - return Float64.(P_T_prf), P_T_blk, Float64.(σ_range), Float64.(imag_k2) + return Float64.(P_T_prf), Float64(P_T_blk), Float64.(σ_range), Float64.(imag_k2) end @@ -1122,14 +1122,14 @@ module Obliqua - `n::Int=2` : Power of the radial factor (goes with (r/a)^{n}, since r< Date: Sun, 10 May 2026 12:01:34 +0000 Subject: [PATCH 29/36] Updated tests --- res/interior_data/runtests_mantle.json | 2 +- src/common.jl | 2 +- test/runtests.jl | 65 ++++++++++++++------------ test/test.toml | 2 +- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/res/interior_data/runtests_mantle.json b/res/interior_data/runtests_mantle.json index 9cae3e3..97b34c0 100644 --- a/res/interior_data/runtests_mantle.json +++ b/res/interior_data/runtests_mantle.json @@ -216,7 +216,7 @@ 9.99986800867969e+21, 9.999977507085548e+21, 9.999972511551995e+21, - 2.057646520237794e+21, + 5903717639502.909, 5903717639502.909, 4796274140547.893, 3909525547701.5, diff --git a/src/common.jl b/src/common.jl index c6521f0..4fc0fff 100644 --- a/src/common.jl +++ b/src/common.jl @@ -273,7 +273,7 @@ module common if type=="liquid" Ic[Y[1],1] = -r^n / g - Ic[Y[1],1] = -r^n / g + Ic[Y[1],3] = 1.0 Ic[Y[2],2] = 1.0 Ic[Y[3],3] = g*ρ Ic[Y[5],1] = r^n diff --git a/test/runtests.jl b/test/runtests.jl index c357ef5..f14f418 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -152,9 +152,9 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 3.3836853500831803e-15, 3.199107433705875e-15, 3.1066684894960716e-15, 3.0607994210208172e-15, 3.049518217180047e-15, 3.0708441754457593e-15, 6.484028375243808e-16, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 48799.97241357575 - imag_k2_expt = [6.68650424205084e-5] + power_prf_expt = [0.0, 0.0, 7.12546350513748e-15, 7.141653473320296e-15, 7.266955578280898e-15, 7.413341032856918e-15, 7.5585214768856e-15, 7.702021368425093e-15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 107761.54726903763 + imag_k2_expt = [0.00014765337095640072] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -190,9 +190,9 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 8.38538695455464e-19, 7.568669610705781e-19, 7.289216195147812e-19, 7.17739811066603e-19, 7.147892586185959e-19, 7.194534263837449e-19, 2.6571851057720503e-25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 11.123362128828322 - imag_k2_expt = [1.5241075841179643e-8] + power_prf_expt = [0.0, 0.0, 1.722694782808758e-18, 1.6491571129537458e-18, 1.6649092230686594e-18, 1.6982534724631658e-18, 1.7317130709612992e-18, 1.764826259885598e-18, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 24.92921722417735 + imag_k2_expt = [3.415766617812618e-8] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -222,11 +222,11 @@ if suite > 2 end if suite > 2 - # test solid1d module with andrade rheology + # test solid1d-relax module with andrade rheology @info " " @info "Testing solid1d-relax module with andrade rheology" - # update config to use only solid1d + # update config to use only solid1d-relax cfg["orbit"]["obliqua"]["module_solid"] = "solid1d-relax" cfg["orbit"]["obliqua"]["module_fluid"] = "none" cfg["orbit"]["obliqua"]["module_mushy"] = "none" @@ -236,9 +236,9 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 1.2500871386903165e-14, 1.3490093179591017e-14, 1.379776907343251e-14, 1.4367432888221434e-14, 1.5036981510041056e-14, 1.576511111912771e-14, 1.6552497289716098e-14, 1.8304993632021006e-15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 248561.9899812635 - imag_k2_expt = [0.0003405761762193043] + power_prf_expt = [0.0, 7.750115499856373e-15, 8.057499722716283e-15, 8.059575850362661e-15, 8.185436835049663e-15, 8.33553548214869e-15, 8.484799440335237e-15, 8.632786806517406e-15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 137743.23201529591 + imag_k2_expt = [0.0001887338577527246] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -274,9 +274,9 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 3.061317898445062e-18, 3.31954900407594e-18, 3.2426735038389572e-18, 3.3496980944013153e-18, 3.505053284163295e-18, 3.674843274104151e-18, 3.85852228062806e-18, 7.519018418066546e-25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 57.809566146923785 - imag_k2_expt = [7.92098622508629e-8] + power_prf_expt = [0.0, 1.8887574959136746e-18, 1.972957870150714e-18, 1.884584844212067e-18, 1.8986419071321904e-18, 1.9329191192829336e-18, 1.967464038487986e-18, 2.0017743960506084e-18, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 32.41911211996522 + imag_k2_expt = [4.44202158305658e-8] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -306,23 +306,26 @@ if suite > 2 end if suite > 4 - # test solid1d module with andrade rheology + # test solid1d-mush module with andrade rheology @info " " @info "Testing solid1d-mush module with andrade rheology" - # update config to use only solid1d + # update config to use only solid1d-mush cfg["orbit"]["obliqua"]["module_solid"] = "solid1d-mush" cfg["orbit"]["obliqua"]["module_fluid"] = "none" cfg["orbit"]["obliqua"]["module_mushy"] = "none" cfg["orbit"]["obliqua"]["material"] = "andrade" + # lower visc_sus to include mush + visc_sus = cfg["orbit"]["obliqua"]["visc_sus"] = 5e10 + omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 4.950610209920039e-14, 4.567639539072659e-14, 4.2821578796970674e-14, 4.0271551813907144e-14, 3.787137048538955e-14, 3.560979144308022e-14, 1.2496829309351557e-11, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 4.8052844153493536e8 - imag_k2_expt = [0.6584133768599382] + power_prf_expt = [0.0, 0.0, 9.86707030745882e-15, 9.150910190756036e-15, 8.622765164513395e-15, 8.150013792655512e-15, 7.702026907818404e-15, 7.276882015209697e-15, 2.36961014227723e-14, 1.9566536667394827e-14, 1.6121402444527615e-14, 7.538539756442404e-15, 8.508094326483907e-15, 3.568025886298421e-15, 3.582082099457579e-15, 1.8450607517355677e-15, 1.3451995999757726e-15, 8.725598622614597e-16, 4.877653976477049e-16, 3.636754700375336e-16, 1.762407992723698e-16, 1.343339281131328e-16, 6.412353878778215e-17, 4.3760705656795036e-17, 2.3569631734565316e-17, 1.2275542502234671e-17, 8.578724675042095e-18, 2.996216880865113e-18, 2.818774227616443e-18, 7.725592763205111e-19, 7.291475505535899e-19, 4.578348846472072e-19, 1.7918454344299687e-18, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 3.0395423054335793e6 + imag_k2_expt = [0.004164738526270287] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -358,9 +361,9 @@ if suite > 4 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 1.200063876496574e-17, 1.0577506483396584e-17, 9.837824669298708e-18, 9.24980671498098e-18, 8.698457949883253e-18, 8.179102199913946e-18, 1.2481596843673565e-11, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - power_blk_expt = 4.8114338657619107e8 - imag_k2_expt = [0.6592559659893207] + power_prf_expt = [0.0, 0.0, 2.811270711855333e-18, 2.4873681738050708e-18, 2.322121690521703e-18, 2.191368048585138e-18, 2.0681585759977523e-18, 1.9514616381743257e-18, 9.401030519482231e-17, 1.3496470773634048e-16, 6.582027976570243e-17, 7.489744673034899e-17, 6.156680255962594e-17, 3.460102640163939e-17, 4.550106850237844e-17, 1.8331157658567176e-17, 2.6909888276869764e-17, 1.138847035948234e-17, 1.405393299838946e-17, 6.943151792071354e-18, 6.977590742356405e-18, 3.80235936859e-18, 3.4214575190463734e-18, 1.8384897659114318e-18, 1.6702466275884794e-18, 7.837030357462777e-19, 7.983327480776783e-19, 3.0102026582678284e-19, 3.561792052557227e-19, 1.152470964370035e-19, 1.3751179251924843e-19, 9.509403483402107e-20, 9.061242335085774e-19, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 76721.32337867716 + imag_k2_expt = [0.00010512248857021226] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -387,6 +390,8 @@ if suite > 4 total += 1 @info "--------------------------" + visc_sus = cfg["orbit"]["obliqua"]["visc_sus"] = 5e13 + end if suite > 2 @@ -404,8 +409,8 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_blk_expt = 5.173115797760875e6 - imag_k2_expt = [0.007088131204911426] + power_blk_expt = 5.167672882253202e6 + imag_k2_expt = [0.007080673397902285] _, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -440,9 +445,9 @@ if suite > 2 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 9.038225140970252e-14, 9.038225140970252e-14, 9.038225140970252e-14, 9.038225140970252e-14, 7.352121265265731e-14, 5.620007993230152e-14, 4.295071065802233e-14, 3.282134829196775e-14] - power_blk_expt = 5.173115797760875e6 - imag_k2_expt = [0.007088131204911426] + power_prf_expt = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 9.028715534438163e-14, 9.028715534438163e-14, 9.028715534438163e-14, 9.028715534438163e-14, 7.344385700006137e-14, 5.6140948782225885e-14, 4.2905519887460544e-14, 3.2786815172550926e-14] + power_blk_expt = 5.167672882253202e6 + imag_k2_expt = [0.007080673397902285] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg @@ -487,9 +492,9 @@ if suite > 10 omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) - power_prf_expt = [0.0, 0.0, 1.4016031926830564e-12, 1.3252327158389514e-12, 1.2870162668501596e-12, 1.2680881334840961e-12, 1.2634868744830177e-12, 1.2723920449064774e-12, 2.700139333088046e-13, 2.700139333088046e-13, 9.135453140439972e-14, 3.077589624238481e-14, 1.032409751431994e-14, 3.448876655072261e-15, 1.1473875813510601e-15, 3.8016457270591504e-16, 1.2545374349729542e-16, 4.1235090092699916e-17, 1.3500251803670166e-17, 4.402790932153665e-18, 1.430357630822549e-18, 4.629244748327109e-19, 1.4925970913368033e-19, 4.794678235714497e-20, 1.534538342612161e-20, 4.893435781788792e-21, 1.5548335355809905e-21, 4.922698894532402e-22, 1.5530558012141815e-22, 4.882585539387812e-23, 1.5296984689356948e-23, 4.776048253436516e-24, 1.4861128774147332e-24, 4.608589397064881e-25, 1.4243931534608167e-25, 4.387827153552935e-26, 1.347220472644357e-26, 4.122956530401987e-27, 1.2576818499209302e-27, 3.8241549088487843e-28, 1.1590793200304743e-28, 3.501981611078859e-29, 1.0547445654520945e-29, 3.166816462678909e-30, 9.478730353074464e-31, 2.8284121594062167e-31, 8.415173815813368e-32, 2.4999722468286247e-32, 7.535872945365564e-33, 2.7103998397967852e-33, 2.461067331488008e-33, 6.40449392644815e-33, 2.1334331756124745e-32, 7.306476076697219e-32, 2.5150336009252767e-31, 8.68411965800176e-31, 3.007289565812955e-30, 1.0444320858282567e-29, 3.6377645206897917e-29, 1.2706705686237368e-28, 4.451129198391182e-28, 1.5636563114412864e-27, 5.508603317277516e-27, 1.9461047005488956e-26, 6.894630582425686e-26, 2.4494674298625543e-25, 8.726581495295039e-25, 3.1176298746233385e-24, 1.116886818577161e-23, 4.0123094730131237e-23, 1.4453629793639127e-22, 5.220996833425348e-22, 1.891127615394336e-21, 6.868726096099303e-21, 2.501593333124611e-20, 9.13565540811195e-20, 3.3453585303030626e-19, 1.2283530149639029e-18, 4.5225047381945086e-18, 1.6695830999220718e-17, 6.180280947734556e-17, 2.29391690004367e-16, 8.537184470396599e-16, 3.1857928013568695e-15, 1.1920232695749766e-14, 4.4721346239449414e-14, 1.6823123189920694e-13, 6.345397076604935e-13, 2.3997738290864037e-12, 9.099974217275072e-12, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 3.459931207093036e-11, 2.8144721960568983e-11, 2.1514002847231398e-11, 1.64419993296216e-11, 1.2564368592020458e-11] - power_blk_expt = 1.5086896530891235e9 - imag_k2_expt = [0.006235304368310926, 0.0059553773333793384, 0.005675317002289581, 0.005395132786467927, 0.005114835104873023, 0.004834435546425278, 0.004553947086848024, 0.004273384382550748, 0.00399276417566161] + power_prf_expt = [0.0, 0.0, 2.9507745130337324e-12, 2.957492544894895e-12, 3.0093798848852802e-12, 3.0699942838630136e-12, 3.1301087443252187e-12, 3.189526446012955e-12, 3.189526446012955e-12, 1.0963618378318885e-12, 3.7524391601966574e-13, 1.278880490634037e-13, 4.340368867102278e-14, 1.4669889562954858e-14, 4.938022630587577e-15, 1.6554924192323772e-15, 5.528030698211065e-16, 1.8386685971776486e-16, 6.091808976258348e-17, 2.0105661404428437e-17, 6.610565688410192e-18, 2.1653365808715678e-18, 7.066372826281614e-19, 2.2975730444184624e-19, 7.443214658305481e-20, 2.402627776885429e-20, 7.727926641765474e-21, 2.4768823612742404e-21, 7.910948363947936e-22, 2.5179490667963084e-22, 7.986832225390398e-23, 2.524788461375205e-23, 7.95447314090666e-24, 2.497736398632058e-24, 7.81705067304542e-25, 2.43844179976677e-25, 7.581700680138862e-26, 2.3497243537257714e-26, 7.258955978439079e-27, 2.2353675605634917e-27, 6.86201250668302e-28, 2.099866879583562e-28, 6.405887712936706e-29, 1.948154896774264e-29, 5.906543012799335e-30, 1.7853323147817247e-30, 5.380272249605189e-31, 1.6172101556799416e-31, 4.8696574173948217e-32, 1.5395902009428098e-32, 7.449587884676381e-33, 1.1867910509433857e-32, 3.615139994984785e-32, 1.213099710206271e-31, 4.11842994772204e-31, 1.4033105735719724e-30, 4.795701640364832e-30, 1.6435941328164814e-29, 5.649022188495877e-29, 1.947075293104084e-28, 6.730043193775503e-28, 2.3327794465513773e-27, 8.108593064357973e-27, 2.8263685143966506e-26, 9.879140502313731e-26, 3.4626807127680235e-25, 1.217039381153432e-24, 4.289353781166347e-24, 1.5158997325577025e-23, 5.372011529522199e-23, 1.9089206788068747e-22, 6.801741207805623e-22, 2.4301357661387006e-21, 8.705951947519668e-21, 3.1273357857633184e-20, 1.1264259524532833e-19, 4.068161322472987e-19, 1.473189801198765e-18, 5.3491160810343616e-18, 1.9474486452782743e-17, 7.10900292213008e-17, 2.6020054102308763e-16, 9.549107380509521e-16, 3.513749608145839e-15, 1.2963753981674927e-14, 4.795581609850411e-14, 1.7786931534026058e-13, 6.614679959444938e-13, 2.46640058030969e-12, 9.220710510053815e-12, 3.4562908259458124e-11, 3.4562908259458124e-11, 3.4562908259458124e-11, 3.4562908259458124e-11, 3.4562908259458124e-11, 2.8115109373183124e-11, 2.149136679880193e-11, 1.6424699810989694e-11, 1.2551148938850651e-11] + power_blk_expt = 2.4310813508705845e9 + imag_k2_expt = [0.01000287153467124, 0.009561732793922827, 0.00912095768370095, 0.008680565130249904, 0.00824057872897682, 0.007801027901476037, 0.007361949428107609, 0.006923389510482543, 0.0064854065968675805] power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg diff --git a/test/test.toml b/test/test.toml index 4f4363e..759f2cc 100644 --- a/test/test.toml +++ b/test/test.toml @@ -107,5 +107,5 @@ version = "1.0" # Version of this configuration file [struct] mass_tot = 1.0 # M_earth core_density = 10738.33 # Core density [kg m-3] - core_shear = 0.0 # Core viscosity [Pa s] + core_shear = 0.0 # Core shear [Pa] core_bulk = 5e11 # Core bulk modulus [Pa] From 80cef74c85c59e7dfa6a80471e62196dfa7595ca Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 10 May 2026 12:58:10 +0000 Subject: [PATCH 30/36] Updated docs. --- docs/make.jl | 10 +- docs/src/explanation/images/tech_view.png | Bin 0 -> 94964 bytes docs/src/explanation/iter_proc.md | 29 + docs/src/explanation/main_loop.md | 103 ++ docs/src/explanation/post_proc.md | 106 ++ docs/src/explanation/technical_overview.md | 8 + docs/src/reference/solid-phase.md | 1017 ++++++++--------- .../solid/images/solid1d_mush_relax.png | Bin 0 -> 103543 bytes .../src/reference/solid/solid1d_mush_relax.md | 450 +++++++- docs/src/reference/tidal-potentials.md | 51 +- 10 files changed, 1209 insertions(+), 565 deletions(-) create mode 100644 docs/src/explanation/images/tech_view.png create mode 100644 docs/src/explanation/iter_proc.md create mode 100644 docs/src/explanation/main_loop.md create mode 100644 docs/src/explanation/post_proc.md create mode 100644 docs/src/explanation/technical_overview.md create mode 100644 docs/src/reference/solid/images/solid1d_mush_relax.png diff --git a/docs/make.jl b/docs/make.jl index 80a7d8c..9791727 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -40,10 +40,16 @@ makedocs( "4 - Mush layer" => "reference/mush-layer.md", "5 - Liquid-phase" => "reference/liquid-phase.md", "6 - Surface Loading" => "reference/surface-loading.md", - "7 - Tidal Potentials" => "reference/tidal-potentials.md" + "7 - Tidal potential" => "reference/tidal-potential.md" + ] + ), + PageNode("Explanation" => "explanation/index.md", [ + "1 - Technical overview" => "explanation/technical_overview.md", + "2 - Main loop" => "explanation/main_loop.md", + "3 - Iterative process" => "explanation/iter_proc.md", + "4 - Post-processing" => "explanation/post_proc.md" ] ), - "Explanation" => "explanation/index.md", "Troubleshooting" => "troubleshooting.md", "Development" => "development.md", "Related codes" => "ecosystem.md" diff --git a/docs/src/explanation/images/tech_view.png b/docs/src/explanation/images/tech_view.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0235519b74ada3a77f2de3dfe5f2b1d8588125 GIT binary patch literal 94964 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%0D=?QmyA*R@uQU|wLEoZ* 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 literal 0 HcmV?d00001 diff --git a/docs/src/explanation/iter_proc.md b/docs/src/explanation/iter_proc.md new file mode 100644 index 0000000..ffb67d8 --- /dev/null +++ b/docs/src/explanation/iter_proc.md @@ -0,0 +1,29 @@ + +### Iterative Process + +Now that all the required quantities and settings have been determined +and checked, we can move to the actual tidal calculation. Obliqua loops +over the segments, at each step it updates the mean density of the layer +below the current for the density contrast (or density ratio) +calculation. Additionally, specifically for the fluid models we include +an efficiency term to account for the reduced dissipation between the +fluid mantle and the fluid core. Note that at the moment an intermediate +mush segment forms, the efficiency is set back to 1. Within each segment +it loops over all the forcing frequencies. + +For each forcing frequency the code calls the corresponding tides model +(e.g. solid, fluid, mush). For now these models basically act as sort of +black boxes where some interior properties are provided and some forcing +frequency is given, the model then returns the expected planet-wide +deformation, a.k.a. the $n$th degree Lovenumber $k_n(\sigma)$; the +planet-wide loading Lovenumber $k'_n(\sigma)$; and the normalized +heating profile in the segment. All the details regarding these models +will be given in the corresponding sections below. The currently +available tidal models are "solid0d", "solid1d", "solid1d_relax", +"solid1d_mush", "solid1d_mush_relax"; "fluid0d", "fluid1d"; "interp", +"none". For details see the Reference documantation. + +The "interp" model requires knowledge of heating at both interfaces, as +such an additional code block is included to update the heating in the +"interp" region during the tidal calculation in the next segment after +the "interp" region. diff --git a/docs/src/explanation/main_loop.md b/docs/src/explanation/main_loop.md new file mode 100644 index 0000000..89a298f --- /dev/null +++ b/docs/src/explanation/main_loop.md @@ -0,0 +1,103 @@ + +### Main loop + +Now that the user is able to interface with Obliqua, we will shift our +focus to the inner workings of the main function. Before starting the +main iteration, Obliqua checks if all required configuration parameters +are specified. Subsequently, it loads the configuration into the +variables used by the code. Some configuration parameters may be set to +"none", the function `nothing_if_none` converts these strings to nothing +type elements in Julia. The interior arrays are then converted to +Bigfloat, this helps stabilize the solver and allows it to cover even +large discontinuities. + +# Segments + +The interior is subdivided into so-called "segments". These segments are +identified based on their local viscosity, any segment below the +liquidus viscosity is liquid, and above the solidus is solid. In between +these the formalism employed is mush. Notably, the advised choice it +setting the solidus viscosity to $\sim 10^{5}$ Pa s, allowing for +solid-like viscous dissipation and compaction to characterize the +dissipation in the mush. Only at very low viscosities does one expect +fluid waves, as such the liquidus may be set to as low as $\sim 10^2$ Pa +s. To bridge the region between these models one can either further +extend the solid formalism, or use the "interp" model in the mush, +allowing for a more smooth transition in the heating profile. + +# Core + +The code assumes a uniform core. The effects on the local gravity are +calculated and the density for the fluid density contrast are set. + +# $k$-range + +The tidal potential can be expanded in an infinite sum of spherical +harmonic terms with some degree $n$, order $m$, and harmonic $k$ +(integer values). The user has the option the provide these quantities +in the configuration. However, specifically the $k$-range may also be +determined by the code. The desired range increases with eccentricity, +for our purposes the desired range $K$ contains + +```math +\{k \in K \, \forall \, k : X^{-(n+1), m}_k \geq 0.01\ | k \in Z\} +``` + +where $X^{-(n+1), m}_k$ is the Hansen coefficient. Basically, we only +include $k$ in the range $K$ if the corresponding Hansen coefficient +signifies a contribution greater than 1% to the complete tidal response. +One may specify a different criterion and generate their $K$ using the +included Notebook on the Obliqua Github repository +"/examples/hansen_k_table.ipynb". + +For testing convenience Obliqua includes two modes: "full" and +"adaptive". In principle, one should use "full" to test the tidal +calculation over a large range of forcing frequencies, and use +"adaptive" when using Obliqua with a PROTEUS-like framework. The former +will determine the planet response to all relevant forcing frequencies +and assumes $k = 1$ for every forcing frequency, while the latter only +calculates the response at the exact forcing frequencies excited by the +specific orbital configuration using $K$. + +# Forcing frequency + +The forcing frequencies $\sigma$ are simply obtained for every order $m$ +and harmonic $k$ as + +```math +\sigma = m \Omega - k \omega. +``` + +# Complex shear modulus + +The frequency response of the mantle is computed assuming a homogeneous +viscoelastic mantle. Obliqua implements two rheological models: Maxwell +and Andrade. Unlike the Maxwell model, which captures only +elastic--viscous behavior through a single Maxwell timescale, the +Andrade rheology includes a transient anelastic component. This yields +an elastic response at high forcing frequencies, a viscous response at +low frequencies governed by the Maxwell timescale $\tau_M = \eta / \mu$, +and a smooth anelastic transition in between characterized by the +Andrade timescale $\tau_A$ and creep exponent $\alpha$. As a result, the +Andrade rheology reproduces the observed attenuation behavior of +planetary mantles more accurately, particularly at high tidal forcing +frequencies. The rheologies are incorporated via their definitions of +the complex shear modulus. For the Andrade rheology, the complex shear +modulus is + +```math +\tilde{\mu}(\sigma) = +\frac{\mu}{ +1 + (\mathrm{i}\sigma\eta/\mu)^{-\alpha}\Gamma(1+\alpha) + + (\mathrm{i}\sigma\eta/\mu)^{-1}}, \qquad \alpha = 0.3, +``` + +where $\mu$ is the elastic shear modulus and $\Gamma$ the Gamma +function. + +For the Maxwell limit $\alpha = 0$ and + +```math +\tilde{\mu}(\sigma) = +\frac{\mu}{1 + (\mathrm{i}\sigma\eta/\mu)^{-1}}. +``` diff --git a/docs/src/explanation/post_proc.md b/docs/src/explanation/post_proc.md new file mode 100644 index 0000000..52a4ce9 --- /dev/null +++ b/docs/src/explanation/post_proc.md @@ -0,0 +1,106 @@ + +### Post-processing + +The final steps involve collecting the Lovenumbers and heating, and +applying the corresponding scalings to the normalized quantities. + +The Lovenumbers can be combined using the following relation + +```math +k_n(\sigma) = k_{n}^{(\mathrm{solid})}(\sigma) + \bigl[1 + k'_{n}(\sigma)\bigr]\,k_{n}^{(\mathrm{fluid})}(\sigma). +``` + +Note that this relation implicitly assumes a solid interior covered by a +fluid magma ocean. For our purposes this assumption is fine. In the +special case where there is a subsurface fluid-like magma ocean, we may +include a correction (see Farhat 2025) due to a lithosphere. However, if +the subsurface magma ocean occurs deep in the mantle, it may be better +modeled using the solid formalism. More appropriate would be to use of +the "solid1d_mush" models as it accounts also for pore pressure in +partial melts. I have not yet encountered a case with a fluid-like deep +subsurface magma ocean. Despite this, it should be noted that in this +extreme scenario the model will likely not be able to properly represent +dissipation in the fluid, since there is currently no way to properly +combine the Lovenumbers. + +Next we shall construct the global heating profile and bulk heating. For +the latter we require the imaginary part of the global tidal Lovenumber + +```math +-\Im[k_n(\sigma)] +``` + +If earlier we had define the spectrum "full" then at this point we shall +assume that $k=1$ and determine the corresponding Hansen coefficient +(note that we have to make this assumption here since there is no +meaningful choice for $k$ since we picked an arbitrary forcing frequency +range.) + +$$X_1^{-(n+1),m}(e)$$ + +and normalization + +```math +A_{n,m,1} = (2 - \delta_{m,0}\delta_{1,0}) (1 - \delta_{m,0}\delta_{1<0}) \sqrt{\frac{2 {\times 2\pi}}{2n+1}\frac{(n-m)!}{(n+m)!}} P_n^m(0) X_1^{-(n+1),m}(e). +``` + +The tidal potential is + +$$U_{n,m,1} = \frac{GM}{a} \left(\frac{R}{a}\right)^n A_{n,m,1}(e)$$ + +The prefactor is $$\text{prefactor} \, = \frac{(2n + 1)R}{8πG} \sigma$$ The +normalized heating profile is then simply + +$$H(r, \sigma) = \tilde{H}(r, \sigma) \times |U_{n,m,1}|^2$$ + +where $\tilde{H}(r, \sigma)$ is the unormalized heating profile returned by +the black box tides models. The global heating follows from + +$$H(\sigma) = \text{prefactor} \times (-\Im[k_n(\sigma)]) \times |U_{n,m,1}|^2$$ + +If instead we had defined the spectrum "adaptive" then at this point we +will loop over the $(n,m,k)$-pairs and determine all heating based on +the most general expressions without making any assumptions (actually, +we still assume co-planarity, hence no obliquity tides :o). For every +pair we have + +$$X_k^{-(n+1),m}(e)$$ + +and normalization + +$$A_{n,m,k} = (2 - \delta_{m,0}\delta_{k,0}) (1 - \delta_{m,0}\delta_{k<0}) \sqrt{\frac{2 {\times 2\pi}}{2n+1}\frac{(n-m)!}{(n+m)!}} P_n^m(0) X_k^{-(n+1),m}(e).$$ + +The tidal potential is + +$$U_{n,m,k} = \frac{GM}{a} \left(\frac{R}{a}\right)^n A_{n,m,k}(e)$$ + +The prefactor is + +$$\text{prefactor} \, = \frac{(2n + 1)R}{8πG} \sigma$$ + +The normalized heating profile is then simply + +$$H(r, \sigma) = \tilde{H}(r, \sigma) \times |U_{n,m,k}|^2$$ + +where $\tilde{H}(r, \sigma)$ is the unormalized heating profile returned by +the black box tides models. The global heating follows from + +$$H(\sigma) = \text{prefactor} \times (-\Im[k_n(\sigma)]) \times |U_{n,m,k}|^2$$ + +Finally, in both spectrum cases, the heating rates can be integrated +across the forcing frequency domain + +$$H(r) = \int H(r, \sigma) d\sigma$$ + +and + +$$H = \int H(\sigma) d\sigma$$ + +(note the integral is actually just a +sum, given the discrete finite set of forcing frequencies.) The +resulting heating profile and global heating rates can be (and should +be!) compared through integration of the radial heating profile. They +should match as long as the surface load Lovenumber +$k'_n(\sigma) \ll 1$. For coupling to orbital dynamics we also return +the tidal Lovenumbers $k_n(\sigma)$ and corresponding forcing +frequencies $\sigma$. diff --git a/docs/src/explanation/technical_overview.md b/docs/src/explanation/technical_overview.md new file mode 100644 index 0000000..ca6427f --- /dev/null +++ b/docs/src/explanation/technical_overview.md @@ -0,0 +1,8 @@ + +### Technical overview + +We will now turn to the technical details that make Obliqua tick. Firstly, a step-by-step breakdown is given describing the main processes present in Obliqua. + +![Technical Overview](images/tech_view.png) + +The processes and quantities listed in in the figure will be further discussed in subsequent sections. \ No newline at end of file diff --git a/docs/src/reference/solid-phase.md b/docs/src/reference/solid-phase.md index bb39d61..01ac69c 100644 --- a/docs/src/reference/solid-phase.md +++ b/docs/src/reference/solid-phase.md @@ -1,568 +1,497 @@ -### Reference (2) +### Solid-Phase -# Solid-Phase +We will now provide the complete theoretical background used within the +tidal models. Admittedly, this section will get quite complicated, but I +will try to formulate things in a clear way! We will start with the +original solid tides model, namely "Lovepy" or as we refer to it +"solid1d". But first let us establish some core concepts. -To describe tidal and rotational deformations of a spherically symmetric body, `Obliqua` considers the spheroidal displacement–stress–gravity system. For each harmonic degree (``\ell``) and order (``m``), the spheroidal perturbed state is represented by the 6-vector +In gravito-elastic tidal theory the response of a medium is represented +by so called $y$-functions. These functions form a state vector at every +radius $\pmb{y}(r)$. These functions also depend on the $(n,m)$-pair; +$\pmb{y_{n,m}}(r)$. The solid tide modules incorporated into Obliqua +make use of the spheroidal 6--vector solution and the spheroidal +8--vector solution, where the latter is used specifically for the +poro-visco elastic models. Generally the first six $y$-functions are +labeled as follows -```math - \mathbf{y}_{\ell m} = - \begin{pmatrix} - U_{\ell m} \ - V_{\ell m} \ - R_{\ell m} \ - S_{\ell m} \ - \Phi_{\ell m} \ - Q_{\ell m} - \end{pmatrix}, -``` +$$\pmb{y}_{n,m} = (U_{n,m} , V_{n,m} , X_{n,m} , Y_{n,m}, \Phi_{n,m} , \Psi_{n,m} )^T$$ -where - -| Component | Physical Meaning | Units (SI) | Normalization Scale | Notes | -| --------------- | ---------------------------------------------------------------------------------------------- | ---------- | ------------------- | ------------------------------- | -| ``U_{\ell m}`` | Radial displacement | m | ``R_0`` | Typical planetary radius | -| ``V_{\ell m}`` | Tangential displacement | m | ``R_0`` | Same as radial displacement | -| ``R_{\ell m}`` | Radial stress | Pa | ``\mu_0`` | Characteristic shear modulus | -| ``S_{\ell m}`` | Tangential stress | Pa | ``\mu_0`` | Same as radial stress | -| ``\Phi_{\ell m}`` | Gravitational potential perturbation | m``^2/``s``^2`` | ``g_0 R_0`` | ``g_0`` is characteristic gravity | -| ``Q_{\ell m}`` | Potential stress | m/s``^2`` | ``g_0`` | Same units as gravity | - -The spheroidal vector satisfies the first-order ODE system - -```math -\frac{d\mathbf{y}_{\ell m}}{dr} -= \mathbf{A}_{\ell}(r)\,\mathbf{y}_{\ell m}(r). -``` - -Here, the coefficient matrix ``\mathbf{A}_{\ell}(r)`` represents the responds of the mantle to deformations, and is given by - -```math -\mathbf{A}_\ell(r) = -\begin{pmatrix} --\frac{2\lambda}{r\beta} & -\frac{\ell(\ell+1)\lambda}{r\beta} & -\frac{1}{\beta} & 0 & 0 & 0 \\[1.2em] - --\frac{1}{r} & -\frac{1}{r} & -0 & -\frac{1}{\mu} & -0 & 0 \\[1.2em] - -\frac{4}{r}\!\left( \frac{3\kappa\mu}{r\beta} - \rho_{0}g \right) - \rho_{0}\omega^{2} & -\frac{\ell(\ell+1)}{r}\!\left(\rho_{0}g - \frac{6\kappa\mu}{r\beta}\right) & --\frac{4\mu}{r\beta} & -\frac{\ell(\ell+1)}{r} & --\frac{\rho_{0}(\ell+1)}{r} & -\rho_{0} \\[1.2em] - -\frac{1}{r}\!\left(\rho_{0}g - \frac{6\mu\kappa}{r\beta}\right) & -\frac{2\mu}{r^{2}}\!\left[\ell(\ell+1)\!\left(1+\frac{\lambda}{\beta}\right)-1\right] - \rho_{0}\omega^{2} & --\frac{\lambda}{r\beta} & --\frac{3}{r} & -\frac{\rho_{0}}{r} & -0 \\[1.2em] - --4\pi G \rho_{0} & -0 & 0 & 0 & --\frac{\ell+1}{r} & -1 \\[1.2em] - --\frac{4\pi G \rho_{0} (\ell+1)}{r} & -\frac{4\pi G \rho_{0} \ell(\ell+1)}{r} & -0 & 0 & 0 & -\frac{\ell-1}{r} -\end{pmatrix}. -``` - -The material parameters satisfy - -```math -\beta = \lambda + 2\mu -``` - -and - -```math -\lambda = \kappa - \frac{2}{3}\mu. -``` - -When solving the spheroidal displacement–stress–gravity system, the 6-vector can span vastly different physical units. This disparity can make the coefficient matrix ``\mathbf{A}_\ell(r)`` highly ill-conditioned, leading to numerical instability when computing linearly independent solutions or performing matrix inversions. - -To mitigate this, we introduce a unit-normalization scaling matrix ``\mathbf{S}``, defined as -```math - \mathbf{S} = \mathrm{diag}\Big(R_0, R_0, \mu_0, \mu_0, g_0 R_0, g_0 \Big), -``` - -where ``R_0, \mu_0, g_0`` are characteristic scales for length, stress, and gravity, respectively. The scaled variables are then - -```math - \tilde{\mathbf{y}}_{\ell m} = \mathbf{S}^{-1}\mathbf{y}_{\ell m}, \quad - \tilde{\mathbf{A}}_\ell = \mathbf{S}^{-1} \mathbf{A}_\ell \mathbf{S}. -``` - ---- - -### Core–Mantle Boundary - -In order to solve the system a general solution is constructed through propagation from the core-mantle boundary outwards. At the CMB radius ``r_C``, the spheroidal solution satisfies - -```math -\mathbf{y}_\ell(r_C^+) - = \mathbf{I}_C \mathbf{C}, -``` - -where ``\mathbf{C} = (C_1, C_2, C_3)^T`` is a vector of integration constants determined by surface boundary conditions. The CMB Interface Matrix is given as - -```math -\mathbf{I}_C = -\begin{pmatrix} --\psi_\ell(r_C)/g(r_C) & 0 & 1 \\[1.2em] -0 & 1 & 0 \\[1.2em] -0 & 0 & g(r_C)/\rho_0(r_C^-) \\[1.2em] -0 & 0 & 0 \\[1.2em] -\psi_\ell(r_C) & 0 & 0 \\[1.2em] -q_\ell(r_C) & 0 & 4\pi G \rho_0(r_C^-) -\end{pmatrix}. -``` - -Note: the density ``\rho_0`` used here corresponds to the core density. The assumed normalization implies - -```math -\mathbf{\tilde{I}}_C = \mathbf{S}^{-1} \mathbf{I}_C. -``` - -Once the constants ``\mathbf{C}`` are determined, the full perturbed state of the solid mantle is known. - ---- - -### (Visco)elastic Solution - -We provide two methods to solve the coupled system of ODEs, the shooting method and the relaxation method. - -#### Shooting method - -propagate the solution using the so-called propagator matrix (``\pmb{\Pi}_\ell``). The propagator matrix solves the homogeneous differential system - -```math -\frac{d\pmb{\Pi}_\ell(r, r')}{dr} = \pmb{A}_\ell(r)\,\pmb{\Pi}_\ell(r, r'), -``` -at radius ``r`` w.r.t. the solution at the previous layer ``r'``, this is also know as the Cauchy data at radius (``r'``). If ``r = r'`` we have - -```math -\pmb{\Pi}_\ell(r', r') = \pmb{1}. -``` - -Each column of the propagator matrix is one of the six linearly independent solutions of - -```math -\frac{d\pmb{y}_{\ell m}}{dr} = \pmb{A}_\ell(r)\,\pmb{y}_{\ell m}. -``` - -The six linearly independent solutions are multiplied by the propagator, forming a basis matrix. We impose continuity: - -```math -\pmb{\Pi}_\ell(r_j^+, r') = \pmb{\Pi}_\ell(r_j^-, r'), -``` - -and apply CMB boundary conditions: - -```math -\pmb{y}_{\ell m}(r_C^+) = \pmb{y}_0 = \pmb{I}_C\,\pmb{C}. -``` - -Therefore, - -```math -\pmb{y}_{\ell m}(r) = -\pmb{\Pi}_\ell(r, r_C^+)\,\pmb{I}_C\,\pmb{C}. -``` - -This equation can be solved iteratively, up till the surface to yield the general responds of the interior to any form of tidal- or load induced deformation. - -#### Relaxation method - -Alternatively, to solve the above equation numerically, we approximate it using a second-order finite-difference scheme: - -```math -\mathbf{y}_{n+1} - \mathbf{y}_n = \frac{\Delta r}{2} \left( \mathbf{A}_{n+1} \mathbf{y}_{n+1} + \mathbf{A}_n \mathbf{y}_n \right) -``` - -Here, ``\mathbf{y}_n`` and ``\mathbf{A}_n`` are evaluated at radius ``r = r_n`` for ``n = 1, \dots, N``, and ``\Delta r = r_{n+1} - r_n`` is the grid spacing. - -The grid is non-uniform and follows an exponential distribution, with the largest spacing ``\Delta r_{\max}`` located just above the core–mantle boundary (CMB), and the smallest spacing ``\Delta r_{\min}`` near the surface. - -The total number of nodes is given by: - -```math -N = \mathrm{round} \left( -\frac{R_E - R_c}{\Delta r_{\min}} \cdot \frac{\alpha}{\exp(\alpha) - 1} -\right) -``` - -where ``R_c`` is the core radius and ``\alpha = \ln\left(\frac{\Delta r_{\max}}{\Delta r_{\min}}\right)``. - -The nodal positions are then defined as: - -```math -r_i = R_E + (R_c - R_E) -\left( -\frac{ -\exp\left( \alpha \frac{N - i}{N - 1} \right) - 1 -}{ -\exp(\alpha) - 1 -} -\right), \quad i = 1, \ldots, N -``` - -Rearranging the finite-difference equation yields a linear relation between adjacent nodes: - -```math -\mathbf{C}_n \mathbf{y}_n + \mathbf{D}_{n+1} \mathbf{y}_{n+1} = \mathbf{0} -``` - -where the matrices ``\mathbf{C}_n`` and ``\mathbf{D}_{n+1}`` are defined as: - -```math -\mathbf{C}_n = \mathbf{I} + \frac{\Delta r}{2} \mathbf{A}_n -``` - -```math -\mathbf{D}_{n+1} = -\mathbf{I} + \frac{\Delta r}{2} \mathbf{A}_{n+1} -``` - -The columns of ``\mathbf{I}_C`` represent the three elementary solutions of an isotropic, homogeneous elastic–gravitational sphere. The vector ``\mathbf{C}`` contains the integration constants: - -```math -\mathbf{C} = (C_1, C_2, C_3)^T -``` - -These constants are determined from the boundary conditions at the planetary surface, applied to the stress components of the spheroidal solution. The general solution can be written as a linear combination of the elementary solutions ``\mathbf{y}^{(i)}``, which are regular at the centre: - -```math -\mathbf{y}_l(r_C^+) = C_1 \mathbf{y}^{(1)} + C_2 \mathbf{y}^{(2)} + C_3 \mathbf{y}^{(3)} -``` - -Eliminating the coefficients ``C_i`` leads to: - -```math -\begin{pmatrix} -U \\ -V \\ -\phi -\end{pmatrix} -- -\begin{pmatrix} -U^{(1)} & U^{(2)} & U^{(3)} \\ -V^{(1)} & V^{(2)} & V^{(3)} \\ -\phi^{(1)} & \phi^{(2)} & \phi^{(3)} -\end{pmatrix} -\begin{pmatrix} -X^{(1)} & X^{(2)} & X^{(3)} \\ -Y^{(1)} & Y^{(2)} & Y^{(3)} \\ -\psi^{(1)} & \psi^{(2)} & \psi^{(3)} -\end{pmatrix}^{-1} -\begin{pmatrix} -X \\ -Y \\ -\psi -\end{pmatrix} -= 0 -``` - -Here, ``U^{(i)}, V^{(i)}, \phi^{(i)}, X^{(i)}, Y^{(i)}, \psi^{(i)}`` are the components of the (i)-th elementary solution. - -This can be written compactly as a boundary condition: - -```math -\mathbf{B} \, \mathbf{y} = 0 -``` - -where ``\mathbf{B}`` is a ``3 \times 6`` matrix: - -```math -\mathbf{B} = -\begin{pmatrix} -1 & 0 & b_{11} & b_{12} & 0 & b_{13} \\ -0 & 1 & b_{21} & b_{22} & 0 & b_{23} \\ -0 & 0 & b_{31} & b_{32} & 1 & b_{33} -\end{pmatrix} -``` - -The coefficients ``b_{ij}`` are defined as: - -```math -\begin{pmatrix} -b_{11} & b_{12} & b_{13} \\ -b_{21} & b_{22} & b_{23} \\ -b_{31} & b_{32} & b_{33} -\end{pmatrix} -= -- -\begin{pmatrix} -U^{(1)} & U^{(2)} & U^{(3)} \\ -V^{(1)} & V^{(2)} & V^{(3)} \\ -\phi^{(1)} & \phi^{(2)} & \phi^{(3)} -\end{pmatrix} -\begin{pmatrix} -X^{(1)} & X^{(2)} & X^{(3)} \\ -Y^{(1)} & Y^{(2)} & Y^{(3)} \\ -\psi^{(1)} & \psi^{(2)} & \psi^{(3)} -\end{pmatrix}^{-1} -``` - -Notably, the second and third columns are swapped compared to Kobayashi (2007) for consistency with ``\mathbf{A}``. The difference equation together with the boundary conditions (given in the next sections) define the complete system of oscillations: - -```math -\begin{pmatrix} -B_1 \\ -C_1 & D_2 \\ -& C_2 & D_3 \\ -& & \ddots & \ddots \\ -& & & C_{N-1} & D_N \\ -& & & & B_N -\end{pmatrix} -\begin{pmatrix} -y_1 \\ -y_2 \\ -y_3 \\ -\vdots \\ -y_{N-1} \\ -y_N -\end{pmatrix} -= -\begin{pmatrix} -0 \\ -0 \\ -0 \\ -\vdots \\ -0 \\ -b -\end{pmatrix} -``` - -Here, ``B_1`` and ``B_N`` represent the inner and outer boundary conditions, respectively, while ``C_n`` and ``D_n`` correspond to the discretised equations of motion, which depend on the forcing frequency ``\omega`` and the interior structure of the mantle. All unspecified entries in the global matrix are zero. - -The structure of the system can be exploited to reduce computational cost. Following a Henyey-type relaxation method (e.g. Unno et al. 1989), the system can be rewritten in block form: - -```math -\begin{pmatrix} -S_1 & Q_1 \\ -P_2 & S_2 & Q_2 \\ -& \ddots & \ddots & \ddots \\ -& & P_{N-1} & S_{N-1} & Q_{N-1} \\ -& & & P_N & S_N -\end{pmatrix} -\begin{pmatrix} -y_1 \\ -y_2 \\ -\vdots \\ -y_{N-1} \\ -y_N -\end{pmatrix} -= -\begin{pmatrix} -0 \\ -0 \\ -\vdots \\ -0 \\ -b -\end{pmatrix} -``` - -In this formulation, ``S_1`` contains contributions from ``B_1`` and the upper block of ``C_1``. The upper and lower parts of ``Q_1`` consist of a zero block and the upper part of ``D_2``, respectively, while ``P_2`` contains the lower part of ``C_1`` combined with a zero block. - -The submatrices are defined as: - -```math -P_n \equiv -\begin{bmatrix} -C^{l}_{n-1} \\ -0 -\end{bmatrix}, -\quad n = 2, \ldots, N -``` - -```math -S_n \equiv -\begin{bmatrix} -D^{l}_n \\ -C^{u}_n -\end{bmatrix}, -\quad n = 2, \ldots, N-1 -``` - -```math -Q_n \equiv -\begin{bmatrix} -0 \\ -D^{u}_{n+1} -\end{bmatrix}, -\quad n = 1, \ldots, N-1 -``` - -Here, superscripts ``u`` and ``l`` denote the upper and lower blocks of the corresponding matrices. The matrix ``S_N`` consists of the lower part of ``D_N`` combined with ``B_N``. Each of the block matrices ``P_n``, ``S_n``, and ``Q_n`` has size ``6 \times 6``. Both formulations are equivalent, and the full system corresponds to a banded matrix of size ``6N \times 6N`` for spheroidal oscillations in a solid sphere. Because coupling is restricted to nearest neighbours, the system can be solved efficiently using a recursive scheme. - -The first block row reads: - -```math -S_1 y_1 + Q_1 y_2 = 0 -``` - -which gives: - -```math -y_1 = -S_1^{-1} Q_1 y_2 -``` - -Substituting into the next block row yields: +where the first and second components are the radial and tangential +displacements, the third and fourth components the radial and tangential +stresses, the fifth component the potential and the sixth component the +so called 'potential stress'. By extension, the seventh and eighth +$y$-functions are -```math -\left(-P_2 S_1^{-1} Q_1 + S_2 \right) y_2 + Q_2 y_3 = 0 -``` +$$\pmb{y}_{n,m} = (U_{n,m} , V_{n,m} , X_{n,m} , Y_{n,m}, \Phi_{n,m} , \Psi_{n,m}, P_{n,m}, R_{n,m})^T$$ -so that: +with $P_{n,m}$ the pore pressure, and $R_{n,m}$ the relative tangential +displacement. Notably, these $y$-functions come in pairs, although this +may not be generally true. We can define two sets, both sets are bounded +from below (e.g. core or internal source), but only one set is bounded +from above (e.g. surface or internal sink). The sets are -```math -y_2 = -\left(-P_2 S_1^{-1} Q_1 + S_2 \right)^{-1} Q_2 y_3 -``` - -Proceeding recursively, we obtain: - -```math -y_n = R_n y_{n+1} -``` - -with - -```math -R_n = -X_n^{-1} Q_n -``` +$$\text{lower} = U_{n,m} , V_{n,m}, \Phi_{n,m}, P_{n,m}$$ and -```math -X_n = P_n R_{n-1} + S_n -``` - -The initial condition is: - -```math -R_1 = -S_1^{-1} Q_1 -``` - -Finally, the last block equation becomes (see below for the surface boundary conditions): - -```math -X_N y_N = b = +$$\text{upper} = X_{n,m} , Y_{n,m}, \Psi_{n,m}, R_{n,m}.$$ + +It is worth +noting that there is a direct relation between the lower bound +$y$-functions at the surface and the Lovenumbers + +$$\begin{aligned} + h_n &= y_1(R) \\ + l_n &= y_2(R) \\ + k_n &= y_5(R) - 1 +\end{aligned}$$ + +assuming the $y$-functions are dimensionless (note the +index $i$ in $y_i$ corresponds to the $i$th $y$-function in the state +vector $\pmb{y}_{n,m}$ given above). The relations are identical for the +load Lovenumbers. + +$$\begin{aligned} + h'_n &= y_1(R) \\ + l'_n &= y_2(R) \\ + k'_n &= y_5(R) - 1 +\end{aligned}$$ + +However, in the case of a pressure load (atmosphere), +the relations change slightly + +$$\begin{aligned} + h^P_n &= y_1(R) \\ + l^P_n &= y_2(R) \\ + k^P_n &= y_5(R) +\end{aligned}$$ + +At the same time the upper $y$-functions at the surface +are subject to the cause specific boundary conditions. For the general +case, if we denote $P$ as an external pressure, $\zeta$ as a surface +mass load, $U$ as an external potential, and $\tau$ as a tangential +component of traction, we can write the general boundary conditions for +a given $n$th degree as + +$$\begin{aligned} +\begin{array}{c|cccc} + & U & \zeta_n & \tau & P \\ \hline +y_{3}(a) & 0 & -g_e \zeta_n & 0 & -P_n \\ +y_{4}(a) & 0 & 0 & \tau_n & 0 \\ +\dfrac{n+1}{a} y_{5}(a) + y_{6}(a) + & \dfrac{2n+1}{a} U_n & 4\pi G \zeta_n & 0 & 0 +\end{array} +\end{aligned}$$ + +where $g_e$ is the norm of surface gravity. The surface +mass load can also be written as an external potential $U'$ such that +$\zeta_n = [(2n + 1)/4 \pi G a] U'_n$. + +$$\begin{aligned} +\label{eq:bound} +y_{3}(R) &= - \frac{(2n + 1)g_e}{4 \pi G R} U'_n - P_n \\ +y_{4}(R) &= \tau_n \\ +\dfrac{n+1}{R} y_{5}(R) + y_{6}(R) + &= \frac{2n+1}{R} (U_n + U'_n) +\end{aligned}$$ + +For now we only use the following: the tidal Love number +corresponding to the case of an external potential perturbation $U$ and +the load Love number computed for a surface mass load perturbation +$\zeta$ or equivalently a potential $U'$. They can be directly +determined setting, respectively $(U, U', \tau, P)$ to $(1,0,0,0)$ for +TLN and $(0,1,0,0)$ for LLN in system. Interestingly, the tidal +potential imposed here $U = 1$ is identical to the tidal potential +$U_{n,m,k}$ listed earlier (up to some normalization factor). We simply +set $U = 1$ here to nondimensionalize the Lovenumbers, and scale the +solution later on with $U_{n,m,k}$. + +It should be stated that all of these terms ($U, \zeta_n, \tau, P$) are +non-dimensional here (i.e. 1 if present or 0 if not present). They each +give rise to their own set of Lovenumbers ($h_n , l_n , k_n$), which can +be combined into a distortion potential of the planet through + +$$U_n^D = k_n^T + (1+k_n^L)U_n^L + K_n^P U_n^P,$$ + +where $U_n^L$ and +$U_n^P$ are, respectively, the mass-load and pressure potentials: + +$$U_n^L = \frac{3}{2n+1}\frac{g \mathcal{S}_n}{\rho_b}, \qquad U_n^P = \frac{3}{2n+1}\frac{\mathcal{Q_n}}{\rho_b}$$ + +with surface gravity $g$, bulk density $\rho_b$, surface density of the +mass load $\mathcal{S}_n$, and surface pressure $\mathcal{Q_n}$. In +principle, we can account for the stacking of different layers with +there own Lovenumbers by manipulating this expression. For example, +Farhat et al. (2025) provides the forms for $U_n^L$ for a magma ocean +and $\mathcal{Q_n}$ for a uniform lithosphere/crust for the Andrade +rheology. M. Beuthe (2016) provides its explicit form for a crust +characterized by a Maxwell rheology, allowing for radial variations in +the physical properties. I don't know the exact form we should use for +atmospheric pressure, but it can probably be found. Nevertheless, we +should note that each of these terms is effectively just a correction to +the global Lovenumber (and as shown by Farhat et al., 2025 for the +crust, often has only a small effect). As such, we may consider +implementing these corrections in the future, but they should not alter +the results drastically. Given this, we currently combine the +Lovenumbers of the solid and fluid using the simple form + +$$k_n = k^T_n + (1 + k^L_n) k_n^{T, \text{(fluid)}}$$ + +to get the global +Lovenumber $k_n$. For this, the determination of tidal and load +lovenumber $k_n$ has been implemented in Obliqua. The other terms are +currently set to zero, but can easily also be determined, allowing us to +further expand on this in the future. Finally, note that the loading +Lovenumbers (both gravity and pressure) are only defined for the solid +part of the mantle. + +Evidently, these $y$-functions are functions of radius, hence we wrote +$\pmb{y_{n,m}}(r)$ before. They are effectively the solutions at each +radii to the following first order differential equation + +$$\frac{d\pmb{y}_{n,m}(r)}{dr} = \pmb{A}_n (r) \pmb{y}_{n,m}(r) - \pmb{f}_{n,m}(r)$$ + +where $\pmb{A}_n$ represents the motion matrix with unit $\sim1/r$, it +is a differential operator. The correction term $\pmb{f}_{n,m}(r)$ +represents sources and sinks that may be present within the interior +(e.g. earthquakes, actually we will use it for porous interfaces!). + +In principle $\pmb{A}_n$ can be anything you want, as long as it +represents the medium that you are considering. There are many different +motion matrices in the literature, but for our purposes one is of +specific interest + +$$\small +\pmb{A}_n(r) = \\ \begin{pmatrix} -0 \\ -0 \\ -\frac{2n+1}{R} -\end{pmatrix} -``` - -with - -```math -X_N = P_N R_{N-1} + S_N -``` - -This recursive formulation reduces the computational cost from solving a full ``6N \times 6N`` system to a linear sequence of ``N`` matrix operations. - -Two different propagation operators appear in the formulation. From the differential equations, one obtains the local transfer relation: - -```math -y_n = -C_n^{-1} D_{n+1} y_{n+1} \equiv \tilde{R}_n y_{n+1} -``` - -Although this resembles the recursive relation above, the operators are fundamentally different. The matrix ``\tilde{R}_n`` is constructed directly from the differential operator. Since both ``C_n`` and ``D_n`` are invertible, ``\tilde{R}_n`` is also invertible, allowing propagation in both directions along the grid. - -In contrast, the relaxation operator is: - -```math -y_n = R_n y_{n+1}, \qquad R_n = -X_n^{-1} Q_n -``` - -Here, ``Q_n`` is singular by construction, so ``R_n`` is not invertible. This enforces a one-way recursion from ``n+1`` to ``n``. Rather than being a limitation, this structure ensures numerical stability by enforcing simultaneous compatibility with both the differential equations and boundary conditions. - +-\dfrac{2\lambda}{r\beta} & \dfrac{\ell(\ell+1)\lambda}{r\beta} & \dfrac{1}{\beta} & 0 & 0 & 0 \\[1.2em] ---- +-\dfrac{1}{r} & \dfrac{1}{r} & 0 & \frac{1}{\mu} & 0 & 0 \\[1.2em] -### Surface Boundary Conditions +\dfrac{4}{r}\!\left( \dfrac{3\kappa\mu}{r\beta} - \rho_{0}g \right) { - \rho_0 \omega^2} & +\dfrac{\ell(\ell+1)}{r}\!\left(\rho_{0}g - \dfrac{6\kappa\mu}{r\beta}\right) & +-\dfrac{4\mu}{r\beta} & \dfrac{\ell(\ell+1)}{r} & - \dfrac{\rho_{0}(\ell+1)}{r} & +\rho_{0} \\[1.2em] +\dfrac{1}{r}\!\left(\rho_{0}g - \dfrac{6\mu\kappa}{r\beta}\right) & +\dfrac{2\mu}{r^{2}}\!\left[\ell(\ell+1)\!\left(1+\dfrac{\lambda}{\beta}\right)-1\right] { - \rho_0 \omega^2} & +-\dfrac{\lambda}{r\beta} & - \dfrac{3}{r} & +\dfrac{\rho_{0}}{r} & 0 \\[1.2em] +-4\pi G \rho_{0} & 0 & 0 & 0 & -\dfrac{\ell+1}{r} & 1 \\[1.2em] +-\dfrac{4\pi G \rho_{0} (\ell+1)}{r} & \dfrac{4\pi G \rho_{0} \ell(\ell+1)}{r} & 0 & 0 & 0 & \dfrac{\ell-1}{r} +\end{pmatrix}$$ -Finally, to find the specific solution for the case where a planet is orbiting around a star-like object, we can specify the surface (``r=a^-``) boundary condition. In particular we need to constrain the 3rd, 4th, and 6th ``y``-values. The integration constants will follow from +with ($\ell = n$, same thing, different notation) and -```math -\pmb{C} = -\left(\pmb{P}_1 \pmb{\Pi}_\ell(a, r_C)\,\pmb{I}_C\right)^{-1}\pmb{b}. -``` +$$\beta = \lambda + 2 \mu$$ -Thus, +and $\lambda$ is the second Lamé parameter +that is expressed in terms of the shear modulus $\mu$ (also known as +first Lamé parameter) and the bulk modulus $\kappa$ -```math -\pmb{y}_{\ell m}(r) -= -\pmb{\Pi}_\ell(r, r_C)\, -\pmb{I}_C\, -\left(\pmb{P}_1 \pmb{\Pi}_\ell(a, r_C)\,\pmb{I}_C\right)^{-1} -\pmb{b}. -``` +$$\lambda = \kappa - \frac{2}{3}\mu.$$ -To solve this system we thus need only provide ``\pmb{P}_1\,\pmb{y}(a^-)``. Additionally for the relaxation method we can impose a semi-free-surface outer boundary. The boundary conditions can be written in the same form as the inner system with +As you can see, this matrix has size $6\times 6$, implying that it +couples six $y$-functions. Specifically these are +$U_{n,m} , V_{n,m} , X_{n,m} , Y_{n,m}, \Phi_{n,m} , \Psi_{n,m}$. We may +extend the motion matrix to also include corrections from porosity, the +corresponding a matrix will have size $8\times 8$ -```math -\mathbf{B} = +$$\tiny +A = \begin{pmatrix} -0 & 0 & 1 & 0 & 0 & 0 \\ -0 & 0 & 0 & 1 & 0 & 0 \\ -0 & 0 & 0 & 0 & (n+1)/R & 1 -\end{pmatrix} -``` - -This corresponds to free-surface conditions applied to ``y_1``, ``y_2``, and (semi) ``y_5``, while the remaining components are constrained accordingly. In the general case, let ``P`` denote the external atmospheric pressure, ``\zeta`` the surface mass load, ``U`` an external potential, and ``\tau`` the tangential traction component. Then the boundary conditions for a spherical harmonic degree ``n`` can be written as: - -```math -\begin{cases} -y_{3n}(a) = -g_e \zeta_n - P_n \\ -y_{4n}(a) = \tau_n \\ -y_{6n}(a) + \frac{n+1}{a} y_{5n}(a) -= \frac{2n+1}{a} U_n + 4\pi G \zeta_n -\end{cases} -``` - -where ``g_e`` is the surface gravity magnitude. - -Farrell (1972) and Longman (1962) showed that the surface mass load can alternatively be expressed as an equivalent external potential ``U'``, such that: - -```math -\zeta_n = \frac{2n+1}{4\pi G a} U'_n -``` - -We distinguish between Tidal Love numbers (response to an external potential perturbation ``U``) and Load Love numbers (response to a surface mass load ``\zeta`` (or equivalently ``U'``)). These can be obtained by solving the system with the following choices: - -* TLN: ``(U, U', \tau, P) = (1, 0, 0, 0)`` -* LLN: ``(U, U', \tau, P) = (0, 1, 0, 0)`` - -When the outer boundary is driven by tides from an external perturber, the final step in the relaxation method system can be written as: - -```math -\mathbf{B} \mathbf{y} = \mathbf{b} -``` +\cdot & \cdot & \cdot & \cdot & \cdot & \cdot & \alpha \beta^{-1} & 0 \\ +\cdot & \cdot & \cdot & \cdot & \cdot & \cdot & 0 & 0 \\ +1i k \rho_\ell^2 g^2 n(n+1)\! \dfrac{r^{-2}}{\omega \eta_\ell} & \cdot & \cdot & \cdot & +-\! (n+1) r^{-1} 1i k \rho_\ell^2 g n \dfrac{r^{-1}}{\omega \eta_\ell} & +\cdot & 1i k \rho_\ell g n(n+1)\!\dfrac{r^{-2}}{\omega \eta_\ell} - 4\mu \alpha \beta^{-1} r^{-1} & 1i k \rho_\ell^2 g^2 n(n+1)\!\dfrac{r^{-2}}{\omega \eta_\ell} - 4 \phi \rho_\ell g r^{-1} \\ +0 & 0 & 0 & 0 & 0 & 0 & 2\alpha\mu r^{-1}\beta^{-1} & \phi \rho_\ell g r^{-1} \\ +0 & 0 & 0 & 0 & 0 & 0 & 0 & 4\pi G \rho_\ell \phi \\ +-1i 4\pi G n(n+1) r^{-1} k\rho_\ell^2 g\dfrac{r^{-1}}{\omega\eta_\ell} & 0 & 0 & 0 & +1i 4\pi n(n+1)G\rho_\ell^2 k\dfrac{r^{-2}}{\omega\eta_\ell} & 0 & +-1i 4\pi n(n+1)G\rho_\ell k\dfrac{r^{-2}}{\omega\eta_\ell} & +4\pi G (n+1)r^{-1}\!\left(\phi\rho_\ell - 1i n k\rho_\ell^2 g\dfrac{r^{-1}}{\omega\eta_\ell}\right) \\ +\rho_\ell g r^{-1}\!\left(4 - 1i k\rho_\ell g n(n+1)\dfrac{r^{-1}}{\omega \phi \eta_\ell}\right) & +-\rho_\ell n(n+1) g r^{-1} & +0 & 0 & +-\rho_\ell (n+1)r^{-1}\!\left(1 - 1i k\rho_\ell g n\dfrac{r^{-1}}{\omega \phi \eta_\ell}\right) & +\rho_\ell & +-1i k\rho_\ell g n(n+1)\!\dfrac{r^{-2}}{\omega \phi \eta_\ell} & +-1i \omega \phi \eta_\ell / k - 4\pi G(\rho - \phi \rho_\ell)\rho_\ell + \rho_\ell g r^{-1}\!\left(4 - 1i k\rho_\ell g n(n+1)\dfrac{r^{-1}}{\omega \phi \eta_\ell}\right) \\ +r^{-1}\!\left(1i k\rho_\ell g n(n+1)\dfrac{r^{-1}}{\omega \phi \eta_\ell} - \dfrac{\alpha}{\phi} 4\mu\beta^{-1}\right) & +\dfrac{\alpha}{\phi} 2n(n+1)\mu \beta^{-1} r^{-1} & +-\dfrac{\alpha}{\phi}\beta^{-1} & +0 & +-1i k \rho_\ell n(n+1)\!\dfrac{r^{-2}}{\omega \phi \eta_\ell} & +0 & +1i k n(n+1)\!\dfrac{r^{-2}}{\omega \phi \eta_\ell} - \dfrac{1}{\phi}(S + \alpha^2 \beta^{-1}) & +1i k \rho_\ell g n(n+1)\!\dfrac{r^{-2}}{\omega \phi \eta_\ell} - 2r^{-1} +\end{pmatrix}$$ + +where + +$$\beta^{-1} = \frac{1}{2\mu + \lambda}, \quad +S = \frac{\phi}{K_\ell} + \frac{\alpha - \phi}{K}, \quad +\lambda = K_d - \frac{2\mu}{3}.$$ + +The terms involving $k$, $\eta_\ell$, +and $\omega$ represent viscous coupling between the solid and liquid +phases, while the terms with $G$ describe self-gravitational feedback. +When $\phi = 0$, the system reduces to the single-phase viscoelastic +formulation. + +It is important to note that the ordering of the elements in +$\pmb{A}_n(r)$ and $\pmb{y}_{n,m}(r)$ must be consistent. Different +papers will order the $y$-functions differently (specifically $y_2$ and +$y_3$ are often flipped). We will also perform a reorganization, where +we order $\pmb{y}_{n,m}(r)$ as $[\text{lower}, \text{upper}]^T$. +However, for now we can omit this as there is currently no benefit to +doing this yet. + +This puts us in a bit of an awkward spot, since we have a coupled system +of ODEs, but we can only constrain half of the $y$-functions at the +surface. The missing piece is the core solution vector! Interestingly, +we can determine also tides acting within the core if we really want to, +since it would merely require another $\pmb{A}_n(r)$ for that regime. We +can then simply impose a constraint on the lower $y$-functions at the +center of the planet leaving us with six unknowns and six knows, solving +the system. There is a catch, as this particular approach has a +singularity at $r=0$. One can potentially omit this by starting the +solution at some $\delta r > 0$, but this still leaves us with a core +that has no radially resolved structure. Instead we can make use of the +analytically derived solutions for a homogeneous core! The solutions +place constraints on all six $y$-functions at the Core-Mantle Boundary, +hence over constraining the system. To combat this, we now have three +solution vectors (instead of just 1) at the CMB. These solution vectors +are so-called elementary solutions ($\mathbf{y}^{(i)}, i = 1,2,3$) of an +isotropic homogeneous elastic-gravitational sphere. The true solution is +naturally some weighted average of the three solutions, hence +introducing three new unknowns: we are saved the system is no longer +overestimated! + +Let us define $\pmb{C}$ the vector of constants of integration (weights) +$$\pmb{C} = (a_1, a_2, a_3),$$ these constants of integration must be +determined using the boundary condition at the Earth's surface for the +stress components of the spheroidal vector solution. The (true) modal +solution vector can be written as a superposition of the elementary +solutions $y^{(i)}$ which are regular at the centre of the body, + +$$\pmb{y}_n(r_C^+) = a_1 \mathbf{y}^{(1)} + a_2 \mathbf{y}^{(2)} + a_3 \mathbf{y}^{(3)},$$ + +implying that as our true solution $\pmb{y}_n(r_C^+)$ approaches the +core from above ($r_C^+$) it must smoothly (for mathematicians: +smoothness class $C^0$; continuous) transition into the core solution +vector (which is then the weighted superposition of the elementary +solutions). So just to be clear, the core also only has one solution, +but it can be expressed as three elementary solutions. The moment we fix +$\pmb{C}$ we implicitly fix also the core solution! + +We can now introduce the exact elementary solutions at the CMB. +Equations (95) for toroidal modes and (98) and (100) for spheroidal +modes of Takeuchi & Saito (1972) are adopted for the inner boundary +conditions. We express the spheroidal vector solution at the bottom of +the solid mantle as + +$$\pmb{y}_n(r_C^+) = \pmb{I}_C \pmb{C}$$ -with +where +$\pmb{I}_C$ is the core-mantle boundary (CMB) matrix for a fluid core +(quantities correspond to core density/radius/surface gravity!) -```math -\mathbf{b} = +$$\small +\pmb{I}_C = \\ \begin{pmatrix} -0 \\ -0 \\ -\frac{2n+1}{R} -\end{pmatrix} -``` - -This expression follows from a simplified form of the general boundary conditions, assuming the outer radius ``a = R`` (planetary radius). - ---- - +-r^n/g & 0 & 1 \\[1.2em] +0 & 1 & 0 \\[1.2em] +0 & 0 & g/\rho \\[1.2em] +0 & 0 & 0 \\[1.2em] +r^n & 0 & 0 \\[1.2em] +2(n-1)r^{(n-1)} & 0 & 4 \pi G \rho +\end{pmatrix}$$ + +The three columns correspond to the elementary +solutions. Similar to how we were able to define different +$\pmb{A}_l(r)$, we may also impose different effects on our core, +yielding different elementary solutions. Specifically, we may impose +that the core is a incompressible solid, or that it is a rotating fluid +core, taking into account inertia effects at high forcing frequencies. +For now, this simple and elegant solution will suffice, though the +inclusion of inertia effects will be essential for the Earth-Moon +system, this does not change the method so we can proceed like this for +now. + +Given the above you may skip this subsection, here I will briefly +introduce $\pmb{I}_C$ for the compressible fluid core with inertial +effects (assuming shear is zero.) We start with the aforementioned +elementary solutions from Equations from Takeuchi & Saito (1972) + +$$\begin{aligned} + y_1 (r) &= n r^{n-1} \\ + y_2 (r) &= r^{n-1} \\ + y_3 (r) &= 0 \\ + y_4 (r) &= 0 \\ + y_5 (r) &= -(n\gamma - \omega^2 ) r^n \\ + y_6 (r) &= -[2(n-1)n\gamma - (2n+1)\omega^2] r^{n-1} +\end{aligned}$$ + +where $\gamma = 4\pi G\rho /3$. The second elementary +solution is more troublesome + +$$\begin{aligned} + y_1 (r) &= - \frac{r^{l+1}}{2n+3} \left[ \frac12 n h \psi_n(x) + f \phi_{n+1} (x) \right] \\ + y_2 (r) &= - \frac{r^{l+1}}{2n+3} \left[ \frac12 h \psi_n(x) - \phi_{n+1} (x) \right] \\ + y_3 (r) &= - \lambda r^n f \phi_n (x) \\ + y_4 (r) &= 0 \\ + y_5 (r) &= - r^{n+2} \left[ \frac{\alpha^2 f}{r^2} - \frac{3\gamma f}{2(2n+3)} \psi_n (x) \right] \\ + y_6 (r) &= - r^{n+1} \left[ \frac{(2n+1) \alpha^2 f}{r^2} - \frac{3\gamma [ (2n+1)f - nh]}{2(2n+3)}\psi_n(x)\right] +\end{aligned}$$ + +where + +$$\begin{aligned} + x &= kr \\ + k^2 &= \frac{1}{\alpha^2} \left[ \omega^2 + 4\gamma - \frac{n(n+1)\gamma^2}{\omega^2} \right] \\ + f &= -\frac{\omega^2}{\gamma} \\ + h &= f - (n+1) \\ + \phi_n (x) &= \frac{(2n+1)!!}{x^n} j_n (x) \\ + \psi_n (x) &= \frac{2(2n+3)}{x^2} [1-\phi_n (x)]. +\end{aligned}$$ + +Here $\alpha$ is the compressional wave speed and +$j_n (x)$ is the spherical Bessel function of the first kind. The issue +is that as we decrease the forcing frequency $\omega$, $k^2$ becomes +negative, and $x$ becomes large and imaginary. The spherical Bessel +function of the first kind grows exponentially, and the elementary +solution rapidly grows beyond numerical precision. One potential fix +would be to use non-dimensional scaling, however this is currently not +working properly. Another way to resolve this is to note that the +elementary solution may be scaled entirely by some arbitrary constant +($C_2$), as noted earlier in the text. As such, we may simply divide by +$j_n(x)$ to remain around unity. Note that without this the solution +diverges and the system cannot be resolved. With this in mind we need +the recursion relations for the spherical Bessel function of the first +kind, define + +$$z_{n} (x) = x j_{n+1} (x) / j_n (x)$$ + +such that + +$$z_{n-1} (x) = \frac{x^2}{(2n+1) - z_n(x)}$$ + +This recursion calculation +should be carried out in order of decreasing $n$ starting from a +sufficiently large wavenumber with an initial value +$z_n(x) = x^2/(2n+3)$. We can now do some algebraic manipulation of the +elementary solution to obtain a form with only terms $\propto 1/j_n(x)$ +or unity. Since, as mentioned, $j_n(x)$ will diverge for large imaginary +$x$, the newly obtained solution will be well-behaved around unity. Let +us define + +$$\begin{aligned} + \bar{\phi}_n (x) &\equiv \frac{\phi_n (x)}{j_n (x)} = \frac{(2n+1)!!}{x^n} \\ + \bar{\phi}_{n+1} (x) &\equiv \frac{\phi_{n+1} (x)}{j_n (x)} = \frac{(2n+3)!!}{x^{n+1}} \frac{j_{n+1}(x)}{j_n (x)} = \frac{(2n+3)!!}{x^{n+2}}z_n(x) \\ + \bar{\psi}_n (x) &= \frac{\psi_n(x)}{j_n(x)} = \frac{2(2n+3)}{x^2} \left[ \frac{1}{j_n (x)} - \bar{\phi}_n (x)\right] +\end{aligned}$$ + +The elementary solution is then written: + +$$\begin{aligned} + \bar{y}_1 (r) &= - \frac{r^{n+1}}{2n+3} \left[ \frac{1}{2} n h \bar{\psi}_n(x) + f \frac{(2n+3)!!}{x^{n+2}} z_n(x) \right] \\ + \bar{y}_2 (r) &= - \frac{r^{n+1}}{2n+3} \left[ \frac{1}{2} h \bar{\psi}_n(x) - \frac{(2n+3)!!}{x^{n+2}} z_n(x) \right] \\ + \bar{y}_3 (r) &= - \lambda r^n f \bar{\phi}_n (x) \\ + \bar{y}_4 (r) &= 0 \\ + \bar{y}_5 (r) &= - r^{n+2} \left[ \frac{\alpha^2 f}{r^2} \frac{1}{j_n(x)} - \frac{3\gamma f}{2(2n+3)} \bar{\psi}_n (x) \right] \\ + \bar{y}_6 (r) &= - r^{n+1} \left[ \frac{(2n+1) \alpha^2 f}{r^2} \frac{1}{j_n(x)} - \frac{3\gamma [ (2n+1)f - nh]}{2(2n+3)}\bar{\psi}_n(x)\right] +\end{aligned}$$ + +Now we note that we can simplify the system of equations +when $k^2 \ll -1$, which implies + +$$\begin{aligned} + \omega^2 + 4\gamma &\ll \frac{n(n+1)\gamma^2}{\omega^2} \\ + \omega^4 + 4\gamma \omega^2 &\ll n(n+1)\gamma^2 \\ + \intertext{Noting that the LHS is dominated by $4\gamma \omega^2$ as $\omega \to 0$, we have} + 4\gamma \omega^2 &\ll n(n+1)\gamma^2 \\ + 4\omega^2 &\ll n(n+1)\gamma \qquad \wedge \qquad \gamma \neq 0 +\end{aligned}$$ + +We can now find a typical forcing frequency for the +Earth's iron core, below which we may further simplify the system. Let's +assume for the outer liquid core +$\rho \approx 10000 \text{ to } 12000 \text{ kg/m}^3$. Then, plugging in +to obtain $\gamma$, we have + +$$\gamma \approx \frac{4}{3} \pi (6.67 \times 10^{-11}) (11000) \approx 3 \times 10^{-6} \text{ s}^{-2}$$ + +For a degree $n=2$, that becomes + +$$\frac{n(n+1)}{4} \gamma = \frac{6}{4} (3 \times 10^{-6}) = 4.5 \times 10^{-6} \text{ s}^{-1}$$ + +So that would imply that $\omega \ll 10^{-3} \text{ s}^{-2}$. In this +regime we should use + +$$\begin{aligned} + \bar{y}_1 (r) &= - \frac{r^{n+1}}{2n+3} \left[ \frac{n h (2n+3)}{x^2} (-\bar{\phi}_n) + f \frac{(2n+3)!!}{x^{n+2}} z_n(x) \right] \\ + \bar{y}_2 (r) &= - \frac{r^{n+1}}{2n+3} \left[ \frac{h (2n+3)}{x^2} (-\bar{\phi}_n) - \frac{(2n+3)!!}{x^{n+2}} z_n(x) \right] \\ + \bar{y}_3 (r) &= - \lambda r^n f \bar{\phi}_n (x) \\ + \bar{y}_4 (r) &= 0 \\ + \bar{y}_5 (r) &= - r^{n+2} \left[ - \frac{3\gamma f}{2(2n+3)} \bar{\psi}_n (x) \right] \\ + \bar{y}_6 (r) &= - r^{n+1} \left[ - \frac{3\gamma [ (2n+1)f - nh]}{2(2n+3)}\bar{\psi}_n(x)\right] +\end{aligned}$$ + +which further simplify to + +$$\begin{aligned} + \bar{y}_1 (r) &= - \frac{r^{n+1}(2n+1)!!}{x^{n+2}} \left[ f z_n(x) - nh \right] \\ + \bar{y}_2 (r) &= - \frac{r^{n+1}(2n+1)!!}{x^{n+2}} \left[ -z_n(x) -h \right] \\ + \bar{y}_3 (r) &= - \frac{\lambda f r^n (2n+1)!!}{x^n} \\ + \bar{y}_4 (r) &= 0 \\ + \bar{y}_5 (r) &= - \frac{3\gamma f r^{n+2} (2n+1)!!}{x^{n+2}} \\ + \bar{y}_6 (r) &= - \frac{3\gamma [(2n+1)f - nh] r^{n+1} (2n+1)!!}{x^{n+2}} +\end{aligned}$$ + +Every term now shares the common factor +$\mathcal{K} = \frac{(2n+1)!!}{x^{n+2}}$. If you wish, you can even +divide the entire solution by $\mathcal{K}$ (since the solution is +scalable by an arbitrary constant $C_2$), which leaves us with a +remarkably clean system: + +$$\begin{aligned} + \bar{y}_1 (r) &= -r^{n+1} [f z_n(x) - nh] \\ + \bar{y}_2 (r) &= -r^{n+1} [-z_n(x) - h] \\ + \bar{y}_3 (r) &= -\lambda f r^n x^2 \\ + \bar{y}_4 (r) &= 0 \\ + \bar{y}_5 (r) &= -3\gamma f r^{n+2} \\ + \bar{y}_6 (r) &= -3\gamma [(2n+1)f - nh] r^{n+1} +\end{aligned}$$ + +Finally, we should note that the porosity related $y$-functions need +also be bounded from below and partially from above. Moreover, since a +porous layer is likely to form at some point midway through the mantle, +and not just at the CMB, we will need to account for these boundary +conditions in a slightly different way. Think of the lower boundary as +an extension to the already existing elementary solutions, instead of +three there are now four such elementary solutions. As long as you know +the current form (at some radius) of the three elementary solutions, you +can simply add one more, then forward propagate the four solutions to +the surface (or where ever the porous layer ends) and apply the boundary +conditions directly to obtain the weights +$\pmb{C} = (a_1, a_2, a_3, a_4)$. This is basically the approach taken +in the so-called "shooting method". However, as we will see, this +becomes more difficult once we move to solution/transfer space where we +do not have direct access to the current form of the three elementary +solutions, but rather only know how the solution vector $\pmb{y}_n(r)$ +(this can be any solution vector, not just the true solution) transforms +between layers. This is a central aspect of the "relaxation method" (or +Henyey style solver) that we will employ as it is substantially more +stable then the shooting method. + +For now we will finish this section by stating the elementary boundary +conditions to be imposed on the poro-viscoelastic solution vector. At +the lower boundary of the porous layer the pore pressure is nonzero +$b_{(7,4)} = 1$ and we have zero radial Darcy flux $b_{(8,4)} = 1$. At +the upper part of the porous layer we impose again zero radial Darcy +flux $b_{(8)} = 1$, without any constraint on the pore pressure. + +We will now discuss the different solver schemes. diff --git a/docs/src/reference/solid/images/solid1d_mush_relax.png b/docs/src/reference/solid/images/solid1d_mush_relax.png new file mode 100644 index 0000000000000000000000000000000000000000..cfa6b6a627e860f0e7167a152e9cf999890ca187 GIT binary patch literal 103543 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 literal 0 HcmV?d00001 diff --git a/docs/src/reference/solid/solid1d_mush_relax.md b/docs/src/reference/solid/solid1d_mush_relax.md index 1c8a87b..43c352d 100644 --- a/docs/src/reference/solid/solid1d_mush_relax.md +++ b/docs/src/reference/solid/solid1d_mush_relax.md @@ -3,4 +3,452 @@ # Solid-Phase - solid1d-mush-relax -No spoilers (still a WIP), but this module combines the features of `solid1d_mush` and `solid1d_relax`, providing a radially resolved structure with partially molten regions, solved using a relaxation-based method for improved stability. \ No newline at end of file +For the "solid1d-mush-relax" model we will need to use many of the +concepts we saw for the other models. The basic structure is identical +to that of "solid1d-relax", but instead of just one motion matrix we +will now be working with two motion matrices (i.e. the two +$\pmb{A}_n(r)$ listed earlier in the document). In order to grasp what +is going on in the system let us draw schematically the structure of the +solver. + +![Schematic of "solid1d-mush-relax" +model.](images/solid1d_mush_relax.png) + +Note that for symmetry reasons I have reordered the $y$-functions. This +way when we embed the $6\times6$ system in the $8\times8$ system we +split the lower and upper parts and add one new porous $y$-function to +both sets. If we had not done this, then the new grouping would contain +4 old and 2 old + 2 new $y$-functions, instead of 3 old + 1 new and 3 +old + 1 new. Within the code we create a transfer matrix array $R$ +(length $N$) with size $8 \times 8$, we subsequently expose the +$6\times 6$ system as a "view". The specific ording as seen by the two +systems is + +$$\begin{aligned} + \pmb{y}_{n,m} &= (U_{n,m} , V_{n,m} , \Phi_{n,m} , X_{n,m} , Y_{n,m}, \Psi_{n,m})^T & (6\times6) \\ + \pmb{y}_{n,m} &= (U_{n,m} , V_{n,m} , \Phi_{n,m} , P_{n,m}, X_{n,m} , Y_{n,m}, \Psi_{n,m}, R_{n,m})^T & (8\times8) +\end{aligned}$$ + +With this clarified, let us have a look at the diagram. + +From the left, we start with the core solution boundary + +$$ +B = +\begin{pmatrix} +1 & 0 & 0 & b_{11} & b_{12} & b_{13} \\ +0 & 1 & 0 & b_{21} & b_{22} & b_{23} \\ +0 & 0 & 1 & b_{31} & b_{32} & b_{33} +\end{pmatrix},$$ + +We than impose continuity between $B$ and our modal +solution vector, this is denoted by the dotted vertical line. + +$$\begin{pmatrix} + 1 & & & & & \\ + & 1 & & & \\ + & & 1 & & \\ + & & & 1 & \\ + & & & & 1 \\ + & & & & & 1 + \end{pmatrix}_{(6\times6)} + \pmb{y}_{n,m}(r_1^-) = + \begin{pmatrix} + 1 & & & & & \\ + & 1 & & & \\ + & & 1 & & \\ + & & & 1 & \\ + & & & & 1 \\ + & & & & & 1 + \end{pmatrix}_{(6\times6)} + \pmb{y}_{n,m}(r_C^+)$$ + +We then start constructing $R_n$ throughout +the solid layers + +$$ + \pmb{C}_n \pmb{y}_n + \pmb{D}_{n+1} \pmb{y}_{n+1} = \pmb{0}$$ + +with +$\pmb{A}_n$ the $6 \times 6$ motion matrix. At some point we encounter a +porous layer, at this point we introduce the porous $y$-functions. We +will introduce them in a decoupled fashion and allow them to couple to +the $6 \times 6$ system in a minute. First we need to introduce an +infinitesimal layer over which we impose continuity + +$$\begin{pmatrix} + 1 & & & & & & & \\ + & 1 & & & & & & \\ + & & 1 & & & & & \\ + & & & 1 & & & & \\ + & & & & 1 & & & \\ + & & & & & 1 & & \\ + & & & & & & 1 & \\ + & & & & & & & 1 + \end{pmatrix}_{(8\times8)} + \pmb{y}_{n,m}(r_i^+) + b(r_i^+) = + \begin{pmatrix} + 1 & & & & & \\ + & 1 & & & \\ + & & 1 & & \\ + & & & & & \\ + & & & 1 & \\ + & & & & 1 \\ + & & & & & 1 \\ + & & & & & + \end{pmatrix}_{(8\times6)} + \pmb{y}_{n,m}(r_i^-)$$ + +Evidently, for the LHS to have a zero on the +4th and 8th rows we must have + +$$\begin{aligned} + b_4(r_i^+) &= - y_4(r_i^+) \\ + b_8(r_i^+) &= - y_8(r_i^+) +\end{aligned}$$ + +while continuity of the $6 \times 6$ system implies that +the other components of $b(r_i^+)$ are all zero. One may be inclined to +put $b_4(r_i^+) = -1$ and $b_8(r_i^+) = 0$, i.e. none-zero pore pressure +and zero Darcy flux, but there is a catch! These conditions only hold +for the elementary solution, and not for the modal solution. Instead we +would have to put $b_7(r_i^+) =-a_4$ and $b_8(r_i^+) = 0$, but what is +$a_4$? Since we eliminated all weights already at the core this boundary +is not very useful. Actually, let us try the following + +$$\begin{aligned} + b_4(r_i^+) &= -y_4(r_i^+) \\ + b_8(r_i^+) &= 0 +\end{aligned}$$ + +and treat $y_4(r_i^+)$ as some undetermined quantity. + +This will enter into the equations as follows + +$$\begin{pmatrix} +B_1 \\ +C_1 & D_2 \\ + & C_2 & D_3 \\ + &&\ddots & \ddots \\ + &&&C_{i-1} & D_i \\ + &&&&C_{i}^- & D_i^+ \\ + &&&&& C_{i} & D_{i+1} \\ + &&&&&&\ddots & \ddots \\ + &&&&&&& C_{N-1} & D_N \\ +&&&&&&&&B_N +\end{pmatrix} +\begin{pmatrix} +\pmb{y} (r_C^+) \\ +\pmb{y} (r_2) \\ +\pmb{y} (r_3) \\ +\vdots \\ +\pmb{y} (r_i^-) \\ +\pmb{y} (r_i^+) \\ +\pmb{y} (r_{i+1}) \\ +\vdots \\ +\pmb{y}(r_{N-1}) \\ +\pmb{y}(r_N) +\end{pmatrix} += \begin{pmatrix} +0 \\ +0 \\ +0 \\ +\vdots \\ +0 \\ +b(r_i^+) \\ +0 \\ +\vdots \\ +0 \\ +b +\end{pmatrix},$$ + +where + +$$C_{i}^- = \begin{pmatrix} + 1 & & & & & \\ + & 1 & & & \\ + & & 1 & & \\ + & & & & & \\ + & & & 1 & \\ + & & & & 1 \\ + & & & & & 1 \\ + & & & & & + \end{pmatrix}_{(8\times6)} + \qquad \text{and} \qquad + D_i^+ = -\begin{pmatrix} + 1 & & & & & & & \\ + & 1 & & & & & & \\ + & & 1 & & & & & \\ + & & & 1 & & & & \\ + & & & & 1 & & & \\ + & & & & & 1 & & \\ + & & & & & & 1 & \\ + & & & & & & & 1 + \end{pmatrix}_{(8\times8)}$$ + +At this point we need to make sure also +include the inhomogeneous part ($b_4(r_i^+)$ and $b_8(r_i^+)$) in our +forwarding scheme. The governing equations change slightly, and we need +to be careful about the dimensions of the matrices involved in the +calculation around the interface. In order to not accidentally remove +information from the systems, we shall embed the $6\times6$ system in +the $8\times8$ space. For some matrix $M$ this implies + +$$M_{(3\times6)} = \begin{bmatrix} + a_1 & b_1 & c_1 & d_1 & e_1 & f_1 \\ + a_2 & b_2 & c_2 & d_2 & e_2 & f_2 \\ + a_3 & b_3 & c_3 & d_3 & e_3 & f_3 \\ + \end{bmatrix}_{(3\times6)} \Rightarrow M_{(4\times8)} = + \begin{bmatrix} + a_1 & b_1 & c_1 & 0 & d_1 & e_1 & f_1 & 0 \\ + a_2 & b_2 & c_2 & 0 & d_2 & e_2 & f_2 & 0 \\ + a_3 & b_3 & c_3 & 0 & d_3 & e_3 & f_3 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ + \end{bmatrix}_{(4\times8)}$$ + +Applying this logic to $C^{l}_{i-1}$ and ${C_i^-}^{u}$ + +$$P_i^- \equiv +\begin{bmatrix} +C^{l}_{i-1} \\ +0 +\end{bmatrix}_{(8\times8)}$$ + +$$S_i^- \equiv +\begin{bmatrix} +D^{l}_i \\ +{C_i^-}^{u} +\end{bmatrix}_{(8\times8)}$$ + +$$Q_i^- \equiv +\begin{bmatrix} +0 \\ +{D_i^+}^{u} +\end{bmatrix}_{(8\times8)}$$ + +We now obtain an $R_i^-$ matrix with dimensions $8 \times 8$. Now we +need to figure out how the $8 \times 8$ system departs from the +interface. Lets start by considering the governing equation for a system +with internal boundary + +$$\left(P_n R_{n-1} + S_n \right) y_n + Q_n y_{n+1} = b_n.$$ + +We can +reformulate this specifically for the interface layer as + +$$\left(P_i^+ R_{i}^- + S_i^+ \right) y_i^+ + Q_i^+ y_{i+1} = b(r_i^+)$$ + +where + +$$P_i^+ \equiv +\begin{bmatrix} +{C_{i}^-}^{l} \\ +0 +\end{bmatrix}$$ + +Again, we need to be careful here given the odd +dimensions of ${C_{i}^-}^{l}$. The continuity of the porous +$y$-functions into their own $2\times2$ system implies that we expand +${C_{i}^-}$ into the $8 \times 8$ identity matrix (as seen from the +porous layer). We thus have $P_i^+$ with dimensions $8 \times 8$. Next + +$$S_i^+ \equiv +\begin{bmatrix} +{D_i^+}^{l} \\ +{C_{i+1}}^{u} +\end{bmatrix}$$ + +is trivial, and similarly + +$$Q_i^+ \equiv +\begin{bmatrix} +0 \\ +{D_{i+1}}^{u} +\end{bmatrix}$$ + +is trivial, both are $8 \times 8$ matrices. + +We can now plug everything in and simplify + +$$\begin{aligned} + \left(P_i^+ R_{i}^- + S_i^+ \right) y_i^+ + Q_i^+ y_{i+1} &= b(r_i^+) +\end{aligned}$$ + +Recall that $b(r_i^+)$ depends on $y_i^+$, we may write $b(r_i^+) = K y_i^+$ where $K$ extracts the 7th and 8th components of $y_i^+$ and changes the sign. We thus write + +$$\begin{aligned} + \left(P_i^+ R_{i}^- + S_i^+ + K\right) y_i^+ &= - Q_i^+ y_{i+1} \\ + y_i^+ &= - \left(P_i^+ R_{i}^- + S_i^+ + K\right)^{-1} Q_i^+ y_{i+1} \\ + &= R_i^+ y_{i+1} +\end{aligned}$$ + +With these conditions on $\pmb{1}$ and $K$ we can +determine the interface form of the equations. + +However, we should be careful not to accidentally store too much +information in $R_i^-$ and $R_i^+$. Realize that we embedded the +$6 \times 6$ system in an $8 \times 8$ space to construct a square and +invertible matrix $X_i^-$. This came at the cost of padding the matrices +with zeros. If one instead combined the non-square matrices, they would +obtain $R_i^-$ with size $6 \times 8$ + +$$\begin{aligned} + P_i^- \cdot R_{i-1} + S_i^- &= X_i^- \\ + (7 \times 6) \cdot (6 \times 6) + (7 \times 6) &\Rightarrow (7 \times 6) \\ + -(X_i^- )^{-1} Q_i^- &= R_i^- \\ + (6 \times 7) \cdot (7 \times 8) &\Rightarrow (6 \times 8) +\end{aligned}$$ + +Hence, at this step the rows corresponding to $y_7$ and +$y_8$ to zero. Next we repeat these steps and include the internal +boundary condition on $y_7$ + +$$\begin{aligned} + P_i^+ \cdot R_i^- + S_i^+ + K &= X_i^+ \\ + (8 \times 6) \cdot (6 \times 8) + (8 \times 8) + (8 \times 8) &\Rightarrow (8 \times 8) \\ + -(X_i^+ )^{-1} Q_i^+ &= R_i^+ \\ + (8 \times 8) \cdot (8 \times 8) &\Rightarrow (8 \times 8) +\end{aligned}$$ + +The resulting system contains now information on $y_8$ +from below, hence we must set the row corresponding to $y_8$ to zero in +$R$. This closes the system and accounts for all dof. + +Now we proceed constructing $R_n$ through the porous layers + +$$ + \pmb{C}_n \pmb{y}_n + \pmb{D}_{n+1} \pmb{y}_{n+1} = \pmb{0}$$ with +$\pmb{A}_n$ the $8 \times 8$ motion matrix. + +When we reach the top of the porous layers, potentially at the surface, +but maybe sooner, we need to decouple $y_7$ and $y_8$ from the +$6 \times 6$ system. Impose again an infinitesimal layer + +$$\begin{pmatrix} + 1 & & & & & \\ + & 1 & & & \\ + & & 1 & & \\ + & & & & & \\ + & & & 1 & \\ + & & & & 1 \\ + & & & & & 1 \\ + & & & & & + \end{pmatrix}_{(8\times6)} + \pmb{y}_{n,m}(r_i^+) + b(r_i^+) = + \begin{pmatrix} + 1 & & & & & & & \\ + & 1 & & & & & & \\ + & & 1 & & & & & \\ + & & & 1 & & & & \\ + & & & & 1 & & & \\ + & & & & & 1 & & \\ + & & & & & & 1 & \\ + & & & & & & & 1 + \end{pmatrix}_{(8\times8)} + \pmb{y}_{n,m}(r_i^-)$$ + +Here we apply the constraint that the Darcy +flux vanishes at the interface. The pore pressure remains unconstrained. +Evidently, for the RHS to have a zero on the 8th row we must have + +$$\begin{aligned} + b_7(r_i^+) &= y_7(r_i^-) \\ + b_8(r_i^+) &= 0 +\end{aligned}$$ + +Similar to before, this interface will enter into the equations as +follows + +$$\begin{pmatrix} +B_1 \\ +C_1 & D_2 \\ + & C_2 & D_3 \\ + &&\ddots & \ddots \\ + &&&C_{i-1} & D_i \\ + &&&&C_{i}^- & D_i^+ \\ + &&&&& C_{i} & D_{i+1} \\ + &&&&&&\ddots & \ddots \\ + &&&&&&& C_{N-1} & D_N \\ +&&&&&&&&B_N +\end{pmatrix} +\begin{pmatrix} +\pmb{y} (r_C^+) \\ +\pmb{y} (r_2) \\ +\pmb{y} (r_3) \\ +\vdots \\ +\pmb{y} (r_i^-) \\ +\pmb{y} (r_i^+) \\ +\pmb{y} (r_{i+1}) \\ +\vdots \\ +\pmb{y}(r_{N-1}) \\ +\pmb{y}(r_N) +\end{pmatrix} += \begin{pmatrix} +0 \\ +0 \\ +0 \\ +\vdots \\ +0 \\ +b(r_i^+) \\ +0 \\ +\vdots \\ +0 \\ +b +\end{pmatrix},$$ + +where + +$$C_{i}^- = \begin{pmatrix} + 1 & & & & & & & \\ + & 1 & & & & & & \\ + & & 1 & & & & & \\ + & & & 1 & & & & \\ + & & & & 1 & & & \\ + & & & & & 1 & & \\ + & & & & & & 1 & \\ + & & & & & & & 1 + \end{pmatrix}_{(8\times8)} + \qquad \text{and} \qquad + D_i^+ = -\begin{pmatrix} + 1 & & & & & \\ + & 1 & & & \\ + & & 1 & & \\ + & & & 1 & \\ + & & & & 1 \\ + & & & & & 1 \\ + & & & & & \\ + & & & & & + \end{pmatrix}_{(8\times6)}$$ + +Again we will pad some of the matrices where necessary + +$$P_i^- \equiv +\begin{bmatrix} +C^{l}_{i-1} \\ +0 +\end{bmatrix}$$ + +$$S_i^- \equiv +\begin{bmatrix} +D^{l}_i \\ +{C_i^-}^{u} +\end{bmatrix}$$ + +$$Q_i^- \equiv +\begin{bmatrix} +0 \\ +{D_i^+}^{u} +\end{bmatrix}$$ + +and apply the conditions on $\pmb{1}$ and $K$ to obtain +the interface form of the equations. (This is still a partial work in +progress, however it seems to work in the numerical implementation.) A +simply work around these interfaces is to assign a porosity slightly +greater than the porosity threshold in the code, this way the solver +will be constructed as purely mush propagator without interfaces. + +**The model implementation has been tested and has been found internally +consistent for forcing frequencies between $10^{-4}$ and $10^{-6}$ Hz. +This range may be extended when the non-dimensionalization is properly +implemented, or when the interface conditions are behaving as +expected.** diff --git a/docs/src/reference/tidal-potentials.md b/docs/src/reference/tidal-potentials.md index a287b9e..223bbc6 100644 --- a/docs/src/reference/tidal-potentials.md +++ b/docs/src/reference/tidal-potentials.md @@ -5,28 +5,43 @@ ### Tidal Dissipation -The mode amplitude of the external tidal potential is +To determine the total tidal heating, `Obliqua` loops over all $(n,m,k)$ pairs and calculates heating rates based on general eccentric expressions. While the model currently assumes **co-planarity** (precluding obliquity tides), it fully accounts for eccentricity through Hansen coefficients. -```math -U_{22k} = -\frac{G M_\star}{a} -\left(\frac{R}{a}\right)^2 -\sqrt{\frac{6\pi}{5}}\,X_{22k}(e), -``` +### Tidal Potential and Normalization +For every triplet $(n, m, k)$, we calculate the Hansen coefficient $X_k^{-(n+1),m}(e)$ and the normalization factor $A_{n,m,k}$: -The tidal power per mode is +$$A_{n,m,k} = (2 - \delta_{m,0}\delta_{k,0}) (1 - \delta_{m,0}\delta_{k<0}) \sqrt{\frac{4\pi}{2n+1}\frac{(n-m)!}{(n+m)!}} P_n^m(0) X_k^{-(n+1),m}(e)$$ -```math -P_{T,k} = -\frac{5 R \sigma}{8\pi G} -\,\Im\!\bigl[k_{2}(\sigma)\bigr] -\,|U_{22k}|^2. -``` +The associated tidal potential $U_{n,m,k}$ is defined as: -Such that the total tidal dissipation: +$$U_{n,m,k} = \frac{GM}{a} \left(\frac{R}{a}\right)^n A_{n,m,k}(e)$$ -```math -P_{\mathrm{tidal}} = -\sum_k P_{T,k}. -``` +--- + +### Heating Rates +The "black box" tidal models return an unnormalized heating profile $\tilde{H}(r, \sigma)$. To obtain the physical heating profile $H(r, \sigma)$ at a specific forcing frequency $\sigma$, we scale the output by the square of the tidal potential: + +$$H(r, \sigma) = \tilde{H}(r, \sigma) \times |U_{n,m,k}|^2$$ + +The **global heating rate** $H(\sigma)$ for a given frequency is determined using the imaginary part of the tidal Love number $k_n(\sigma)$: + +$$H(\sigma) = \text{prefactor} \times (-\text{Im}[k_n(\sigma)]) \times |U_{n,m,k}|^2$$ + +where the energy dissipation prefactor is: + +$$\text{prefactor} = \frac{(2n + 1)R}{8\pi G} \sigma$$ + +--- + +### Spectral Summation +Since the tidal forcing consists of a discrete set of frequencies, the total heating is determined by summing across the forcing frequency domain: + +* **Radial Heating Profile:** $H(r) = \sum_{\sigma} H(r, \sigma)$ +* **Total Global Heating:** $H = \sum_{\sigma} H(\sigma)$ + +> **Consistency Check:** The volume integration of the radial profile $H(r)$ should match the global heating rate $H$, provided the surface load Love number $k'_n(\sigma)$ remains small compared to unity ($k'_n \ll 1$). + +### Outputs for Orbital Dynamics +In addition to the heating rates, `Obliqua` returns the complex tidal Love numbers $k_n(\sigma)$ and their corresponding forcing frequencies $\sigma$. these are required for calculating tidal torques and the long-term orbital evolution of the system. --- \ No newline at end of file From 3c8f1ab18c2f22a0f828089740db0eb48e641a98 Mon Sep 17 00:00:00 2001 From: Marijn Date: Sun, 10 May 2026 13:05:55 +0000 Subject: [PATCH 31/36] Fixed small issue. --- docs/make.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 9791727..f85c841 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -40,7 +40,7 @@ makedocs( "4 - Mush layer" => "reference/mush-layer.md", "5 - Liquid-phase" => "reference/liquid-phase.md", "6 - Surface Loading" => "reference/surface-loading.md", - "7 - Tidal potential" => "reference/tidal-potential.md" + "7 - Tidal potential" => "reference/tidal-potentials.md" ] ), PageNode("Explanation" => "explanation/index.md", [ From de4fe6118554d9ec79f423369f42a67d4a3e22da Mon Sep 17 00:00:00 2001 From: Marijn Date: Wed, 13 May 2026 20:48:29 +0000 Subject: [PATCH 32/36] Added mulitfloats, tested float64. --- Project.toml | 2 ++ src/Obliqua.jl | 15 +++++++++++---- src/common.jl | 9 ++++++++- src/fluid0d.jl | 10 +++++++++- src/load.jl | 10 +++++++++- src/solid1d_mush_relax.jl | 11 +++++++++-- src/solid1d_relax.jl | 5 ++++- 7 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Project.toml b/Project.toml index 6ad621c..fcb7ea2 100644 --- a/Project.toml +++ b/Project.toml @@ -13,6 +13,7 @@ Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" +MultiFloats = "bdf0d083-296b-4888-a5b6-7498122e68a5" NCDatasets = "85f8d34a-cbdd-5861-8df4-14fed0d494ab" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" @@ -28,6 +29,7 @@ GenericLinearAlgebra = "0" Interpolations = "0.16.2" JSON = "1.3.0" LoggingExtras = "1.2.0" +MultiFloats = "3.2.6" NCDatasets = "0.14.15" Plots = "1.41.2" Printf = "1.11.0" diff --git a/src/Obliqua.jl b/src/Obliqua.jl index a9d21b2..1fc2ecc 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -15,6 +15,7 @@ module Obliqua using Interpolations using LinearAlgebra using DoubleFloats + using MultiFloats using AssociatedLegendrePolynomials # Include local jl files @@ -59,6 +60,12 @@ module Obliqua prec = BigFloat precc = Complex{BigFloat} + # prec = Float64x4 + # precc = Complex{Float64x4} + + # prec = Float64 + # precc = Complex{Float64} + const AU::prec = prec(1.495978707e11) # m const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 const M_Earth::prec = prec(5.9724e24) # kg @@ -1313,8 +1320,8 @@ module Obliqua y_t, y_l = solid1d_relax.compute_y(r_centers, ρ, g, μ, κ, omega, n, ρ_core, μ_core, κ_core, M_tot; core=core) # for debugging: plot y-function relaxation solution - plotting.plot_relaxation_solution(y_t, r_centers, - filename="$OUT_DIR/relaxation_solution.png") + # plotting.plot_relaxation_solution(y_t, r_centers, + # filename="$OUT_DIR/relaxation_solution.png") # Love numbers (we scale them based on radius fraction of the planet) k2_T = (y_t[5, end] - 1) .* (maximum(r) ./ R) @@ -1584,8 +1591,8 @@ module Obliqua # solve y functions across grid y_t, y_l = solid1d_mush_relax.compute_y(r_centers, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, ρ_core, μ_core, κ_core, M_tot; core=core) - plotting.plot_relaxation_solution(y_t, r_centers, - filename="$OUT_DIR/relaxation_solution.png") + # plotting.plot_relaxation_solution(y_t, r_centers, + # filename="$OUT_DIR/relaxation_solution.png") # Love numbers k2_T = (y_t[5, end] - 1) .* (maximum(r) ./ R) diff --git a/src/common.jl b/src/common.jl index 4fc0fff..de9a1f6 100644 --- a/src/common.jl +++ b/src/common.jl @@ -5,6 +5,7 @@ module common import GenericLinearAlgebra using DoubleFloats + using MultiFloats using AssociatedLegendrePolynomials using StaticArrays using SpecialFunctions @@ -12,9 +13,15 @@ module common export define_spherical_grid, get_scales, get_Ic, get_A, get_A!, get_heating_profile, get_heating_map - prec = BigFloat + prec = BigFloat precc = Complex{BigFloat} + # prec = Float64x4 + # precc = Complex{Float64x4} + + # prec = Float64 + # precc = Complex{Float64} + const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 diff --git a/src/fluid0d.jl b/src/fluid0d.jl index 0233fb9..adeba6e 100644 --- a/src/fluid0d.jl +++ b/src/fluid0d.jl @@ -2,12 +2,20 @@ module fluid0d + using MultiFloats + export compute_fluid_lovenumbers # Precision types - prec = BigFloat + prec = BigFloat precc = Complex{BigFloat} + # prec = Float64x4 + # precc = Complex{Float64x4} + + # prec = Float64 + # precc = Complex{Float64} + # Constants const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 diff --git a/src/load.jl b/src/load.jl index 6744744..a0721f3 100644 --- a/src/load.jl +++ b/src/load.jl @@ -9,10 +9,18 @@ module load using JSON using LoggingExtras using Printf + using MultiFloats export load_interior - prec = BigFloat + prec = BigFloat + precc = Complex{BigFloat} + + # prec = Float64x4 + # precc = Complex{Float64x4} + + # prec = Float64 + # precc = Complex{Float64} """ load_interior(fname::String) diff --git a/src/solid1d_mush_relax.jl b/src/solid1d_mush_relax.jl index 9dd9756..94d832f 100644 --- a/src/solid1d_mush_relax.jl +++ b/src/solid1d_mush_relax.jl @@ -8,14 +8,21 @@ module solid1d_mush_relax using LinearAlgebra import GenericLinearAlgebra using DoubleFloats + using MultiFloats using StaticArrays using SpecialFunctions using SparseArrays using Printf - prec = BigFloat + prec = BigFloat precc = Complex{BigFloat} + # prec = Float64x4 + # precc = Complex{Float64x4} + + # prec = Float64 + # precc = Complex{Float64} + const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 @@ -440,7 +447,7 @@ module solid1d_mush_relax Sn = [Sn_u; Cn_u] Qn = [Qn_u; Dnp_u] Kn = zeros(precc, 8, 8) - Kn[Y[8], Y[8]] = 1.0 + Kn[Y[8], Y[8]] = -1.0 # perform recursion R_prev = R[i-1] diff --git a/src/solid1d_relax.jl b/src/solid1d_relax.jl index 1363e92..91fd937 100644 --- a/src/solid1d_relax.jl +++ b/src/solid1d_relax.jl @@ -12,9 +12,12 @@ module solid1d_relax using SpecialFunctions using SparseArrays - prec = BigFloat + prec = BigFloat precc = Complex{BigFloat} + # prec = Float64 + # precc = Complex{Float64} + const G::prec = prec(6.6743e-11) # m^3 kg^-1 s^-2 From ad0e1455e7b55cfe3f15a5c10108da6788619678 Mon Sep 17 00:00:00 2001 From: Marijn Date: Wed, 13 May 2026 20:49:05 +0000 Subject: [PATCH 33/36] Added type checks to functions. --- src/solid1d.jl | 100 ++++++++++++---------- src/solid1d_mush.jl | 201 ++++++++++++++++++++++++-------------------- 2 files changed, 163 insertions(+), 138 deletions(-) diff --git a/src/solid1d.jl b/src/solid1d.jl index b3ad124..ea9b20d 100644 --- a/src/solid1d.jl +++ b/src/solid1d.jl @@ -32,17 +32,17 @@ module solid1d Discretize the primary layers given by `r` into `nr` discrete secondary layers. # Arguments - - `r::Array{Float64,2}` : 2D array of primary layer boundaries. + - `r::Array{prec,1}` : 1D array of primary layer boundaries. # Keyword Arguments - `nr::Int=80` : Number of secondary layers to discretize. # Returns - - `rs::Array{Float64,2}` : 2D array of secondary layer boundaries/ + - `rs::Array{prec,2}` : 2D array of secondary layer boundaries/ """ - function expand_layers(r; nr::Int=80) + function expand_layers(r::Array{prec,1}; nr::Int=80) - rs = zeros(Float64, (nr+1, length(r)-1)) + rs = zeros(prec, (nr+1, length(r)-1)) for i in 1:length(r)-1 rfine = LinRange(r[i], r[i+1], nr+1) @@ -59,20 +59,20 @@ module solid1d Compute the radial gravity structure associated with a density profile `r` at intervals given by `r`. # Arguments - - `r::Array{Float64,2}` : 2D array of layer boundaries. - - `ρ::Array{Float64,1}` : 1D array of layer densities. The length of `ρ` must be equal to the number of columns in `r`. - - `m_core::Float64` : Mass of the core, which is used to compute the gravity at the core boundary. + - `r::Array{prec,2}` : 2D array of layer boundaries. + - `ρ::Array{prec,1}` : 1D array of layer densities. The length of `ρ` must be equal to the number of columns in `r`. + - `m_core::prec` : Mass of the core, which is used to compute the gravity at the core boundary. # Returns - - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. The dimensions of `g` are the same as `r`. + - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. The dimensions of `g` are the same as `r`. # Notes `r` must be be a 2D array, with index 1 representing the top radius of secondary layers, and index 2 representing the top radius of primary layers. """ - function get_g(r, ρ, m_core) - g = zeros(Float64, size(r)) - M = zeros(Float64, size(r)) + function get_g(r::Array{prec,2}, ρ::Array{prec,1}, m_core::prec) + g = zeros(prec, size(r)) + M = zeros(prec, size(r)) for i in 1:size(r)[2] M[2:end,i] = 4.0/3.0 * π .* diff(r[:,i].^3) .* ρ[i] @@ -95,19 +95,19 @@ module solid1d # Arguments - `n::Int` : Tidal degree. - `m::Int` : Tidal order. - - `theta::Array{Float64,1}` : Array of colatitudes in radians. - - `phi::Array{Float64,1}` : Array of longitudes in radians. + - `theta::AbstractArray` : Array of colatitudes in radians. + - `phi::LinearAlgebra.Adjoint` : Array of longitudes in radians. # Returns - `Ynm::Array{ComplexF64,2}` : 2D array of spherical harmonic values for each combination of theta and phi. """ - function Ynm(n, m, theta, phi) + function Ynm(n::Int, m::Int, theta::AbstractArray, phi::LinearAlgebra.Adjoint) return Plm.(n, m, cos.(theta)) .* exp.(1im * m .* phi) end """ - define_spherical_grid(res) + define_spherical_grid(res, n, m) Create the spherical grid of angular resolution `res` in degrees. This is used for numerical integrations over solid angle. A new grid can easily be defined by @@ -124,7 +124,7 @@ module solid1d solid1d.clats[:] # colatitude grid solid1d.lons[:] # longitude grid """ - function define_spherical_grid(res, n, m) + function define_spherical_grid(res::Float64, n::Int, m::Int) solid1d.res = res # θ and φ grids @@ -224,7 +224,7 @@ module solid1d - `g1::prec` : Gravity at radius r1. - `g2::prec` : Gravity at radius r2. - `ρ::prec` : Density at radius r. - - `μ::prec` : Shear modulus at radius r. + - `μ::precc` : Shear modulus at radius r. - `K::prec` : Bulk modulus at radius r. - `n::Int` : Tidal degree. @@ -234,7 +234,7 @@ module solid1d # Notes See 'get_B!' for definition. """ - function get_B(ω, r1, r2, g1, g2, ρ, μ, K, n) + function get_B(ω::prec, r1::prec, r2::prec, g1::prec, g2::prec, ρ::prec, μ::precc, K::prec, n::Int) B = zeros(precc, 6, 6) get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) return B @@ -255,14 +255,14 @@ module solid1d - `g1::prec` : Gravity at radius r1. - `g2::prec` : Gravity at radius r2. - `ρ::prec` : Density at radius r. - - `μ::prec` : Shear modulus at radius r. + - `μ::precc` : Shear modulus at radius r. - `K::prec` : Bulk modulus at radius r. - `n::Int` : Tidal degree. # Notes See also [`get_B`](@ref) """ - function get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) + function get_B!(B::Array{precc,2}, ω::prec, r1::prec, r2::prec, g1::prec, g2::prec, ρ::prec, μ::precc, K::prec, n::Int) dr = r2 - r1 rhalf = r1 + 0.5dr @@ -297,16 +297,16 @@ module solid1d in Hay et al., (2025). # Arguments - - `Bprod2::Array{precc,4}` : 6x6x(nr-1)x(nlayers-1) array to store the B products across each secondary layer within each primary layer. + - `Bprod2::Array{precc` : 6x6x(nr-1)x(nlayers-1) array to store the B products across each secondary layer within each primary layer. - `ω::prec` : Forcing frequency. - - `r::Array{prec,2}` : 2D array of layer boundaries. - - `ρ::Array{prec,1}` : 1D array of layer densities. - - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. - - `μ::Array{prec,1}` : 1D array of layer shear moduli. - - `K::Array{prec,1}` : 1D array of layer bulk moduli. + - `r::SubArray{prec,2}` : 2D array of layer boundaries. + - `ρ::prec` : 1D array of layer densities. + - `g::SubArray{prec,2}` : 2D array of gravity values at the layer boundaries. + - `μ::precc` : 1D array of layer shear moduli. + - `K::prec` : 1D array of layer bulk moduli. - `n::Int` : Tidal degree. """ - function get_B_product!(Bprod2, ω, r, ρ, g, μ, K, n) + function get_B_product!(Bprod2::Array{precc}, ω::prec, r::SubArray{prec}, ρ::prec, g::SubArray{prec}, μ::precc, K::prec, n::Int) Bstart = Matrix{precc}(I, 6, 6) B = zeros(precc, 6, 6) @@ -336,7 +336,7 @@ module solid1d - `r::Array{prec,2}` : 2D array of layer boundaries. - `ρ::Array{prec,1}` : 1D array of layer densities. - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. - - `μ::Array{prec,1}` : 1D array of layer shear moduli. + - `μ::Array{precc,1}` : 1D array of layer shear moduli. - `K::Array{prec,1}` : 1D array of layer bulk moduli. - `n::Int` : Tidal degree. - `ρ_core::prec` : Density of the core, which is used to compute the starting vector for the numerical integration across the interior. @@ -350,7 +350,7 @@ module solid1d - `M::Array{precc,2}` : 3x3 M matrix, which is used to propagate the solution across the entire interior. - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. """ - function compute_M(ω, r, ρ, g, μ, K, n, ρ_core, μ_core, κ_core; core="liquid") + function compute_M(ω::prec, r::Array{prec,2}, ρ::Array{prec,1}, g::Array{prec,2}, μ::Array{precc,1}, K::Array{prec,1}, n::Int, ρ_core::prec, μ_core::prec, κ_core::prec; core::String="liquid") r, ρ, g, μ, K = convert_params_to_prec(r, ρ, g, μ, K) nlayers = size(r)[2] @@ -400,7 +400,7 @@ module solid1d # Returns - `y::Array{ComplexF64,3}` : 3D array of the solution vector y across the interior. """ - function compute_y(r, g, M, y1_4, n; load=false) + function compute_y(r::Array{prec,2}, g::Array{prec,2}, M::Array{precc,2}, y1_4::Array{precc,4}, n::Int; load::Bool=false) tau = 0.0 P = 0.0 @@ -444,16 +444,16 @@ module solid1d Calculate the strain tensor ϵ at a particular radial level. # Arguments - - `ϵ::Array{ComplexF64,3}` : 3D array to store the strain tensor at a particular radial level, with dimensions corresponding to latitude, longitude, and the 6 independent components of the strain tensor. - - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 6 components. + - `ϵ::SubArray` : 3D array to store the strain tensor at a particular radial level, with dimensions corresponding to latitude, longitude, and the 6 independent components of the strain tensor. + - `y::SubArray` : 1D array of the solution vector y at a particular radial level, with 6 components. - `n::Int` : Tidal degree. - - `rr::prec` : Radius at which to compute the strain tensor. - - `ρr::prec` : Density at radius rr. - - `gr::prec` : Gravity at radius rr. - - `μr::prec` : Shear modulus at radius rr. - - `Ksr::prec` : Bulk modulus at radius rr. + - `rr::Float64` : Radius at which to compute the strain tensor. + - `ρr::Float64` : Density at radius rr. + - `gr::Float64` : Gravity at radius rr. + - `μr::ComplexF64` : Shear modulus at radius rr. + - `Ksr::Float64` : Bulk modulus at radius rr. """ - function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr) + function compute_strain_ten!(ϵ::SubArray, y::SubArray, n::Int, rr::Float64, ρr::Float64, gr::Float64, μr::ComplexF64, Ksr::Float64) i = 1 @views Y = solid1d.Y[i,:,:] @@ -490,14 +490,14 @@ module solid1d otherwise all layers will be caclulated. # Arguments - - `y::Array{ComplexF64,4}` : 4D array of the solution vector y across the interior, returned by `compute_y`. - - `r::Array{Float64,2}` : 2D array of layer boundaries. - - `ρ::Array{Float64,1}` : 1D array of layer densities. - - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. - - `μ::Array{Float64,1}` : 1D array of layer shear moduli. - - `κ::Array{Float64,1}` : 1D array of layer bulk moduli. + - `y::Array{ComplexF64}` : 4D array of the solution vector y across the interior, returned by `compute_y`. + - `r::Matrix` : 2D array of layer boundaries. + - `ρ::AbstractVector` : 1D array of layer densities. + - `g::Matrix` : 2D array of gravity values at the layer boundaries. + - `μ::AbstractVector` : 1D array of layer shear moduli. + - `κ::AbstractVector` : 1D array of layer bulk moduli. - `n::Int` : Tidal degree. - - `ω::Float64` : Tidal frequency in radians per second. + - `ω::prec` : Tidal frequency in radians per second. # Keyword Arguments - `lay::Int=nothing` : If specified, compute the heating profile for only the layer corresponding to this index. Otherwise, compute for all layers. @@ -508,7 +508,15 @@ module solid1d - `Eκ_tot::Array{Float64,1}` : 1D array of total power dissipated in each primary layer due to compaction, in W. - `Eκ_vol::Array{Float64,2}` : 2D array of angular averaged volumetric heating profiles in W/m^3 for dissipation due to compaction, with dimensions corresponding to the secondary layer and primary layer indices. """ - function get_heating_profile(y, r, ρ, g, μ, κ, n, ω; lay=nothing) + function get_heating_profile(y::Array{ComplexF64}, r::Matrix, ρ::AbstractVector, g::Matrix, μ::AbstractVector, κ::AbstractVector, n::Int, ω::prec; lay::Union{Int,Nothing}=nothing) + + # convert to Float64 or ComplexF64 for heating calculations + r = Float64.(r) + ρ = Float64.(ρ) + g = Float64.(g) + μ = ComplexF64.(μ) + κ = Float64.(κ) + ω = Float64(ω) dres = deg2rad(solid1d.res) diff --git a/src/solid1d_mush.jl b/src/solid1d_mush.jl index b38d1b7..bfde740 100644 --- a/src/solid1d_mush.jl +++ b/src/solid1d_mush.jl @@ -32,17 +32,17 @@ module solid1d_mush Discretize the primary layers given by `r` into `nr` discrete secondary layers. # Arguments - - `r::Array{Float64,2}` : 2D array of primary layer boundaries. + - `r::Array{prec,1}` : 1D array of primary layer boundaries. # Keyword Arguments - - `nr::Int=80` : Number of secondary layers to discretize. + - `nr::Int=80` : Number of secondary layers to discretize. # Returns - - `rs::Array{Float64,2}` : 2D array of secondary layer boundaries/ + - `rs::Array{prec,2}` : 2D array of secondary layer boundaries. """ - function expand_layers(r; nr::Int=80) + function expand_layers(r::Array{prec,1}; nr::Int=80) - rs = zeros(Float64, (nr+1, length(r)-1)) + rs = zeros(prec, (nr+1, length(r)-1)) for i in 1:length(r)-1 rfine = LinRange(r[i], r[i+1], nr+1) @@ -59,20 +59,20 @@ module solid1d_mush Compute the radial gravity structure associated with a density profile `r` at intervals given by `r`. # Arguments - - `r::Array{Float64,2}` : 2D array of layer boundaries. - - `ρ::Array{Float64,1}` : 1D array of layer densities. The length of `ρ` must be equal to the number of columns in `r`. - - `m_core::Float64` : Mass of the planetary core. + - `r::Array{prec,2}` : 2D array of layer boundaries. + - `ρ::Array{prec,1}` : 1D array of layer densities. The length of `ρ` must be equal to the number of columns in `r`. + - `m_core::prec` : Mass of the planetary core. # Returns - - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. The dimensions of `g` are the same as `r`. + - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. The dimensions of `g` are the same as `r`. # Notes `r` must be be a 2D array, with index 1 representing the top radius of secondary layers, and index 2 representing the top radius of primary layers. """ - function get_g(r, ρ, m_core) - g = zeros(Float64, size(r)) - M = zeros(Float64, size(r)) + function get_g(r::Array{prec,2}, ρ::Array{prec,1}, m_core::prec) + g = zeros(prec, size(r)) + M = zeros(prec, size(r)) for i in 1:size(r)[2] M[2:end,i] = 4.0/3.0 * π .* diff(r[:,i].^3) .* ρ[i] @@ -95,19 +95,19 @@ module solid1d_mush # Arguments - `n::Int` : Tidal degree. - `m::Int` : Tidal order. - - `theta::Array{Float64,1}` : Array of colatitudes in radians. - - `phi::Array{Float64,1}` : Array of longitudes in radians. + - `theta::AbstractArray` : Array of colatitudes in radians. + - `phi::LinearAlgebra.Adjoint` : Array of longitudes in radians. # Returns - `Ynm::Array{ComplexF64,2}` : 2D array of spherical harmonic values for each combination of theta and phi. """ - function Ynm(n, m, theta, phi) + function Ynm(n::Int, m::Int, theta::AbstractArray, phi::LinearAlgebra.Adjoint) return Plm.(n, m, cos.(theta)) .* exp.(1im * m .* phi) end """ - define_spherical_grid(res) + define_spherical_grid(res, n, m) Create the spherical grid of angular resolution `res` in degrees. This is used for numerical integrations over solid angle. A new grid can easily be defined by @@ -119,12 +119,12 @@ module solid1d_mush - `m::Int` : Tidal order. # Notes - The grid is internal to solid1d, but can be accessed with + The grid is internal to solid1d_mush, but can be accessed with - solid1d.clats[:] # colatitude grid - solid1d.lons[:] # longitude grid + solid1d_mush.clats[:] # colatitude grid + solid1d_mush.lons[:] # longitude grid """ - function define_spherical_grid(res, n, m) + function define_spherical_grid(res::Float64, n::Int, m::Int) solid1d_mush.res = res # θ and φ grids @@ -190,11 +190,12 @@ module solid1d_mush convert_params_to_prec(r, ρ, g, μ, κs, ω, ρl, κl, κd, α, ηl, ϕ, k) Convert input parameters into the required precision. + # Arguments - `r::Array{Float64,2}` : 2D array of layer boundaries. - `ρ::Array{Float64,1}` : 1D array of layer densities. - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. - - `μ::Array{ComplexF64,1}` : 1D array of layer shear moduli. + - `μ::Array{Float64,1}` : 1D array of layer shear moduli. - `κs::Array{Float64,1}` : 1D array of layer bulk moduli. - `ω::Float64` : Forcing frequency. - `ρl::Array{Float64,1}` : 1D array of layer liquid densities. @@ -239,7 +240,7 @@ module solid1d_mush - `g1::prec` : Gravity at radius r1. - `g2::prec` : Gravity at radius r2. - `ρ::prec` : Density at radius r. - - `μ::prec` : Shear modulus at radius r. + - `μ::precc` : Shear modulus at radius r. - `K::prec` : Bulk modulus at radius r. - `n::Int` : Tidal degree. @@ -249,7 +250,7 @@ module solid1d_mush # Notes See 'get_B!' for definition. """ - function get_B(ω, r1, r2, g1, g2, ρ, μ, K, n) + function get_B(ω::prec, r1::prec, r2::prec, g1::prec, g2::prec, ρ::prec, μ::precc, K::prec, n::Int) B = zeros(precc, 6, 6) get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) return B @@ -270,14 +271,14 @@ module solid1d_mush - `g1::prec` : Gravity at radius r1. - `g2::prec` : Gravity at radius r2. - `ρ::prec` : Density at radius r. - - `μ::prec` : Shear modulus at radius r. + - `μ::precc` : Shear modulus at radius r. - `K::prec` : Bulk modulus at radius r. - `n::Int` : Tidal degree. # Notes See also [`get_B`](@ref) """ - function get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, n) + function get_B!(B::Array{precc,2}, ω::prec, r1::prec, r2::prec, g1::prec, g2::prec, ρ::prec, μ::precc, K::prec, n::Int) dr = r2 - r1 rhalf = r1 + 0.5dr @@ -314,7 +315,7 @@ module solid1d_mush - `g1::prec` : Gravity at radius r1. - `g2::prec` : Gravity at radius r2. - `ρ::prec` : Density at radius r. - - `μ::prec` : Shear modulus at radius r. + - `μ::precc` : Shear modulus at radius r. - `K::prec` : Bulk modulus at radius r. - `ρₗ::prec` : Liquid density at radius r. - `Kl::prec` : Liquid bulk modulus at radius r. @@ -331,7 +332,7 @@ module solid1d_mush # Notes See 'get_B!' for definition. """ - function get_B(ω, r1, r2, g1, g2, ρ, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + function get_B(ω::prec, r1::prec, r2::prec, g1::prec, g2::prec, ρ::prec, μ::precc, K::prec, ρₗ::prec, Kl::prec, Kd::prec, α::prec, ηₗ::prec, ϕ::prec, k::prec, n::Int) B = zeros(precc, 8, 8) get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) @@ -353,7 +354,7 @@ module solid1d_mush - `g1::prec` : Gravity at radius r1. - `g2::prec` : Gravity at radius r2. - `ρ::prec` : Density at radius r. - - `μ::prec` : Shear modulus at radius r. + - `μ::precc` : Shear modulus at radius r. - `K::prec` : Bulk modulus at radius r. - `ρₗ::prec` : Liquid density at radius r. - `Kl::prec` : Liquid bulk modulus at radius r. @@ -367,7 +368,7 @@ module solid1d_mush # Notes See also [`get_B`](@ref) """ - function get_B!(B, ω, r1, r2, g1, g2, ρ, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + function get_B!(B::Array{precc,2}, ω::prec, r1::prec, r2::prec, g1::prec, g2::prec, ρ::prec, μ::precc, K::prec, ρₗ::prec, Kl::prec, Kd::prec, α::prec, ηₗ::prec, ϕ::prec, k::prec, n::Int) dr = r2 - r1 rhalf = r1 + 0.5dr @@ -405,23 +406,23 @@ module solid1d_mush Hay et al., (2025). # Arguments - - `Bprod2::Array{precc,4}` : 8x8x(nr-1)x(nlayers-1) array to store the B products across each secondary layer within each primary layer. + - `Bprod2::Array{precc}` : 8x8x(nr-1)x(nlayers-1) array to store the B products across each secondary layer within each primary layer. - `ω::prec` : Forcing frequency. - - `r::Array{prec,2}` : 2D array of layer boundaries. - - `ρ::Array{prec,1}` : 1D array of layer densities. - - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. - - `μ::Array{prec,1}` : 1D array of layer shear moduli. - - `K::Array{prec,1}` : 1D array of layer bulk moduli. - - `ρₗ::Array{prec,1}` : 1D array of liquid densities at layer boundaries. - - `Kl::Array{prec,1}` : 1D array of liquid bulk moduli at layer boundaries. - - `Kd::Array{prec,1}` : 1D array of drained bulk moduli at layer boundaries. - - `α::Array{prec,1}` : 1D array of Biot coefficients at layer boundaries. - - `ηₗ::Array{prec,1}` : 1D array of liquid viscosities at layer boundaries. - - `ϕ::Array{prec,1}` : 1D array of porosities at layer boundaries. - - `k::Array{prec,1}` : 1D array of permeabilities at layer boundaries. + - `r::SubArray{prec}` : 1D array of layer boundaries. + - `ρ::prec` : 1D array of layer densities. + - `g::SubArray{prec}` : 1D array of gravity values at the layer boundaries. + - `μ::precc` : 1D array of layer shear moduli. + - `K::prec` : 1D array of layer bulk moduli. + - `ρₗ::prec` : 1D array of liquid densities at layer boundaries. + - `Kl::prec` : 1D array of liquid bulk moduli at layer boundaries. + - `Kd::prec` : 1D array of drained bulk moduli at layer boundaries. + - `α::prec` : 1D array of Biot coefficients at layer boundaries. + - `ηₗ::prec` : 1D array of liquid viscosities at layer boundaries. + - `ϕ::prec` : 1D array of porosities at layer boundaries. + - `k::prec` : 1D array of permeabilities at layer boundaries. - `n::Int` : Tidal degree. """ - function get_B_product!(Bprod2, ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n) + function get_B_product!(Bprod2::Array{precc}, ω::prec, r::SubArray{prec}, ρ::prec, g::SubArray{prec}, μ::precc, K::prec, ρₗ::prec, Kl::prec, Kd::prec, α::prec, ηₗ::prec, ϕ::prec, k::prec, n::Int) # Check dimensions of Bprod2 nr = size(r)[1] @@ -470,7 +471,7 @@ module solid1d_mush - `r::Array{prec,2}` : 2D array of layer boundaries. - `ρ::Array{prec,1}` : 1D array of layer densities. - `g::Array{prec,2}` : 2D array of gravity values at the layer boundaries. - - `μ::Array{prec,1}` : 1D array of layer shear moduli. + - `μ::Array{precc,1}` : 1D array of layer shear moduli. - `K::Array{prec,1}` : 1D array of layer bulk moduli. - `ρₗ::Array{prec,1}` : 1D array of liquid densities at layer boundaries. - `Kl::Array{prec,1}` : 1D array of liquid bulk moduli at layer boundaries. @@ -488,10 +489,10 @@ module solid1d_mush - `core::String="liquid"` : Type of core, either "liquid" or "solid". This is used to compute the starting vector for the numerical integration across the interior. # Returns - - `M::Array{precc,2}` : 4x4 M matrix, which is used to propagate the solution across the entire interior. - - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. + - `M::Array{precc,2}` : 4x4 M matrix, which is used to propagate the solution across the entire interior. + - `y1_4::Array{precc,4}` : 4D array of the y solutions across each layer, which is used in the `compute_y` function to compute the solution vector across the interior. """ - function compute_M(ω, r, ρ, g, μ, K, ρₗ, Kl, Kd, α, ηₗ, ϕ, k, n, ρ_core, μ_core, κ_core; core="liquid") + function compute_M(ω::prec, r::Array{prec,2}, ρ::Array{prec,1}, g::Array{prec,2}, μ::Array{precc,1}, K::Array{prec,1}, ρₗ::Array{prec,1}, Kl::Array{prec,1}, Kd::Array{prec,1}, α::Array{prec,1}, ηₗ::Array{prec,1}, ϕ::Array{prec,1}, k::Array{prec,1}, n::Int, ρ_core::prec, μ_core::prec, κ_core::prec; core::String="liquid") porous_layer = ϕ .> 0.0 ## Convert parameters to the precision of precc: @@ -564,7 +565,7 @@ module solid1d_mush # Returns - `y::Array{ComplexF64,4}` : 4D array of the solution vector y across the interior. """ - function compute_y(r, g, M, y1_4, n; load=false) + function compute_y(r::Array{prec,2}, g::Array{prec,2}, M::Array{precc,2}, y1_4::Array{precc,4}, n::Int; load::Bool=false) tau = 0.0 P = 0.0 @@ -608,24 +609,24 @@ module solid1d_mush Calculate the strain tensor ϵ at a particular radial level. # Arguments - - `ϵ::Array{ComplexF64,4}` : 4D array to store the strain tensor at a particular radial level, with dimensions corresponding to latitude, longitude, and the 6 independent components of the strain tensor. - - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 6 components. + - `ϵ::SubArray` : 4D array to store the strain tensor at a particular radial level, with dimensions corresponding to latitude, longitude, and the 6 independent components of the strain tensor. + - `y::SubArray` : 1D array of the solution vector y at a particular radial level, with 6 components. - `n::Int` : Tidal degree. - - `rr::prec` : Radius at which to compute the strain tensor. - - `ρr::prec` : Density at radius rr. - - `gr::prec` : Gravity at radius rr. - - `μr::prec` : Shear modulus at radius rr. - - `Ksr::prec` : Bulk modulus at radius rr. - - `ω::prec` : Forcing frequency. - - `ρlr::prec` : Liquid density at radius rr. - - `Klr::prec` : Liquid bulk modulus at radius rr. - - `Kdr::prec` : Drained bulk modulus at radius rr. - - `αr::prec` : Biot coefficient at radius rr. - - `ηlr::prec` : Liquid viscosity at radius rr. - - `ϕr::prec` : Porosity at radius rr. - - `kr::prec` : Permeability at radius rr. + - `rr::Float64` : Radius at which to compute the strain tensor. + - `ρr::Float64` : Density at radius rr. + - `gr::Float64` : Gravity at radius rr. + - `μr::ComplexF64` : Shear modulus at radius rr. + - `Ksr::Float64` : Bulk modulus at radius rr. + - `ω::Float64` : Forcing frequency. + - `ρlr::Float64` : Liquid density at radius rr. + - `Klr::Float64` : Liquid bulk modulus at radius rr. + - `Kdr::Float64` : Drained bulk modulus at radius rr. + - `αr::Float64` : Biot coefficient at radius rr. + - `ηlr::Float64` : Liquid viscosity at radius rr. + - `ϕr::Float64` : Porosity at radius rr. + - `kr::Float64` : Permeability at radius rr. """ - function compute_strain_ten!(ϵ, y, n, rr, ρr, gr, μr, Ksr, ω, ρlr, Klr, Kdr, αr, ηlr, ϕr, kr) + function compute_strain_ten!(ϵ::SubArray, y::SubArray, n::Int, rr::Float64, ρr::Float64, gr::Float64, μr::ComplexF64, Ksr::Float64, ω::Float64, ρlr::Float64, Klr::Float64, Kdr::Float64, αr::Float64, ηlr::Float64, ϕr::Float64, kr::Float64) i = 1 @views Y = solid1d_mush.Y[i,:,:] @@ -659,18 +660,18 @@ module solid1d_mush Calculate the Darcy displacement vector at a particular radial level. # Arguments - - `dis_rel::Array{ComplexF64,4}` : 4D array to store the Darcy displacement vector at a particular radial level, with dimensions corresponding to latitude, longitude, and the 3 components of the Darcy displacement vector. - - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 8 components. + - `dis_rel::SubArray` : 4D array to store the Darcy displacement vector at a particular radial level, with dimensions corresponding to latitude, longitude, and the 3 components of the Darcy displacement vector. + - `y::SubArray` : 1D array of the solution vector y at a particular radial level, with 8 components. - `n::Int` : Tidal degree. - - `r::prec` : Radius at which to compute the Darcy displacement vector. - - `ω::prec` : Forcing frequency. - - `ϕ::prec` : Porosity at radius r. - - `ηl::prec` : Liquid viscosity at radius r. - - `k::prec` : Permeability at radius r. - - `g::prec` : Gravity at radius r. - - `ρl::prec` : Liquid density at radius r. + - `r::Float64` : Radius at which to compute the Darcy displacement vector. + - `ω::Float64` : Forcing frequency. + - `ϕ::Float64` : Porosity at radius r. + - `ηl::Float64` : Liquid viscosity at radius r. + - `k::Float64` : Permeability at radius r. + - `g::Float64` : Gravity at radius r. + - `ρl::Float64` : Liquid density at radius r. """ - function compute_darcy_displacement!(dis_rel, y, n, r, ω, ϕ, ηl, k, g, ρl) + function compute_darcy_displacement!(dis_rel::SubArray, y::SubArray, n::Int, r::Float64, ω::Float64, ϕ::Float64, ηl::Float64, k::Float64, g::Float64, ρl::Float64) i = 1 @views Y = solid1d_mush.Y[i,:,:] @@ -695,11 +696,11 @@ module solid1d_mush Calculate the fluid pore pressur at a particular radial level. # Arguments - - `p::Array{ComplexF64,4}` : 4D array to store the pore pressure at a particular radial level, with dimensions corresponding to latitude and longitude. - - `y::Array{precc,1}` : 1D array of the solution vector y at a particular radial level, with 8 components. + - `p::SubArray` : 4D array to store the pore pressure at a particular radial level, with dimensions corresponding to latitude and longitude. + - `y::SubArray` : 1D array of the solution vector y at a particular radial level, with 8 components. - `n::Int` : Tidal degree. """ - function compute_pore_pressure!(p, y, n) + function compute_pore_pressure!(p::SubArray, y::SubArray, n::Int) i = 1 @views Y = solid1d_mush.Y[i,:,:] @@ -724,20 +725,20 @@ module solid1d_mush layers will be caclulated. # Arguments - - `y::Array{ComplexF64,4}` : 4D array of the solution vector y across the interior, returned by `compute_y`. - - `r::Array{Float64,2}` : 2D array of layer boundaries. - - `ρ::Array{Float64,1}` : 1D array of layer densities. - - `g::Array{Float64,2}` : 2D array of gravity values at the layer boundaries. - - `μ::Array{Float64,1}` : 1D array of layer shear moduli. - - `Ks::Array{Float64,1}` : 1D array of layer bulk moduli for shear dissipation. - - `ω::Float64` : Tidal frequency in radians per second. - - `ρl::Array{Float64,1}` : 1D array of liquid densities at layer boundaries. - - `Kl::Array{Float64,1}` : 1D array of liquid bulk moduli at layer boundaries. - - `Kd::Array{Float64,1}` : 1D array of drained bulk moduli at layer boundaries. - - `α::Array{Float64,1}` : 1D array of Biot coefficients at layer boundaries. - - `ηl::Array{Float64,1}` : 1D array of liquid viscosities at layer boundaries. - - `ϕ::Array{Float64,1}` : 1D array of porosities at layer boundaries. - - `k::Array{Float64,1}` : 1D array of permeabilities at layer boundaries. + - `y::Array{ComplexF64}` : 4D array of the solution vector y across the interior, returned by `compute_y`. + - `r::Matrix` : 2D array of layer boundaries. + - `ρ::AbstractVector` : 1D array of layer densities. + - `g::Matrix` : 2D array of gravity values at the layer boundaries. + - `μ::AbstractVector` : 1D array of layer shear moduli. + - `Ks::AbstractVector` : 1D array of layer bulk moduli for shear dissipation. + - `ω::prec` : Tidal frequency in radians per second. + - `ρl::AbstractVector` : 1D array of liquid densities at layer boundaries. + - `Kl::AbstractVector` : 1D array of liquid bulk moduli at layer boundaries. + - `Kd::AbstractVector` : 1D array of drained bulk moduli at layer boundaries. + - `α::AbstractVector` : 1D array of Biot coefficients at layer boundaries. + - `ηl::AbstractVector` : 1D array of liquid viscosities at layer boundaries. + - `ϕ::AbstractVector` : 1D array of porosities at layer boundaries. + - `k::AbstractVector` : 1D array of permeabilities at layer boundaries. - `n::Int` : Tidal degree. # Keyword Arguments @@ -751,7 +752,23 @@ module solid1d_mush - `El_tot::Array{Float64,1}` : 1D array of total power dissipated in each primary layer due to Darcy flow, in W. - `El_vol::Array{Float64,2}` : 2D array of angular averaged volumetric heating profiles in W/m^3 for dissipation due to Darcy flow, with dimensions corresponding to the secondary layer and primary layer indices. """ - function get_heating_profile(y, r, ρ, g, μ, Ks, ω, ρl, Kl, Kd, α, ηl, ϕ, k, n; lay=nothing) + function get_heating_profile(y::Array{ComplexF64}, r::Matrix, ρ::AbstractVector, g::Matrix, μ::AbstractVector, Ks::AbstractVector, ω::prec, ρl::AbstractVector, Kl::AbstractVector, Kd::AbstractVector, α::AbstractVector, ηl::AbstractVector, ϕ::AbstractVector, k::AbstractVector, n::Int; lay::Union{Int,Nothing}=nothing) + + # convert to Float64 or ComplexF64 for heating calculations + r = Float64.(r) + ρ = Float64.(ρ) + g = Float64.(g) + μ = ComplexF64.(μ) + Ks = Float64.(Ks) + ω = Float64(ω) + ρl = Float64.(ρl) + Kl = Float64.(Kl) + Kd = Float64.(Kd) + α = Float64.(α) + ηl = Float64.(ηl) + ϕ = Float64.(ϕ) + k = Float64.(k) + dres = deg2rad(solid1d_mush.res) @views clats = solid1d_mush.clats[:] From a4a1b7e16d79ed270aea4545fb7485499f2b00dd Mon Sep 17 00:00:00 2001 From: Marijn Date: Thu, 14 May 2026 20:20:47 +0000 Subject: [PATCH 34/36] FIxed issue where y values where mapped incorrectly. --- src/Obliqua.jl | 9 --------- src/solid1d_mush_relax.jl | 37 ++++++++++++++++++------------------- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/Obliqua.jl b/src/Obliqua.jl index 1fc2ecc..35d6d21 100644 --- a/src/Obliqua.jl +++ b/src/Obliqua.jl @@ -1603,15 +1603,6 @@ module Obliqua y_t, r_grid, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, SphericalGrid ) - # check if nan in heating profile, if so, set to zero - # warn user if nan in heating profile - if any(isnan.(Eμ_tot)) || any(isnan.(Eκ_tot)) || any(isnan.(El_tot)) - @info "NaN values found in heating profile, setting to zero. This can occur when the solution is unstable, e.g. for high frequencies or high porosities." - end - Eμ_tot[isnan.(Eμ_tot)] .= 0.0 - Eκ_tot[isnan.(Eκ_tot)] .= 0.0 - El_tot[isnan.(El_tot)] .= 0.0 - # Eμ_map, Eκ_map, El_map = solid1d_mush_relax.get_heating_map( # y_t, r_grid, ρ, g, μc, κs, omega, ρl, κl, κd, α, ηl, ϕ, k, n, SphericalGrid # ) diff --git a/src/solid1d_mush_relax.jl b/src/solid1d_mush_relax.jl index 94d832f..09a84ce 100644 --- a/src/solid1d_mush_relax.jl +++ b/src/solid1d_mush_relax.jl @@ -307,6 +307,7 @@ module solid1d_mush_relax start_id, end_id = ids i = start_id + # define target columns for the 6x6 system variables in the 8x8 system target_cols = [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]] I8 = Matrix{precc}(I, 8, 8) @@ -349,12 +350,16 @@ module solid1d_mush_relax # 4. Perform recursion Xn = Pn * R[i-1] + Sn + Kn - if i == start_id - # For the first step into the mush, we may need to use pinv if the system is not yet fully constrained by the solid boundary conditions. - R[i] .= -pinv(Xn) * Qn - else - R[i] .= -Xn \ Qn - end + R_ifc .= -pinv(Xn) * Qn + + # create a mask or list of all column indices EXCEPT Y[7] and Y[8] + active_cols = [idx for idx in 1:8 if idx != Y[7] && idx != Y[8]] + + # update R only for the rows that are not the Darcy flux constraint + # this ensures y8 remains zero at the interface + R[i][:, active_cols] .= R_ifc[:, active_cols] + R[i][:, Y[7]] .= 0.0 # explicitly enforce the impermeable boundary + R[i][:, Y[8]] .= 0.0 # explicitly enforce the impermeable boundary # 5. Update the "stored" lower halves for the next iteration Cn_l = Cn[5:8, :] @@ -406,9 +411,9 @@ module solid1d_mush_relax start_id, end_id = ids i = start_id - # target_cols - target_cols = [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]] - + # define target columns for the 6x6 system variables in the 8x8 system + target_cols = [1,2,3,5,6,7] + I8 = Matrix{precc}(I, 8, 8) Cn = I8 @@ -447,21 +452,13 @@ module solid1d_mush_relax Sn = [Sn_u; Cn_u] Qn = [Qn_u; Dnp_u] Kn = zeros(precc, 8, 8) - Kn[Y[8], Y[8]] = -1.0 + Kn[Y[8], Y[8]] = 1.0 # perform recursion R_prev = R[i-1] Xn = Pn * R_prev + Sn + Kn - if i == start_id - # for the first step into the mush, we may need to use pinv if the system is not yet fully constrained by the solid boundary conditions. - R_ifc = -pinv(Xn) * Qn - else - R_ifc = -Xn \ Qn - end - - # CHECK THIS, theory needs to be double checked - # R[i] .= R_ifc + R_ifc = -pinv(Xn) * Qn # create a mask or list of all row indices EXCEPT Y[8] active_rows = [idx for idx in 1:8 if idx != Y[8]] @@ -581,6 +578,8 @@ module solid1d_mush_relax """ function core_boundary_mush(R::Vector, ids::Tuple{Int, Int}, r::Vector{prec}, ρ::Vector{prec}, g::Vector{prec}, μ::Vector{precc}, K::Vector{prec}, ω::prec, ρₗ::Vector{prec}, Kl::Vector{prec}, Kd::Vector{prec}, α::Vector{prec}, ηₗ::Vector{prec}, ϕ::Vector{prec}, k::Vector{prec}, ρ_core::prec, μ_core::prec, κ_core::prec, core::String, n::Int; G0=1, Y=[1,2,3,4,5,6,7,8]) + println("Bad") + start_id, end_id = ids # boundary conditions From 587df91e65b713dca91eae7bc831e0551979927c Mon Sep 17 00:00:00 2001 From: Marijn Date: Thu, 14 May 2026 20:40:08 +0000 Subject: [PATCH 35/36] Updated tests, added new test for solid1d_mush_relax. --- src/solid1d_mush_relax.jl | 8 ++-- test/example_k2.jl | 8 +++- test/runtests.jl | 91 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/solid1d_mush_relax.jl b/src/solid1d_mush_relax.jl index 09a84ce..96c3fa1 100644 --- a/src/solid1d_mush_relax.jl +++ b/src/solid1d_mush_relax.jl @@ -302,7 +302,7 @@ module solid1d_mush_relax - `Cn_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Cn matrix for the next iteration. - `Dnp_l::Matrix{precc}` : Updated 3x6 matrix representing the "stored" lower half of the Dnp matrix for the next iteration. """ - function interface_mush_solid(R::Vector, Cn_l::Matrix{precc}, Dnp_l::Matrix{precc}, ids::Tuple{Int, Int}, r::Vector{prec}, ρ::Vector{prec}, g::Vector{prec}, μ::Vector{prec}, K::Vector{prec}, ω::prec, ρₗ::Vector{prec}, Kl::Vector{prec}, Kd::Vector{prec}, α::Vector{prec}, ηₗ::Vector{prec}, ϕ::Vector{prec}, k::Vector{prec}, n::Int; G0=1, Y=[1,2,3,4,5,6,7,8]) + function interface_mush_solid(R::Vector, Cn_l::Matrix{precc}, Dnp_l::Matrix{precc}, ids::Tuple{Int, Int}, r::Vector{prec}, ρ::Vector{prec}, g::Vector{prec}, μ::Vector{precc}, K::Vector{prec}, ω::prec, ρₗ::Vector{prec}, Kl::Vector{prec}, Kd::Vector{prec}, α::Vector{prec}, ηₗ::Vector{prec}, ϕ::Vector{prec}, k::Vector{prec}, n::Int; G0=1, Y=[1,2,3,4,5,6,7,8]) start_id, end_id = ids i = start_id @@ -350,7 +350,7 @@ module solid1d_mush_relax # 4. Perform recursion Xn = Pn * R[i-1] + Sn + Kn - R_ifc .= -pinv(Xn) * Qn + R_ifc = -pinv(Xn) * Qn # create a mask or list of all column indices EXCEPT Y[7] and Y[8] active_cols = [idx for idx in 1:8 if idx != Y[7] && idx != Y[8]] @@ -459,7 +459,7 @@ module solid1d_mush_relax Xn = Pn * R_prev + Sn + Kn R_ifc = -pinv(Xn) * Qn - + # create a mask or list of all row indices EXCEPT Y[8] active_rows = [idx for idx in 1:8 if idx != Y[8]] @@ -578,8 +578,6 @@ module solid1d_mush_relax """ function core_boundary_mush(R::Vector, ids::Tuple{Int, Int}, r::Vector{prec}, ρ::Vector{prec}, g::Vector{prec}, μ::Vector{precc}, K::Vector{prec}, ω::prec, ρₗ::Vector{prec}, Kl::Vector{prec}, Kd::Vector{prec}, α::Vector{prec}, ηₗ::Vector{prec}, ϕ::Vector{prec}, k::Vector{prec}, ρ_core::prec, μ_core::prec, κ_core::prec, core::String, n::Int; G0=1, Y=[1,2,3,4,5,6,7,8]) - println("Bad") - start_id, end_id = ids # boundary conditions diff --git a/test/example_k2.jl b/test/example_k2.jl index e54089a..bb41d9c 100644 --- a/test/example_k2.jl +++ b/test/example_k2.jl @@ -14,7 +14,7 @@ using Obliqua using Plots using Printf -@info "Begin Love tests" +@info "Begin Obliqua tests" # Prepare RES_DIR = joinpath(ROOT_DIR,"res/") @@ -63,7 +63,11 @@ if suite > 10 # Load interior model omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = - load.load_interior_mush_full("$RES_DIR/interior_data/test_mantle_mush_full_test.json", false) + # load.load_interior_mush_full("/home/marijn/PROTEUS/Obliqua_data/output252556.0.json", false) + # load.load_interior_mush_full("/home/marijn/PROTEUS/Obliqua_data/output391705.0.json", false) + # load.load_interior_mush_full("/home/marijn/PROTEUS/Obliqua_data/output2022589.0.json", false) + load.load_interior_mush_full("/home/marijn/PROTEUS/Obliqua_data/output272353.0.json", false) + # load.load_interior_mush_full("$RES_DIR/interior_data/test_mantle_mush_full_test.json", false) # Run tidal calculation power_prf, power_blk, σ_range, imag_k2 = Obliqua.run_tides( diff --git a/test/runtests.jl b/test/runtests.jl index f14f418..517547a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -394,6 +394,97 @@ if suite > 4 end + +if suite > 4 + # test solid1d-mush-relax module with andrade rheology + @info " " + @info "Testing solid1d-mush-relax module with andrade rheology" + + # update config to use only solid1d-mush-relax + cfg["orbit"]["obliqua"]["module_solid"] = "solid1d-mush-relax" + cfg["orbit"]["obliqua"]["module_fluid"] = "none" + cfg["orbit"]["obliqua"]["module_mushy"] = "none" + + cfg["orbit"]["obliqua"]["material"] = "andrade" + + # lower visc_sus to include mush + visc_sus = cfg["orbit"]["obliqua"]["visc_sus"] = 5e10 + + omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = + load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) + + power_prf_expt = [0.0, 8.74351929600201e-15, 7.989822610372303e-15, 7.414357486004026e-15, 6.9903349870719045e-15, 6.610536772586215e-15, 6.250219315451601e-15, 5.907839423696174e-15, 3.847794467433342e-13, 3.7602963735237755e-13, 3.7821403882429245e-13, 3.755688275096694e-13, 3.6466650136154196e-13, 3.6218289299356724e-13, 3.498424065639542e-13, 3.3578880779258607e-13, 3.1142492251997875e-13, 2.7888784172442886e-13, 2.4067411322774943e-13, 2.0618867972042614e-13, 1.8185229307490638e-13, 1.9593218070240336e-13, 2.4264228253900505e-13, 3.0368770742530933e-13, 3.2959863443734273e-13, 2.85511135227396e-13, 1.7904773557765943e-13, 9.079975626260086e-14, 1.2268593552292066e-13, 2.6093493239577975e-13, 3.473651439126167e-13, 2.4789279079589914e-13, 7.19425263293606e-14, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 2.3322822301750936e7 + imag_k2_expt = [0.03195660623239862] + + power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( + omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg + ) + test_pass = true + + test_pass &= all(isapprox.(power_prf, power_prf_expt; rtol=rtol, atol=atol)) + test_pass &= isapprox(power_blk, power_blk_expt; rtol=rtol) + test_pass &= all(isapprox.(imag_k2, imag_k2_expt; rtol=rtol)) + + @info "First 5 expected profile elements: $(power_prf_expt[1:5])" + @info "First 5 modelled profile elements: $(power_prf[1:5])" + @info "Expected total power = $(power_blk_expt) W" + @info "Modelled total power = $(power_blk) W" + @info "Expected imag(k2): $(imag_k2_expt)" + @info "Modelled imag(k2): $(imag_k2)" + + if test_pass + @info "Pass" + else + @warn "Fail" + failed += 1 + end + total += 1 + @info "--------------------------" + + @info " " + @info "Testing solid1d-mush-relax module with maxwell rheology" + + # update config to use maxwell rheology + cfg["orbit"]["obliqua"]["material"] = "maxwell" + + omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, ncalc = + load.load_interior_mush_full("$RES_DIR/interior_data/runtests_mantle.json", false) + + power_prf_expt = [0.0, 7.75383624571581e-15, 1.92697256529584e-18, 1.7084323937987319e-18, 1.5981073286540342e-18, 1.511049911419673e-18, 1.4287862784121927e-18, 1.3506250476775037e-18, 9.182142839201793e-14, 8.250060731715999e-14, 8.631387374789046e-14, 8.681746558266561e-14, 7.837069034933429e-14, 7.881598823431243e-14, 7.297217606240423e-14, 7.090883070546616e-14, 6.534752948270942e-14, 5.784562948163095e-14, 4.45022033344104e-14, 3.058831774403776e-14, 1.5101318147024206e-14, 4.713628266289685e-15, 3.6846633767230745e-15, 1.2885674315548983e-14, 3.207407421406593e-14, 5.0144700080866385e-14, 5.672346564376264e-14, 4.552903385255474e-14, 2.2155190422890972e-14, 4.319509144233177e-15, 6.8952783311108974e-15, 2.7946013816443167e-14, 4.661646947084847e-14, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + power_blk_expt = 4.215267225204773e6 + imag_k2_expt = [0.0057757004335659504] + + power_prf, power_blk, _, imag_k2 = Obliqua.run_tides( + omega, axial, ecc, sma, S_mass, rho, radius, visc, shear, bulk, phi, cfg + ) + test_pass = true + + test_pass &= all(isapprox.(power_prf, power_prf_expt; rtol=rtol, atol=atol)) + test_pass &= isapprox(power_blk, power_blk_expt; rtol=rtol) + test_pass &= all(isapprox.(imag_k2, imag_k2_expt; rtol=rtol)) + + @info "First 5 expected profile elements: $(power_prf_expt[1:5])" + @info "First 5 modelled profile elements: $(power_prf[1:5])" + @info "Expected total power = $(power_blk_expt) W" + @info "Modelled total power = $(power_blk) W" + @info "Expected imag(k2): $(imag_k2_expt)" + @info "Modelled imag(k2): $(imag_k2)" + + if test_pass + @info "Pass" + else + @warn "Fail" + failed += 1 + end + total += 1 + @info "--------------------------" + + visc_sus = cfg["orbit"]["obliqua"]["visc_sus"] = 5e13 + +end + + if suite > 2 # test fluid0d model @info " " From 72b3d6caf49addedf35914eaee78584bdff7371e Mon Sep 17 00:00:00 2001 From: Marijn Date: Thu, 14 May 2026 21:23:22 +0000 Subject: [PATCH 36/36] Updated docstrings. --- src/solid0d.jl | 11 ++++------- src/solid1d_mush_relax.jl | 25 +++++++++++++++++-------- src/solid1d_relax.jl | 14 ++++++-------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/solid0d.jl b/src/solid0d.jl index 7c34494..4f460eb 100644 --- a/src/solid0d.jl +++ b/src/solid0d.jl @@ -11,18 +11,15 @@ module solid0d """ - compute_solid_lovenumbers(omega, R, H_magma, g, ρ_ratio, n, σ_R) + compute_solid_lovenumbers(μc, mass_tot, R, n) Calculate k2 lovenumbers in solid. # Arguments - - `omega::Float64` : Forcing frequency range. - - `R::prec` : Outer radius of fluid segment in mantle. - - `H_magma::prec` : Height of fluid segment in mantle. - - `g::prec` : Surface gravity at top of fluid segment in mantle. - - `ρ_ratio::prec` : Density contrast between current (fluid) and lower (non-fluid) layer. + - `μc::precc` : Complex shear modulus. + - `mass_tot::prec` : Total mass of the planet. + - `R::prec` : Outer radius of the planet. - `n::Int=2` : Power of the radial factor (goes with (r/a)^{n}, since r<