Skip to content

Tutorial: compute_solidus_FS.jl

What This Script Does — The Big Picture

At the exact moment the first melt forms, what is the mineral assemblage across all natural pelite compositions, and how does it vary with bulk chemistry?

This script is a solidus-snapshot diagnostic. Rather than tracking Li enrichment through multiple extraction events, it runs n_ee = 1 extraction event per sample — just enough to capture the equilibrium state at the first melt threshold — and then plots the resulting mineral volume fractions (quartz, plagioclase, alkali feldspar, biotite, muscovite, cordierite, garnet, ilmenite, sillimanite) against bulk composition in Herron chemical space.

The key output is a Herron diagram colored by the combined Q+A+P (quartz + alkali feldspar + plagioclase) volume fraction at the solidus — showing which pelite compositions are most felsic at the onset of melting.


File Structure

compute_solidus_FS.jl     ← main entry point
TE_functions.jl           ← norm2one, retrieve_solidus, get_Kds, get_data_thread
TE_functions_FS.jl        ← water_saturation_curve_FS, get_mol_bulks
TE_fractional.jl          ← threaded_frac_melting engine (referenced but logic inlined here)
plot_figures.jl           ← shared helpers
plot_figures_FS.jl        ← retrieve_outputs_FS and FS scatter plots
plot_bulk_FS.jl           ← plot_bulk_FS(), create_heatmap_data_scattered()  ← NEW

The only new file relative to compute_systematics_FS.jl is plot_bulk_FS.jl, which handles the Herron diagram and IDW-interpolated heatmap.


Key Difference from compute_systematics_FS.jl

compute_systematics_FS.jlcompute_solidus_FS.jl
n_ee10 — full melting history1 — solidus snapshot only
Ex_H2O_sat0.03 — slight over-saturation0.0 — exact saturation
Melting stored inLi_infos struct via threaded_frac_meltingext_out[i] directly, inline
Post-processingretrieve_outputs_FS()Custom inline loop, then plot_bulk_FS()
OutputLi enrichment vs. compositionMineral assemblage at solidus vs. composition

Setting n_ee = 1 and Ex_H2O_sat = 0.0 means: find the exact water-saturated solidus for each sample, run one equilibrium calculation there, and record the phase assemblage. No melt extraction actually happens; the loop runs once and exits.


Step 1 — Parameters

julia
model       = "MM"
P           = 8.0       # kbar — single fixed pressure
Ex_H2O_sat  = 0.0       # no excess water — exact saturation
n_ee        = 1         # one event only → solidus snapshot
e1_liq      = 7.0       # vol% melt threshold (used in bisection)
e1_remain   = 1.0       # retained melt (irrelevant for n_ee=1)

P = 8.0 kbar fixes the depth. All ~600 Forshaw pelite samples are run at this single pressure but each at its own saturation water content.


Step 2 — main() Function

File: compute_solidus_FS.jl, lines 39–265

Stage A — Load Database

julia
bulk_db  = load_Forshaw_mp()
FS_bulks = get_mol_bulks(bulk_db)    # np × 11 matrix, mol fractions
np       = size(FS_bulks, 1)

Identical to the FS script: Excel → DataFrame → wt% → mol fractions → normalized.

Stage B — Pre-allocate Output Flat Vectors

Unlike compute_systematics_FS.jl which packs everything into Li_infos structs, here the MAGEMin outputs are stored directly in flat vectors:

julia
ext_out    = Vector{MAGEMin_C.gmin_struct{Float64, Int64}}(undef, np)
ext_out_te = Vector{MAGEMin_C.out_tepm}(undef, np)

One MAGEMin output struct per sample — no wrapping in Li_infos. This is intentional: since only one extraction event is needed, the extra nesting of Li_infos is unnecessary overhead.

Stage C — Threaded Loop (Inline Melting Engine)

The melting logic that is normally delegated to threaded_frac_melting() is written inline inside the @threads loop here. This gives the developer direct access to intermediate variables without unpacking the Li_infos struct:

julia
@threads :static for i = 1:np
    data_thread = get_data_thread(data)
    bulk        = FS_bulks[i, :]

    # 1. Compute water saturation for this sample at P
    H = water_saturation_curve_FS(data_thread, P, bulk, Xoxides, dtb, Ex_H2O_sat)
    bulk[id_h] = H
    gv = define_bulk_rock(gv, bulk, Xoxides, sys_in, dtb)

    # 2. Initialize Li trace element budget
    C0 = adjust_chemical_system(KDs_database, [Li_content], ["Li"])

    # 3. Find the solidus temperature by bisection
    Tsol = retrieve_solidus(P, gv, z_b, DB, splx_data)

    # 4. Run MAGEMin at the solidus
    out    = point_wise_minimization(P, Tsol, ...)
    out_TE = TE_prediction(out, C0, KDs_database, dtb; ...)

    # 5. Bisect to find T at 7% melt (one event only)
    # ... bisection loop ...

    # 6. Save the result directly
    ext_out[i]    = out
    ext_out_te[i] = out_TE
end

The bisection inside the loop (steps 5–6) is the same algorithm as in threaded_frac_melting() — it narrows the temperature bracket until out.frac_M_vol ≈ e1_liq/100. Since n_ee = 1, the outer for ee = 1:n_ee loop executes exactly once and then exits.

Threading Structure

@threads :static for i = 1:np   (np ≈ 600 samples)

  ├── Thread k reads  FS_bulks[i, :]          → unique row
  ├── Thread k calls  water_saturation_curve_FS → H_ for this sample
  ├── Thread k calls  retrieve_solidus          → T_sol for this sample
  ├── Thread k runs   bisection loop            → T at 7% melt
  └── Thread k writes ext_out[i], ext_out_te[i] → unique indices

No shared mutable state — each thread operates on its own MAGEMin instance and writes to distinct array positions.

Return Values

julia
return ext_out, ext_out_te, np, KDs_database, FS_bulks

The flat ext_out[np] array is returned directly to the caller (no Li_infos wrapping). FS_bulks is also returned — its H₂O column has been updated with the per-sample saturation values during the loop.


Step 3 — Plotting: plot_bulk_FS()

File: plot_bulk_FS.jl, lines 36–337

Called at line 296:

julia
plot_bulk_FS(ext_out, P, np, FS_bulks, n_ee)

This function does two things: extracts mineral volumes from ext_out, then plots them on Herron diagrams.

Stage A — Extract Mineral Volumes

For each of 8 phases (q, pl, afs, sill, g, bi, mu, cd, ilm), the function loops over all samples and reads the volume fraction from ext_out[i].ph_frac_vol:

julia
q_vol = zeros(np)
for i = 1:np
    if isassigned(ext_out, i)          # guard against uninitialized entries
        if "q" in ext_out[i].ph
            id = findfirst(ext_out[i].ph .== "q")
            q_vol[i] = ext_out[i].ph_frac_vol[id]
        end                            # else stays 0.0
    end
end

The isassigned() check is essential — samples that failed to converge leave ext_out[i] uninitialized, and accessing them would crash. The same pattern is repeated for all 8 minerals.

Then the three felsic phases are combined:

julia
pl_vol = afs_vol .+ pl_vol .+ q_vol    # Q + A + P total felsic fraction

Stage B — Compute Herron Coordinates

The Herron classification uses log-ratio chemistry:

julia
# Convert mol fractions back to wt% for the Herron axes
mol_bulk = hcat([mol2wt(FS_bulks[i,:], Xoxides) for i in 1:n]...)

S = mol_bulk[1,:]   # SiO₂ wt%
A = mol_bulk[2,:]   # Al₂O₃ wt%
F = mol_bulk[5,:]   # FeO wt%
K = mol_bulk[6,:]   # K₂O wt%

x_vals = log10.(S ./ A)   # x-axis: silica maturity
y_vals = log10.(F ./ K)   # y-axis: iron enrichment

The Herron diagram divides sedimentary rocks into five fields using hardcoded boundary lines (t, t2, t3, t4, t5):

log10(FeO/K2O)

0.6 ├────────────────────────────── Fe Sand
    │         │
    │  Shale  │  Wacke  │ Sublit. │ Litharenite
    │         │         │         │
0.0 ├─────────┴─────────┴─────────┴──────────── log10(SiO2/Al2O3)
              0.55     0.71      1.04

Stage C — Scatter Plot (Q+A+P colored)

julia
scatter_plot = PlotlyJS.scatter(
    x = x_vals,
    y = y_vals,
    marker = attr(
        color      = pl_vol .* 100.0,      # Q+A+P vol% as color
        colorscale = "RdBu",
        cmin       = pl_vol_mean - pl_vol_std,
        cmax       = pl_vol_mean + pl_vol_std,
    ))

The color scale is centered on the mean Q+A+P fraction and spans ±1 standard deviation, so the diverging colormap highlights samples that are unusually felsic (red) or unusually poor in Q+A+P (blue) at the solidus.

Output: 1e_8.0_kbar_pl.png (filename encodes n_ee and P).

Stage D — IDW Interpolated Heatmap

Because the ~600 samples are scattered irregularly in Herron space (not on a regular grid), a simple heatmap cannot be made directly. Instead, create_heatmap_data_scattered() interpolates the scattered points onto a regular 64×64 grid using Inverse Distance Weighting (IDW):

julia
x_grid, y_grid, z_grid = create_heatmap_data_scattered(x_vals, y_vals, pl_vol.*100.0, 64)

How IDW Works

For each node (xi, yi) on the regular grid, the interpolated value is a weighted average of all data points, where the weight of each point decays with distance:

         Σ (z_k / d_k²)
z(xi,yi) = ──────────────
           Σ (1 / d_k²)

where d_k = distance from (xi,yi) to sample point k
Scattered samples          Regular 64×64 grid
(irregular spacing)        (uniform spacing)

   ·  ·                      + + + + + + +
      ·   ·       IDW →      + + + + + + +
  ·        ·                 + + + + + + +
     ·   ·                   + + + + + + +
  ·     ·                    + + + + + + +

The power parameter is 2 (distance squared in the denominator), which is the standard choice: it gives strong influence to nearby points while allowing distant points to contribute smoothly. A small epsilon (1e-10) prevents division by zero when a grid node coincides exactly with a data point.

The resulting z_grid[64, 64] is plotted as a PlotlyJS.heatmap() with the same Herron boundary lines overlaid.

Output: 1e_8.0_kbar_pl_heatmap.png.


Data Flow Summary

Setup

├── load_Forshaw_mp()           → DataFrame (~600 pelites)
├── get_mol_bulks()             → FS_bulks[np, 11] (mol fractions, H₂O = 0)
├── Initialize_MAGEMin()        → threaded MAGEMin data
├── ext_out[np]                 → flat output vector (one entry per sample)

└── @threads :static for i = 1:np
      ├── water_saturation_curve_FS(P, bulk[i])   → H_ at exact saturation
      ├── retrieve_solidus(P)                      → T_solidus by bisection
      ├── bisect T until frac_M_vol = 7%           → T_extraction
      ├── point_wise_minimization(P, T_extraction) → equilibrium assemblage
      └── ext_out[i] = out                         → store phase assemblage

Post-processing (in plot_bulk_FS)

├── extract q_vol, pl_vol, afs_vol, bi_vol, mu_vol, cd_vol, g_vol, ...
├── pl_vol = q_vol + afs_vol + pl_vol    (Q+A+P combined)
├── compute Herron axes: log10(SiO2/Al2O3), log10(FeO/K2O)

├── PlotlyJS scatter  → 1e_8.0_kbar_pl.png          (sample points)
└── IDW interpolation → 1e_8.0_kbar_pl_heatmap.png  (smooth field)

Connection to the Other Scripts

This script occupies a specific diagnostic role in the project:

QuestionScript
How does Li enrichment vary in P–H₂O space?compute_P-H2O_systematics.jl
Which bulk compositions produce the most Li-rich melt?compute_systematics_FS.jl
At which P–T conditions do successive melt pulses form?compute_PT_curves.jl
What mineral assemblage exists at the solidus across natural pelites?compute_solidus_FS.jl
How does the biotite KD vary with T and P?compute_bi_Li_profiles.jl

Running the Script

bash
julia --threads 8 compute_solidus_FS.jl

Outputs are saved in the working directory:

  • 1e_8.0_kbar_pl.png — Herron scatter, colored by Q+A+P vol%

  • 1e_8.0_kbar_pl_heatmap.png — IDW-smoothed version of the same field

To run at a different pressure, change P = 8.0 at line 275. The filename automatically encodes the pressure and n_ee, so multiple runs do not overwrite each other.