|
| 1 | +module ZirconGrowthExt |
| 2 | + |
| 3 | +using MagmaThermoKinematics |
| 4 | +using MagmaThermoKinematics: JLD2 |
| 5 | +using ZirconGrowth |
| 6 | + |
| 7 | +""" |
| 8 | + simulate_zircon_growth_from_tracers(Tracers; |
| 9 | + params = nothing, |
| 10 | + elements = ZirconGrowth.default_element_data(), |
| 11 | + filename = nothing, |
| 12 | + return_results = false) |
| 13 | + simulate_zircon_growth_from_tracers(tr::Tracer; kwargs...) |
| 14 | + simulate_zircon_growth_from_tracers(dirname::AbstractString; kwargs...) |
| 15 | +
|
| 16 | +Run the [ZirconGrowth.jl](https://github.com/JuliaGeodynamics/ZirconGrowth.jl) crystal-growth |
| 17 | +model on tracer Tt-paths recorded during a MagmaThermoKinematics simulation. |
| 18 | +
|
| 19 | +Three call forms are supported: |
| 20 | +- Pass a `StructArray` of `Tracer` objects directly. |
| 21 | +- Pass a single `Tracer`. |
| 22 | +- Pass a directory path; `dirname/Tracers_SimParams.jld2` is loaded automatically |
| 23 | + (saves to `dirname/ZirconGrowth.jld2` by default). |
| 24 | +
|
| 25 | +Tracers with fewer than 2 time steps are skipped. |
| 26 | +`Tracer.time_vec` must be in **Myr** and `Tracer.T_vec` in **°C**. |
| 27 | +The loop runs on all available Julia threads (`julia --threads auto`). |
| 28 | +
|
| 29 | +## Return value |
| 30 | +
|
| 31 | +By default returns a `NamedTuple` with two `Vector{Float64}` fields, one entry per |
| 32 | +successfully simulated tracer: |
| 33 | +
|
| 34 | +| Field | Description | |
| 35 | +|:--------------------|:------------| |
| 36 | +| `age_years` | Volume-averaged crystallisation age in years before the end of the simulation (see [`volume_averaged_age`](@ref)) | |
| 37 | +| `zircon_radius_um` | Final crystal radius in µm | |
| 38 | +
|
| 39 | +When `return_results = true` a third field `results` is included, containing a |
| 40 | +`Vector{SimulationResult}` with the full ZirconGrowth output for each tracer. |
| 41 | +
|
| 42 | +## Arguments |
| 43 | +- `params` : `ZirconGrowth.GrowthParams`; if `nothing` (default) a fresh instance |
| 44 | + is constructed from each tracer's Tt-path individually. |
| 45 | +- `nx` : number of 1-D spatial grid points for the ZirconGrowth solver |
| 46 | + (default 100). Ignored when `params` is provided explicitly. |
| 47 | +- `elements` : `ZirconGrowth.ElementData` selecting which trace elements are tracked; |
| 48 | + defaults to `ZirconGrowth.default_element_data()`. |
| 49 | +- `filename` : path to a JLD2 file; if provided the `age_years` and |
| 50 | + `zircon_radius_um` vectors (and `results` when requested) are saved |
| 51 | + there. Default `nothing` (no saving) for the `Tracers` form; |
| 52 | + `dirname/ZirconGrowth.jld2` for the `dirname` form. |
| 53 | +- `return_results` : set to `true` to include the full `Vector{SimulationResult}` in the |
| 54 | + return value. Default `false` to save memory. |
| 55 | +
|
| 56 | +## Examples |
| 57 | +
|
| 58 | +```julia |
| 59 | +using MagmaThermoKinematics |
| 60 | +using ZirconGrowth # triggers the extension |
| 61 | +
|
| 62 | +# from a directory produced by a simulation run |
| 63 | +age_years, zircon_radius_um = simulate_zircon_growth_from_tracers("MyRun/") |
| 64 | +
|
| 65 | +# from tracers already in memory |
| 66 | +using JLD2 |
| 67 | +Tracers = JLD2.load("MyRun/Tracers_SimParams.jld2", "Tracers") |
| 68 | +age_years, zircon_radius_um = simulate_zircon_growth_from_tracers(Tracers) |
| 69 | +
|
| 70 | +# single tracer |
| 71 | +res = simulate_zircon_growth_from_tracers(Tracers[1]) |
| 72 | +
|
| 73 | +# also retrieve full SimulationResult objects |
| 74 | +age_years, zircon_radius_um, results = simulate_zircon_growth_from_tracers( |
| 75 | + Tracers; return_results = true) |
| 76 | +``` |
| 77 | +""" |
| 78 | +function MagmaThermoKinematics.simulate_zircon_growth_from_tracers(Tracers; |
| 79 | + params::Union{Nothing, ZirconGrowth.GrowthParams} = nothing, |
| 80 | + nx::Int = 100, |
| 81 | + elements::ZirconGrowth.ElementData = ZirconGrowth.default_element_data(), |
| 82 | + filename::Union{Nothing, AbstractString} = nothing, |
| 83 | + return_results::Bool = false) |
| 84 | + |
| 85 | + n = length(Tracers) |
| 86 | + age_years = Vector{Union{Nothing, Float64}}(nothing, n) |
| 87 | + zircon_radius_um = Vector{Union{Nothing, Float64}}(nothing, n) |
| 88 | + _results = return_results ? Vector{Union{Nothing, ZirconGrowth.SimulationResult}}(nothing, n) : nothing |
| 89 | + done = Threads.Atomic{Int}(0) |
| 90 | + interval = max(1, n ÷ 20) |
| 91 | + |
| 92 | + println("Simulating zircon growth for $n tracers on $(Threads.nthreads()) thread(s)...") |
| 93 | + Threads.@threads for i in eachindex(Tracers) |
| 94 | + tr = Tracers[i] |
| 95 | + isempty(tr.time_vec) && continue |
| 96 | + |
| 97 | + time_Myr = Float64.(tr.time_vec) # already in Myr |
| 98 | + T_C = Float64.(tr.T_vec) |
| 99 | + |
| 100 | + length(time_Myr) < 2 && continue |
| 101 | + |
| 102 | + p = isnothing(params) ? ZirconGrowth.GrowthParams(time_Myr, T_C; nx=nx) : params |
| 103 | + |
| 104 | + res = ZirconGrowth.simulate_from_cooling_path(time_Myr, T_C; |
| 105 | + params = p, elements = elements) |
| 106 | + |
| 107 | + age_years[i] = MagmaThermoKinematics.volume_averaged_age(res) |
| 108 | + zircon_radius_um[i] = res.zircon_radius_um[end] |
| 109 | + return_results && (_results[i] = res) |
| 110 | + |
| 111 | + k = Threads.atomic_add!(done, 1) + 1 |
| 112 | + k % interval == 0 && print("\r $k / $n done...") |
| 113 | + end |
| 114 | + |
| 115 | + age_years = Float64[v for v in age_years if !isnothing(v)] |
| 116 | + zircon_radius_um = Float64[v for v in zircon_radius_um if !isnothing(v)] |
| 117 | + println("\rDone: $(length(age_years)) / $n tracers simulated. ") |
| 118 | + |
| 119 | + if !isnothing(filename) |
| 120 | + JLD2.jldsave(filename; age_years, zircon_radius_um) |
| 121 | + println("Saved ZirconGrowth results to: $filename ($(length(age_years)) tracers)") |
| 122 | + end |
| 123 | + |
| 124 | + if return_results |
| 125 | + results = ZirconGrowth.SimulationResult[r for r in _results if !isnothing(r)] |
| 126 | + return (; age_years, zircon_radius_um, results) |
| 127 | + end |
| 128 | + |
| 129 | + return (; age_years, zircon_radius_um) |
| 130 | +end |
| 131 | + |
| 132 | +function MagmaThermoKinematics.simulate_zircon_growth_from_tracers(tr::MagmaThermoKinematics.Tracer; kwargs...) |
| 133 | + return MagmaThermoKinematics.simulate_zircon_growth_from_tracers((tr,); kwargs...) |
| 134 | +end |
| 135 | + |
| 136 | +""" |
| 137 | + simulate_zircon_growth_from_tracers(dirname::AbstractString; kwargs...) |
| 138 | +
|
| 139 | +Load tracers from `dirname/Tracers_SimParams.jld2` and call |
| 140 | +`simulate_zircon_growth_from_tracers(Tracers; kwargs...)`. |
| 141 | +""" |
| 142 | +function MagmaThermoKinematics.simulate_zircon_growth_from_tracers(dirname::AbstractString; |
| 143 | + filename::Union{Nothing, AbstractString} = joinpath(dirname, "ZirconGrowth.jld2"), |
| 144 | + kwargs...) |
| 145 | + |
| 146 | + Tracers = JLD2.load(joinpath(dirname, "Tracers_SimParams.jld2"), "Tracers") |
| 147 | + return MagmaThermoKinematics.simulate_zircon_growth_from_tracers(Tracers; |
| 148 | + filename = filename, kwargs...) |
| 149 | +end |
| 150 | + |
| 151 | +""" |
| 152 | + volume_averaged_age(result::SimulationResult) -> Float64 |
| 153 | +
|
| 154 | +Compute the volume-averaged crystallisation age (in years) of a single zircon crystal. |
| 155 | +
|
| 156 | +Each concentric shell crystallised at a different time. The shell between radii |
| 157 | +`r[i]` and `r[i+1]` has volume ∝ r[i+1]³ − r[i]³ and an age of |
| 158 | +`time_years[end] − time_years[i]` (measured back from the end of the simulation). |
| 159 | +Shells with zero or negative growth are excluded. |
| 160 | +
|
| 161 | +Returns the volume-weighted mean age in **years**. |
| 162 | +""" |
| 163 | +function MagmaThermoKinematics.volume_averaged_age(result::ZirconGrowth.SimulationResult) |
| 164 | + t = result.time_years |
| 165 | + r = result.zircon_radius_um |
| 166 | + |
| 167 | + age_sum = 0.0 |
| 168 | + vol_sum = 0.0 |
| 169 | + t_end = t[end] |
| 170 | + |
| 171 | + for i in 1:(length(r) - 1) |
| 172 | + dV = r[i+1]^3 - r[i]^3 # proportional to shell volume; 4π/3 cancels |
| 173 | + dV <= 0 && continue |
| 174 | + age_mid = t_end - 0.5*(t[i] + t[i+1]) # age of shell midpoint |
| 175 | + age_sum += age_mid * dV |
| 176 | + vol_sum += dV |
| 177 | + end |
| 178 | + |
| 179 | + return vol_sum > 0 ? age_sum / vol_sum : 0.0 |
| 180 | +end |
| 181 | + |
| 182 | +""" |
| 183 | + volume_averaged_age(results::AbstractVector) -> Vector{Float64} |
| 184 | +
|
| 185 | +Apply `volume_averaged_age` to each element of `results` and return a `Vector{Float64}`. |
| 186 | +""" |
| 187 | +function MagmaThermoKinematics.volume_averaged_age(results::AbstractVector) |
| 188 | + return [MagmaThermoKinematics.volume_averaged_age(r) for r in results] |
| 189 | +end |
| 190 | + |
| 191 | +end # module ZirconGrowthExt |
0 commit comments