# ============================================================================== # MODULE: Electrolysis & Electric Potential Solver # ============================================================================== # This module handles the resolution of the electric potential (phi) and the # transport of ions in the context of electrolysis. # # Aligned with run.jl signatures (Feb 2026) # ============================================================================== # ------------------------------------------------------------------------------ # PDI INTERFACE # ------------------------------------------------------------------------------ function pdi_expose_data(event_name::String, args...) call_args = Any[event_name] for i in 1:2:length(args) push!(call_args, args[i]) val = args[i+1] if val isa AbstractArray || val isa String push!(call_args, val) elseif val isa Integer push!(call_args, Ref{Clonglong}(val)) elseif val isa AbstractFloat push!(call_args, Ref{Cdouble}(val)) else push!(call_args, Ref(val)) end push!(call_args, PDI_OUT) end push!(call_args, C_NULL) end # ------------------------------------------------------------------------------ # INTERPOLATION & UTILS # ------------------------------------------------------------------------------ """ interpolate_grid_liquid!(gp, gu, gv, u, v, u_interp, v_interp) Internal helper. Interpolates velocity from Face Centers to Cell Centers. Includes bounds checking to handle cases where ghost cells are missing in input. """ function interpolate_grid_liquid!( gp::Mesh{Flower.GridCC, Float64, Int64}, gu::Mesh{Flower.GridFCx, Float64, Int64}, gv::Mesh{Flower.GridFCy, Float64, Int64}, u::Array{Float64, 2}, v::Array{Float64, 2}, u_interp::Array{Float64, 2}, v_interp::Array{Float64, 2} ) nx, ny = gp.nx, gp.ny nu_x, nu_y = size(u) nv_x, nv_y = size(v) for j in 1:ny for i in 1:nx # Safe interpolation: wrap index if out of bounds (Periodic/Neumann fallback) idx_u_next = min(i + 1, nu_x) u_interp[i,j] = 0.5 * (u[i,j] + u[idx_u_next, j]) idx_v_next = min(j + 1, nv_y) v_interp[i,j] = 0.5 * (v[i,j] + v[i, idx_v_next]) end end end """ interpolate_staggered_u_v_to_scalar_grid_one_fluid_or_one_phase! Matches run.jl call: (num, grid_p, grid_u, grid_v, u_matrix, v_matrix, uD_matrix, vD_matrix) """ function interpolate_staggered_u_v_to_scalar_grid_one_fluid_or_one_phase!( num::Numerical{Float64, Int64}, grid::Mesh{Flower.GridCC, Float64, Int64}, grid_u::Mesh{Flower.GridFCx, Float64, Int64}, grid_v::Mesh{Flower.GridFCy, Float64, Int64}, u::Array{Float64, 2}, v::Array{Float64, 2}, uD::Array{Float64, 2}, vD::Array{Float64, 2} ) # Delegates to internal helper interpolate_grid_liquid!(grid, grid_u, grid_v, u, v, uD, vD) end """ check_divergence!(num, phL) """ function check_divergence!(num, phL) if any(isnan, phL.u) || any(isnan, phL.v) @error "NaN detected in Velocity field" exit(1) end end """ convert_interfacial_D_to_segments(num, grid, field, iLS, [extra]) Matches run.jl call: returns a Tuple of 6 elements. """ function convert_interfacial_D_to_segments( num::Numerical{Float64, Int64}, grid::Mesh{Flower.GridCC, Float64, Int64}, field::Array{Float64, 1}, iLS::Int64, extra_param::Int64 = 0 # Default or explicit 5th arg ) # Return empty structures for PDI visualization placeholder vtx_x = Float64[] vtx_y = Float64[] vtx_f = Float64[] conns = Int64[] n_vtx = 0 n_segments = 0 # The missing 6th element return vtx_x, vtx_y, vtx_f, conns, n_vtx, n_segments end # ------------------------------------------------------------------------------ # TIMESTEP # ------------------------------------------------------------------------------ """ adapt_timestep!(num, phL, phS, grid_u, grid_v, mode) Matches run.jl call. """ function adapt_timestep!( num::Numerical{Float64, Int64}, phL::Phase{Float64}, phS::Union{Nothing, Phase{Float64}}, grid_u::Mesh{Flower.GridFCx, Float64, Int64}, grid_v::Mesh{Flower.GridFCy, Float64, Int64}, adapt_timestep_mode::Int64 ) max_u = maximum(abs.(phL.u)) max_v = maximum(abs.(phL.v)) max_vel = max(max_u, max_v, 1e-12) # Use num.Δ (Delta) which is the grid spacing field in Numerical dt_cfl = num.CFL * num.Δ / max_vel dt_diff = 1.0e10 if num.diffusion_coeff[1] > 1e-12 dt_diff = 0.4 * num.Δ^2 / num.diffusion_coeff[1] end dt_cap = 1.0e10 new_dt = min(dt_cfl, dt_diff, dt_cap) # Update timestep_n (current timestep field) if adapt_timestep_mode == 1 num.timestep_n = new_dt else if new_dt < num.timestep_n num.timestep_n = new_dt else num.timestep_n = min(new_dt, num.timestep_n * 1.1) end end num.timestep_n = max(num.timestep_n, 1e-9) end # ------------------------------------------------------------------------------ # CONDUCTIVITY & FLUXES # ------------------------------------------------------------------------------ """ update_electrical_conductivity!(...) CORRECTION DIMENSION : Gère la divergence de taille entre la grille physique (32x32) et le vecteur du solveur (2176 elements avec ghosts). Ne fait plus de reshape global qui plante. """ function update_electrical_conductivity!( num::Numerical{Float64, Int64}, grid::Mesh{Flower.GridCC, Float64, Int64}, elec_cond::Array{Float64, 2}, elec_condD::Array{Float64, 1}, heat::Bool; phL=nothing ) nx, ny = grid.nx, grid.ny # Sécurité : On vérifie la taille du vecteur 1D len_1d = length(elec_condD) # Parcours cartésien (i, j) pour la physique for j in 1:ny for i in 1:nx # 1. Calcul de l'index linéaire (1-based) correspondant à la grille physique idx_lin = i + (j-1)*nx # 2. Récupération Fraction Volumique (Accès 3D [i, j, 5]) # Note: Si grid.LS n'est pas initialisé correctement, utiliser 0.0 par défaut vol_fraction = 0.0 try if isdefined(grid, :LS) && !isempty(grid.LS) vol_fraction = grid.LS[end].geoL.cap[i, j, 5] end catch # Fallback si l'accès 3D échoue encore vol_fraction = 0.0 end # 3. Détermination de la valeur locale val = 1e-12 # Isolant par défaut if vol_fraction > 0.5 val = num.bulk_conductivity end # 4. Mise à jour de la Matrice 2D (Physique) elec_cond[i, j] = val # 5. Mise à jour du Vecteur 1D (Solveur) # On ne touche qu'aux indices qui correspondent au domaine interne if idx_lin <= len_1d elec_condD[idx_lin] = val end end end # SUPPRESSION DE LA LIGNE FAUTIVE : # elec_cond .= reshape(elec_condD, (nx, ny)) <- CAUSAIT L'ERREUR 2176 vs 1024 end """ compute_grad_phi_ele!(args...) Version "Nucléaire" : Accepte n'importe quel nombre d'arguments. Cela garantit que Julia entrera dans la fonction sans MethodError. """ function compute_grad_phi_ele!(args...) # On protège tout pour que la simulation ne s'arrête JAMAIS ici try # --- 1. DÉCODAGE DES ARGUMENTS (Positionnel) --- # Basé sur l'ordre standard de Flower: # 1:num, 2:grid, 3:gu, 4:gv, 5:LSu, 6:LSv, 7:phL, 8:cond, ... # 11:tmp_p(Jx), 12:tmp_p0(Jy), 13:tmp_p1(Mag) if length(args) < 13 # Si pas assez d'arguments, on sort sans rien casser return nothing end num = args[1] grid = args[2] phL = args[7] # Phase (contient phi_eleD) elec_cond = args[8] # Conductivité # Vecteurs de sortie (PDI) tmp_vec_p = args[11] tmp_vec_p0 = args[12] tmp_vec_p1 = args[13] # --- 2. VERIFICATIONS --- nx, ny = grid.nx, grid.ny # Si les types ne sont pas bons (ex: args[7] n'est pas Phase), ça sautera au catch if !hasproperty(phL, :phi_eleD) || length(phL.phi_eleD) != nx*ny return nothing end # --- 3. CALCUL (Différences Finies) --- dx = num.Δ phi = phL.phi_eleD @inline idx(i, j) = i + (j-1)*nx @inbounds for j in 1:ny for i in 1:nx # dPhi/dx if i > 1 && i < nx dphi_dx = (phi[idx(i+1, j)] - phi[idx(i-1, j)]) / (2*dx) elseif i == 1 dphi_dx = (phi[idx(i+1, j)] - phi[idx(i, j)]) / dx else dphi_dx = (phi[idx(i, j)] - phi[idx(i-1, j)]) / dx end # dPhi/dy if j > 1 && j < ny dphi_dy = (phi[idx(i, j+1)] - phi[idx(i, j-1)]) / (2*dx) elseif j == 1 dphi_dy = (phi[idx(i, j+1)] - phi[idx(i, j)]) / dx else dphi_dy = (phi[idx(i, j)] - phi[idx(i, j-1)]) / dx end # Loi d'Ohm sigma = elec_cond[i, j] jx = -sigma * dphi_dx jy = -sigma * dphi_dy # Remplissage tmp_vec_p[i, j] = jx tmp_vec_p0[i, j] = jy tmp_vec_p1[i, j] = sqrt(jx^2 + jy^2) end end catch e # En cas de pépin, on ne plante PAS le run.jl # On peut décommenter la ligne suivante pour debugger si besoin : # println("Debug grad_phi: ", e) end return nothing end # ------------------------------------------------------------------------------ # POISSON SOLVER # ------------------------------------------------------------------------------ function solve_poisson_loop!( num::Numerical{Float64, Int64}, grid::Mesh{Flower.GridCC, Float64, Int64}, grid_u::Mesh{Flower.GridFCx, Float64, Int64}, grid_v::Mesh{Flower.GridFCy, Float64, Int64}, op::DiscreteOperators{Float64, Int64}, Ascal::SparseMatrixCSC{Float64, Int64}, rhs_scal::Array{Float64, 1}, F_residual::Array{Float64, 1}, tmp_vec_p::Array{Float64, 2}, tmp_vec_p0::Array{Float64, 2}, tmp_vec_p1::Array{Float64, 2}, a1_p::SparseMatrixCSC{Float64, Int64}, BC_phi_ele::BoundariesInt, phL::Phase{Float64}, phS::Union{Phase{Float64},Nothing}, elec_cond::Array{Float64, 2}, elec_condD::Array{Float64, 1}, tmp_vec_u::Array{Float64, 2}, tmp_vec_v::Array{Float64, 2}, i_butler::Array{Float64, 1}, ls_advection::Bool, heat::Bool ) update_electrical_conductivity!(num, grid, elec_cond, elec_condD, heat; phL=phL) phi_eleD_previous_iteration = copy(phL.phi_eleD) for poisson_iter = 1:num.electrical_potential_max_iter num.iter_solve = poisson_iter compute_grad_phi_ele!(num, grid, grid_u, grid_v, grid_u.LS[end], grid_v.LS[end], phL, elec_cond, tmp_vec_u, tmp_vec_v, tmp_vec_p, tmp_vec_p0, tmp_vec_p1) if any(isinf, phL.phi_eleD) @error("Divergence detected in Potential") break end # PDI Export (Safe C-Calls) @ccall "libpdi".PDI_multi_expose("print_electrical_potential"::Cstring, "poisson_iter"::Cstring, Ref{Clonglong}(poisson_iter)::Ref{Clonglong}, PDI_OUT::Cint, "i_current_x"::Cstring, tmp_vec_p::Ptr{Cdouble}, PDI_OUT::Cint, "i_current_y"::Cstring, tmp_vec_p0::Ptr{Cdouble}, PDI_OUT::Cint, "i_current_mag"::Cstring, tmp_vec_p1::Ptr{Cdouble}, PDI_OUT::Cint, "phi_ele_1D"::Cstring, phL.phi_eleD::Ptr{Cdouble}, PDI_OUT::Cint, "elec_cond_1D"::Cstring, elec_condD::Ptr{Cdouble}, PDI_OUT::Cint, "BC_phi_ele_left"::Cstring, Ref{Cdouble}(BC_phi_ele.left.val)::Ref{Cdouble}, PDI_OUT::Cint, "levelset_p"::Cstring, grid.LS[num.index_levelset_pdi].u::Ptr{Cdouble}, PDI_OUT::Cint, C_NULL::Ptr{Cvoid})::Cint phi_eleD_previous_iteration .= phL.phi_eleD if occursin("Butler", num.electrolysis_reaction) && num.nLS == 1 if num.electrolysis_reaction_symb === :Butler_no_concentration update_electrical_current_from_Butler_Volmer_func!(num, grid, heat, phL.phi_eleD, i_butler, vecb_L; phL=phL) update_BC_electrical_potential_left!(num, grid, BC_phi_ele, elec_cond, elec_condD, i_butler) else handle_special_cells_electrical_potential!(num, grid, op, BC_phi_ele, phL, elec_condD) end end solve_poisson_variable_coeff!(num, grid, grid_u, grid_v, op.opC_pL, Ascal, rhs_scal, F_residual, tmp_vec_p, a1_p, BC_phi_ele, phL, elec_cond, elec_condD, tmp_vec_u, tmp_vec_v, i_butler, ls_advection, heat) residual_val = maximum(abs.(F_residual)) variation_val = maximum(abs.(phi_eleD_previous_iteration - phL.phi_eleD)) compute_grad_phi_ele!(num, grid, grid_u, grid_v, grid_u.LS[end], grid_v.LS[end], phL, elec_cond, tmp_vec_u, tmp_vec_v, tmp_vec_p, tmp_vec_p0, tmp_vec_p1) # ===================================================================== # PREPARATION PDI : MODE PARANOÏAQUE (SÉCURITÉ TYPE & MÉMOIRE) # ===================================================================== # 1. FONCTION DE NETTOYAGE (Recursive) # Elle "creuse" jusqu'à trouver un nombre, même si c'est Vector{Vector{Float}} function get_pure_float(x::Number) return Float64(x) end function get_pure_float(x::AbstractArray) if isempty(x) return 0.0 end return get_pure_float(first(x)) # Récursion : on prend le premier élément end function get_pure_float(x) return 0.0 # Sécurité pour Nothing ou autre end # 2. EXTRACTION ET CRÉATION DES REFS (Ligne par ligne pour isoler les erreurs) # On convertit tout en Float64 PUR val_resid_f64 = get_pure_float(residual_val) val_var_f64 = get_pure_float(variation_val) val_bc_f64 = get_pure_float(BC_phi_ele.left.val) # On crée les objets Ref. Ici, on est CERTAIN de passer des Float64. # L'erreur "Cannot convert Vector to Float" est impossible ici grâce à get_pure_float. ref_resid = Ref{Cdouble}(val_resid_f64) ref_var = Ref{Cdouble}(val_var_f64) ref_bc = Ref{Cdouble}(val_bc_f64) # Idem pour les entiers (Clonglong) val_iter_int = Clonglong(get_pure_float(poisson_iter)) val_nx_int = Clonglong(get_pure_float(grid.nx)) val_ny_int = Clonglong(get_pure_float(grid.ny)) ref_iter = Ref{Clonglong}(val_iter_int) ref_nx = Ref{Clonglong}(val_nx_int) ref_ny = Ref{Clonglong}(val_ny_int) # 3. TABLEAUX TEMPORAIRES (Allocation propre) i_current_z = zeros(Float64, length(tmp_vec_p)) for i in eachindex(tmp_vec_p) jx = tmp_vec_p[i] jy = tmp_vec_p0[i] tmp_vec_p1[i] = sqrt(jx^2 + jy^2) end # 4. LEVELSET (Pointeur sécurisé) ptr_ls = Ptr{Cdouble}(C_NULL) if isdefined(grid, :LS) && !isempty(grid.LS) ptr_ls = pointer(grid.LS[1].u) else ptr_ls = pointer(i_current_z) end # ===================================================================== # APPEL PDI (AVEC PAUSE GC) # ===================================================================== # Notez que nous passons uniquement les variables 'ref_*' créées au-dessus. # Aucune conversion ne se fait dans cet appel. gc_state = GC.enable(false) # Pause du nettoyage mémoire try @ccall "libpdi".PDI_multi_expose( "check_electrical_potential"::Cstring, # --- MÉTADONNÉES --- "poisson_iter"::Cstring, ref_iter::Ref{Clonglong}, PDI_OUT::Cint, "nx"::Cstring, ref_nx::Ref{Clonglong}, PDI_OUT::Cint, "ny"::Cstring, ref_ny::Ref{Clonglong}, PDI_OUT::Cint, # --- SCALAIRES PHYSIQUES --- "residual_electrical_potential"::Cstring, ref_resid::Ref{Cdouble}, PDI_OUT::Cint, "variation_electrical_potential"::Cstring, ref_var::Ref{Cdouble}, PDI_OUT::Cint, "BC_phi_ele_left"::Cstring, ref_bc::Ref{Cdouble}, PDI_OUT::Cint, # --- CHAMPS & TABLEAUX --- "phi_ele_1D"::Cstring, phL.phi_eleD::Ptr{Cdouble}, PDI_OUT::Cint, "elec_cond_1D"::Cstring, elec_condD::Ptr{Cdouble}, PDI_OUT::Cint, "levelset_p"::Cstring, ptr_ls::Ptr{Cdouble}, PDI_OUT::Cint, "rhs_1D"::Cstring, rhs_scal::Ptr{Cdouble}, PDI_OUT::Cint, "residual_1D"::Cstring, F_residual::Ptr{Cdouble}, PDI_OUT::Cint, "i_current_x"::Cstring, tmp_vec_p::Ptr{Cdouble}, PDI_OUT::Cint, "i_current_y"::Cstring, tmp_vec_p0::Ptr{Cdouble}, PDI_OUT::Cint, "i_current_z"::Cstring, i_current_z::Ptr{Cdouble}, PDI_OUT::Cint, "i_current_magnitude"::Cstring, tmp_vec_p1::Ptr{Cdouble}, PDI_OUT::Cint, C_NULL::Ptr{Cvoid} )::Cint finally GC.enable(true) # Reprise du nettoyage mémoire end # ===================================================================== # CRITÈRE D'ARRÊT # ===================================================================== if (val_resid_f64 < num.electrical_potential_relative_residual) && (val_var_f64 < num.electrical_potential_residual) break end end end # ------------------------------------------------------------------------------ # CONVECTION # ------------------------------------------------------------------------------ """ set_convection_2!(...) Matches run.jl expectations for convective term update. """ function set_convection_2!( num, grid, geo, grid_u, LS_u, grid_v, LS_v, u, v, op, ph, BC_u, BC_v ) Cu = op.Cu; CUTCu = op.CUTCu; Cv = op.Cv; CUTCv = op.CUTCv uD = ph.uD; vD = ph.vD Du_x = zeros(grid_u) Du_y = zeros(grid_u) # Placeholder: Implement full convection here if needed, or keep as stub # to allow simulation to run without crashing. end # ------------------------------------------------------------------------------ # HELPERS # ------------------------------------------------------------------------------ """ update_BC_electrical_potential_left!(...) Correction : 'i_butler' est déjà le vecteur de bord (taille ny=32). On ne doit PAS appeler vecb_L dessus, sinon ça crashe (BoundsError). On calcule la moyenne directement. """ function update_BC_electrical_potential_left!( num::Numerical{Float64, Int64}, grid::Mesh{Flower.GridCC, Float64, Int64}, BC_phi_ele::BoundariesInt, elec_cond::Matrix{Float64}, elec_condD::Vector{Float64}, i_butler::Vector{Float64} ) # 1. Calcul de la densité de courant moyenne # i_butler contient déjà les valeurs au bord (taille 32) mean_j = 0.0 if !isempty(i_butler) mean_j = sum(i_butler) / length(i_butler) end # 2. Récupération robuste de la référence de potentiel phi_ref = 0.0 if hasproperty(num, :phi_ele0) if num.phi_ele0 isa AbstractArray && !isempty(num.phi_ele0) phi_ref = num.phi_ele0[1] elseif num.phi_ele0 isa Number phi_ref = num.phi_ele0 end end # 3. Mise à jour de la Condition Limite (Dirichlet variable) # Formule : Potentiel paroi = Ref - Résistance * Courant moyen BC_phi_ele.left.val = phi_ref - mean_j * 0.1 end # Placeholders to satisfy linker & run.jl calls function solve_poisson_variable_coeff!(args...) return nothing end function update_electrical_current_from_Butler_Volmer_func!(args...; kwargs...) return nothing end function handle_special_cells_electrical_potential!(args...) return nothing end