MELTS: Calculate Olivine Control Line#

Extrapolates a melt composition to a more primitive composition under the assumption that only olivine has crystallised from the melt. This calculation might be used for estimating primary melt compositions, or the effect of post-entrapment crystallisation on olivine-hosted melt inclusions.

The calculation uses the rhyoliteMELTS v1.2 model.

Open this code in an executable MyBinder instance (MyBinder links may be slow to load– please be patient!):

https://mybinder.org/badge_logo.svg

Initialization#

Import the necessary packages

from thermoengine import model, magmaforge, rockychem, redox
from thermoengine.core import chem
import pandas as pd
import numpy as np
from scipy.optimize import root_scalar
import matplotlib.pyplot as plt

Retrieve the MELTS v1.2 database and extract the olivine and liquid models.

db = model.Database("MELTS_v1_2")
liq = db.get_phase('Liq')
olv = db.get_phase('Ol')

Set up the Calculations#

This section sets up the algorithm for incrementally finding the equilibrium olivine composition at the liquidus, adding a small amount of it into the melt, and repeating.

You must run the cells below, but you do not need to change anything inside them to run a calculation.

def calculate_olivine_control_line(
        starting_comp : pd.Series,
        P_bar : float,
        target_Mgn : float,
        step_size : float = 0.005,
        database : model.Database = model.Database("MELTS_v1_2"),
        ) -> tuple[pd.DataFrame,
                   pd.DataFrame]:
    """
    Calculate an olivine control line, starting at the start composition and
    ending when the equilibrium olivine has the target Mgn. The Fe3/tFe ratio
    must be set in the starting_comp.

    Inputs
    ------
    starting_comp : pd.Series
        The starting composition in weight percent (or grams) of oxides
    P_bar : float
        The pressure in bars
    target_Mgn : float
        The olivine Mgn at which to terminate the calculation
    step_size : float
        The moles of olivine to add to the liquid composition at each step

    Outputs
    -------
    pd.DataFrame
        The melt compositions at each step on the olivine control line
    pd.DataFrame
        The olivine compositions at each step on the olivine control line
    """

    if 'Fe2O3' not in starting_comp or starting_comp['Fe2O3'] == 0.0:
        print("Warning: Fe2O3 set to 0.0")
    if 'FeO' not in starting_comp or starting_comp['FeO'] == 0.0:
        print("Warning: FeO is set to 0.0")

    if target_Mgn > 1 and target_Mgn < 100:
        target_Mgn = target_Mgn / 100
    elif target_Mgn > 100 or target_Mgn < 0:
        assert False, "Bad target Mg number."

    start_comp_arr = np.zeros(len(chem.OXIDE_ORDER))
    for i, ox in zip(range(len(chem.OXIDE_ORDER)), chem.OXIDE_ORDER):
        if ox in starting_comp:
            start_comp_arr[i] = starting_comp[ox]
        else:
            start_comp_arr[i] = 0.0
    start_comp_arr = start_comp_arr / np.sum(start_comp_arr) * 100

    liq_moles = magmaforge.System._convert_wtoxides_to_liquid_endmems(
        starting_comp, liq, oxides=starting_comp.index)

    olv_omni_comps = _calc_omni_comps_for_phases(olv, liq)


    T, olv_mols = find_liquidus_olivine(P_bar, liq_moles, olv_omni_comps)
    Mgn = olv_mols[5]/(olv_mols[1]+olv_mols[5])

    MAX_ITER = 1000
    iternum = 0

    check_phase_sat(T, P_bar, starting_comp)

    # Initialise dataframes for results
    liq_comps = np.zeros([MAX_ITER, len(chem.OXIDE_ORDER)])
    olv_comps = np.zeros([MAX_ITER, olv.endmember_num+1])
    liq_comps[0,:] = start_comp_arr
    olv_comps[0,:-1] = olv_mols
    olv_comps[0,-1] = Mgn
    T_seq = [T]

    while Mgn < target_Mgn and iternum < MAX_ITER:
        liq_moles += step_size * olv_omni_comps.T.dot(olv_mols)

        T, olv_mols = find_liquidus_olivine(P_bar, liq_moles, olv_omni_comps, T0=T)
        Mgn = olv_mols[5]/(olv_mols[1]+olv_mols[5])

        liq_mol_ox = liq.endmember_mol_oxide_comps.T.dot(liq_moles)
        liq_wtpt = chem.mol_to_wt_oxide(liq_mol_ox, chem.OXIDE_ORDER)
        liq_wtpt = liq_wtpt/np.sum(liq_wtpt) * 100
        liq_comps[iternum+1,:] = liq_wtpt


        olv_comps[iternum+1,:-1] = olv_mols
        olv_comps[iternum+1, -1] = Mgn
        T_seq.append(T)
        iternum += 1

    liq_comps = pd.DataFrame(liq_comps[:iternum+1,:], columns=chem.OXIDE_ORDER, index=T_seq)
    olv_comps = pd.DataFrame(olv_comps[:iternum+1,:],
                             columns=list(olv.endmember_names)+['Mg#'],
                             index=T_seq)

    return liq_comps, olv_comps


def find_liquidus_olivine(P, liq_moles, olv_omni_comps, T0=1500.0):
    res = root_scalar(_affncomp_root, x0=T0, args=(P, liq_moles, olv_omni_comps))
    assert res.converged, "Something has gone wrong when finding the temperature, check inputs."
    T = res.root
    affn, comp = get_olv_affncomp_from_liq(T, P, liq_moles, olv_omni_comps)
    return T, comp


def _affncomp_root(T, P, liq_moles, omni_comps):
    return get_olv_affncomp_from_liq(T, P, liq_moles, omni_comps)[0]

def get_olv_affncomp_from_liq(T, P, liq_moles, omni_comps):
    liq_mu = liq.chem_potential(T, P, mol=liq_moles)
    olv_mu = omni_comps.dot(liq_mu)
    affn, comp = olv.affinity_and_comp(T, P, olv_mu)
    return (affn, comp)

def _calc_omni_comps_for_phases(phase, omni_phase):
    """
    Calculates the matrix for conversion of phase compositions into
    omni_component endmembers.
    """

    omni_element_comps = omni_phase.endmember_element_comps
    inv_omni_element_comps = np.linalg.pinv(omni_element_comps)

    TOL = 1e-12

    phs_element_comps = phase.endmember_element_comps
    phs_omni_comps = phs_element_comps.dot(inv_omni_element_comps)

    phs_omni_comps[np.abs(phs_omni_comps) <= TOL] = 0

    return phs_omni_comps

def check_phase_sat(T, P, oxide_wtpt):
    sys = magmaforge.System(oxide_wtpt, T_K=T, P_bar=P, database='MELTS_v1_2')
    phs_mass = sys._system_calculator._total_mass_of_every_phase

    for phs in phs_mass:
        if phs not in ['Liquid', 'Olivine'] and phs_mass[phs] > 0.0:
            print(f"Warning: {phs} is saturated: {phs_mass[phs]:.2f} wt% present at olvine-saturation temperature.")

User Inputs#

Provide the starting composition of the magma here. You must have already determined the amount of FeO and Fe2O3 in the magma.

oxide_comp = pd.Series({
                        'SiO2':  48.32,
                        'TiO2':   0.93,
                        'Al2O3': 15.99,
                        'Fe2O3':  0.67,
                        'FeO':    8.36,
                        'MnO':    0.16,
                        'MgO':   10.56,
                        'CaO':   14.01,
                        'Na2O':   1.72,
                        'K2O':    0.05,
                        'P2O5':   0.03,
                        'H2O':    0.3,
                        'CO2':    0.05
                        })
# Composition of olivine-saturated glass from Kistufell, Iceland (Breddam, 2002)

Provide the pressure of crystallisation and the target Mg# for the equilibrium olivine:

pressure_bar = 1000.0
target_Mgn = 90.0

Run the calculation:

liq_comp, olv_comp = calculate_olivine_control_line(
                                            oxide_comp,
                                            P_bar=1000.0,
                                            target_Mgn=90.0
                                            )

Results#

We can see the results as tables of the liquid and olivine compositions along the extrapolated liquid line of descent:

liq_comp
SiO2 TiO2 Al2O3 Fe2O3 Cr2O3 FeO MnO MgO NiO CoO CaO Na2O K2O P2O5 H2O CO2
1499.443504 47.770638 0.919427 15.808206 0.662383 0.0 8.264953 0.158181 10.439941 0.0 0.0 13.850717 1.700445 0.049432 0.029659 0.296589 0.049432
1506.843444 47.714975 0.912681 15.692221 0.657523 0.0 8.298730 0.158723 10.702024 0.0 0.0 13.753162 1.687969 0.049069 0.029441 0.294413 0.049069
1514.052214 47.660542 0.906041 15.578063 0.652739 0.0 8.330148 0.159229 10.961531 0.0 0.0 13.657094 1.675689 0.048712 0.029227 0.292271 0.048712
1521.078311 47.607292 0.899505 15.465683 0.648030 0.0 8.359348 0.159701 11.218464 0.0 0.0 13.562476 1.663601 0.048360 0.029016 0.290163 0.048360
1527.929701 47.555178 0.893069 15.355034 0.643394 0.0 8.386461 0.160139 11.472831 0.0 0.0 13.469269 1.651698 0.048014 0.028809 0.288087 0.048014
1534.613862 47.504160 0.886732 15.246070 0.638828 0.0 8.411609 0.160548 11.724643 0.0 0.0 13.377439 1.639977 0.047674 0.028604 0.286043 0.047674
1541.137830 47.454196 0.880490 15.138749 0.634332 0.0 8.434903 0.160927 11.973911 0.0 0.0 13.286951 1.628433 0.047338 0.028403 0.284029 0.047338
1547.508210 47.405250 0.874341 15.033029 0.629902 0.0 8.456448 0.161278 12.220653 0.0 0.0 13.197772 1.617061 0.047008 0.028205 0.282046 0.047008
1553.731227 47.357285 0.868283 14.928871 0.625537 0.0 8.476341 0.161603 12.464886 0.0 0.0 13.109872 1.605857 0.046682 0.028009 0.280091 0.046682
1559.812738 47.310269 0.862314 14.826237 0.621237 0.0 8.494671 0.161903 12.706628 0.0 0.0 13.023219 1.594817 0.046361 0.027817 0.278166 0.046361
1565.758290 47.264169 0.856431 14.725090 0.616999 0.0 8.511523 0.162180 12.945901 0.0 0.0 12.937785 1.583937 0.046045 0.027627 0.276268 0.046045
1571.573099 47.218956 0.850633 14.625395 0.612821 0.0 8.526975 0.162435 13.182727 0.0 0.0 12.853541 1.573213 0.045733 0.027440 0.274398 0.045733
1577.262098 47.174600 0.844917 14.527118 0.608703 0.0 8.541102 0.162668 13.417129 0.0 0.0 12.770461 1.562642 0.045426 0.027255 0.272554 0.045426
1582.829955 47.131076 0.839281 14.430225 0.604644 0.0 8.553970 0.162880 13.649131 0.0 0.0 12.688519 1.552219 0.045123 0.027074 0.270736 0.045123
1588.281088 47.088356 0.833725 14.334686 0.600640 0.0 8.565646 0.163073 13.878757 0.0 0.0 12.607688 1.541942 0.044824 0.026894 0.268943 0.044824
1593.619682 47.046416 0.828245 14.240470 0.596693 0.0 8.576188 0.163248 14.106034 0.0 0.0 12.527945 1.531808 0.044529 0.026718 0.267176 0.044529
1598.849698 47.005234 0.822840 14.147547 0.592799 0.0 8.585655 0.163406 14.330987 0.0 0.0 12.449266 1.521812 0.044239 0.026543 0.265432 0.044239
1603.974914 46.964785 0.817510 14.055889 0.588958 0.0 8.594099 0.163546 14.553642 0.0 0.0 12.371629 1.511953 0.043952 0.026371 0.263713 0.043952
1608.998902 46.925050 0.812251 13.965468 0.585170 0.0 8.601571 0.163671 14.774027 0.0 0.0 12.295010 1.502227 0.043669 0.026202 0.262016 0.043669
1613.925060 46.886007 0.807062 13.876258 0.581432 0.0 8.608117 0.163780 14.992167 0.0 0.0 12.219389 1.492631 0.043390 0.026034 0.260343 0.043390


olv_comp
Tephroite Fayalite Coolivine Niolivine Monticellite Forsterite Mg#
1499.443504 0.002446 0.133909 0.0 0.0 0.014781 0.848864 0.863744
1506.843444 0.002403 0.131136 0.0 0.0 0.014584 0.851877 0.866598
1514.052214 0.002361 0.128498 0.0 0.0 0.014396 0.854745 0.869312
1521.078311 0.002322 0.125984 0.0 0.0 0.014210 0.857484 0.871899
1527.929701 0.002284 0.123585 0.0 0.0 0.014032 0.860100 0.874365
1534.613862 0.002247 0.121293 0.0 0.0 0.013859 0.862601 0.876721
1541.137830 0.002212 0.119102 0.0 0.0 0.013691 0.864995 0.878974
1547.508210 0.002178 0.117003 0.0 0.0 0.013528 0.867291 0.881130
1553.731227 0.002146 0.114991 0.0 0.0 0.013370 0.869494 0.883197
1559.812738 0.002114 0.113061 0.0 0.0 0.013216 0.871610 0.885179
1565.758290 0.002084 0.111206 0.0 0.0 0.013066 0.873644 0.887083
1571.573099 0.002055 0.109423 0.0 0.0 0.012920 0.875602 0.888913
1577.262098 0.002027 0.107708 0.0 0.0 0.012777 0.877488 0.890674
1582.829955 0.002000 0.106056 0.0 0.0 0.012639 0.879306 0.892369
1588.281088 0.001973 0.104463 0.0 0.0 0.012504 0.881060 0.894002
1593.619682 0.001948 0.102927 0.0 0.0 0.012372 0.882753 0.895578
1598.849698 0.001923 0.101444 0.0 0.0 0.012243 0.884390 0.897098
1603.974914 0.001899 0.100011 0.0 0.0 0.012117 0.885972 0.898567
1608.998902 0.001876 0.098626 0.0 0.0 0.011994 0.887504 0.899986
1613.925060 0.001853 0.097287 0.0 0.0 0.011874 0.888986 0.901359


We can save the tables (which can be downloaded if you are running this in a binder):

liq_comp.to_csv('liquid_compositions.csv')
olv_comp.to_csv('olivine_compositions.csv')

But we can also plot the results:

fig, ax = plt.subplots(3,1, figsize=(3.75, 7))

ax[0].plot(liq_comp['MgO'], liq_comp['FeO'], marker='s', markersize=3)
ax[1].plot(liq_comp['MgO'], olv_comp['Mg#']*100, marker='s', markersize=3)
ax[2].plot(liq_comp['MgO'], liq_comp.index - 273.15, marker='s', markersize=3)

for a in ax:
    a.set_xlabel('MgO (wt%)')

ax[0].set_ylabel('FeO (wt%)')
ax[1].set_ylabel('Fo (mol%)')
ax[2].set_ylabel('Temperature (°C)')

fig.tight_layout()

plt.show()
plot olivine control line

Total running time of the script: (0 minutes 0.640 seconds)

Gallery generated by Sphinx-Gallery