{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\" Created on August 22, 2025 // Updated on March 20, 2026 // @author: Sarah Shi \"\"\"\n", "\n", "import os\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "import mineralML as mm\n", "\n", "%matplotlib inline\n", "%config InlineBackend.figure_format = 'png'" ] }, { "cell_type": "markdown", "metadata": {}, "source": "# mineralML Stoichiometry Calculations\n\nThis notebook demonstrates how to use the **stoichiometry** calculators and classifiers defined in mineralML:\n1. Load and prepare data for analysis\n2. Calculate stoichiometry with `BaseMineralCalculator`, for moles, oxygens, cations on a fixed oxygen basis\n3. Apply specialized calculators for different mineral groups\n4. Perform empirical classifications consistent with petrologist-defined schemes\n\nWe loaded in the **mineralML** Python package as `mm`. **mineralML** has trained machine learning models for classifying minerals. This implementation aims to get your electron microprobe or quantitative EDS compositions classified and processed. We remove some degrees of freedom to simplify the process as much as possible. The minerals considered for this study include: Amphibole, Apatite, Biotite, Calcite, Chlorite, Epidote, Feldspar (Alkali Feldspar and Plagioclase), Garnet, Glass, Kalsilite, Leucite, Melilite, Muscovite, Nepheline, Olivine, Oxide (Rhombohedral_Oxides including Hematite-Ilmenite, Spinel_Group including Magnetite-Spinel), Pyroxene (Clinopyroxene, Orthopyroxene, Na-Pyroxene), Quartz, Rutile, Serpentine, Titanite, Tourmaline, and Zircon. \n\nOne CSV file containing your electron microprobe analyses in oxide weight percentages is necessary. Find an example [here](https://github.com/sarahshi/mineralML/blob/main/docs/examples/training_hundred.csv). The necessary oxides are SiO$_2$, TiO$_2$, Al$_2$O$_3$, FeO$_t$, MnO, MgO, CaO, Na$_2$O, K$_2$O, Cr$_2$O$_3$, P$_2$O$_5$, and ZrO$_2$ (if you are aiming to classify zircon). For the oxides not analyzed for specific minerals, the preprocessing will fill in the nan values as 0." }, { "cell_type": "markdown", "metadata": {}, "source": "# BaseMineralCalculator\n\n**`mm.BaseMineralCalculator`** forms the foundation for all stoichiometry workflows in `mineralML`. It provides the core methods to:\n\n- Normalize oxide compositions to a fixed oxygen basis\n- Convert weight% oxides to moles, oxygens, and cations\n- Enforce Fe input consistency (FeOt, Fe₂O₃t, or paired FeO/Fe₂O₃)\n\nIt returns results in a consistent format: \n- `_mols`\n- `_ox`\n- `_cat_{oxbasis}ox` — cations per n oxygens\n\nAll mineral-specific calculators (e.g., `AmphiboleCalculator`, `FeldsparCalculator`, `OlivineCalculator`, `PyroxeneCalculator`) inherit from this base. They extend it with additional logic for site allocation, normalization rules, or classification schemes. For most users, no direct interaction is needed with `BaseMineralCalculator`, but it is useful to know that every group-specific tool builds on this common calculator." }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Load and prepare data for analysis" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Read in your dataframe of mineral data, called training_hundred.csv. \n", "df_load = mm.load_df('TabularData/training_hundred.csv')\n", "display(df_load.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## AmphiboleCalculator and AmphiboleClassifier\n\n**`mm.AmphiboleCalculator`** and **`mm.AmphiboleClassifier`** perform basic calculations associated with amphibole-group minerals along with site allocations (:cite:t:`Leakeetal1997` and :cite:t:`Ridolfi2021` from `Thermobar` :cite:p:`Wieseretal2022`), and additionally plots these data in a classification diagram. `mineralML` adds on to the `Thermobar` classification by also classifying amphiboles on the :cite:t:`Leakeetal1997` calcic amphibole classification diagram and explicitly returning the corresponding `Submineral`, or the specific type of calcic amphibole." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the amphiboles from the training dataset\n", "amp = df_load[df_load.Mineral=='Amphibole']\n", "amp_calc = mm.AmphiboleCalculator(amp)\n", "amp_comp = amp_calc.calculate_components()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's print all the columns in this dataframe, along with the compositions returned.\n", "print(list(amp_comp.columns))\n", "display(amp_comp.head())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Now, let's classify and plot up these amphiboles. \n", "amp_class = mm.AmphiboleClassifier(amp)\n", "amp_comp_class = amp_class.classify()\n", "\n", "# Note the addition of the Submineral column providing a calcic amphibole classification.\n", "display(amp_comp_class.head())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Here, we can plot up these amphiboles. \n", "fig, ax = amp_class.plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## ApatiteCalculator\n\n**`mm.ApatiteCalculator`** performs basic calculations and site allocations associated with apatite-group minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ap = df_load[df_load.Mineral=='Apatite']\n", "ap_calc = mm.ApatiteCalculator(ap)\n", "ap_comp = ap_calc.calculate_components()\n", "display(ap_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## BiotiteCalculator\n\n**`mm.BiotiteCalculator`** performs basic calculations and site allocations associated with biotite-group minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bt = df_load[df_load.Mineral=='Biotite']\n", "bt_calc = mm.BiotiteCalculator(bt)\n", "bt_comp = bt_calc.calculate_components()\n", "display(bt_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## CalciteCalculator\n\n**`mm.CalciteCalculator`** performs basic calculations and site allocations associated with calcite minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cal = df_load[df_load.Mineral=='Carbonate']\n", "cal_calc = mm.CalciteCalculator(cal)\n", "cal_comp = cal_calc.calculate_components()\n", "display(cal_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## ChloriteCalculator\n\n**`mm.ChloriteCalculator`** performs basic calculations and site allocations associated with chlorite minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "chl = df_load[df_load.Mineral=='Chlorite']\n", "chl_calc = mm.ChloriteCalculator(chl)\n", "chl_comp = chl_calc.calculate_components()\n", "display(chl_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## ClinopyroxeneCalculator, OrthopyroxeneCalculator, PyroxeneClassifier\n\n**`mm.ClinopyroxeneCalculator`**, **`mm.OrthopyroxeneCalculator`**, and **`mm.PyroxeneClassifier`** perform basic calculations associated with pyroxene-group minerals along with site allocations and additionally plots these data in a ternary diagram. `mineralML` adds on to the `Thermobar` ternary diagram by also classifying pyroxenes on the :cite:t:`DHZ` pyroxene ternary diagram and explicitly returning the corresponding `Submineral`, or the specific type of pyroxene. \n\n`mm.PyroxeneClassifier` is recommended if you do not know what types of pyroxenes are present. The specific calculators can be used if the type of pyroxene is known." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the pyroxenes from the training dataset\n", "px = df_load[(df_load.Mineral=='Clinopyroxene') | (df_load.Mineral=='Orthopyroxene')]\n", "px_class = mm.PyroxeneClassifier(px)\n", "px_comp = px_class.calculate_components() # mm.PyroxeneClassifier wraps mm.ClinopyroxeneCalculator and mm.OrthopyroxeneCalculator into the calculation.\n", "display(px_comp.head())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Plot these pyroxenes! \n", "fig, ax = px_class.plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## EpidoteCalculator\n\n**`mm.EpidoteCalculator`** performs basic calculations and site allocations associated with epidote minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the epidotes from the training dataset\n", "ep = df_load[df_load.Mineral=='Epidote']\n", "ep_calc = mm.EpidoteCalculator(ep)\n", "ep_comp = ep_calc.calculate_components()\n", "display(ep_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## FeldsparCalculator, FeldsparClassifier\n\n**`mm.FeldsparCalculator`** and **`mm.FeldsparClassifier`** perform basic calculations associated with feldspar-group minerals along with site allocations and additionally plots these data in a ternary diagram. `mineralML` pulls the lines from the `Thermobar` ternary diagram, and further classifies data on the feldspar ternary diagram and explicitly returns the corresponding `Submineral`, or the specific type of feldspar." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the feldspars from the training dataset\n", "feld = df_load[(df_load.Mineral=='KFeldspar') | (df_load.Mineral=='Plagioclase')]\n", "feld_class = mm.FeldsparClassifier(feld) # mm.FeldsparClassifier wraps mm.FeldsparCalculator into the calculation.\n", "feld_comp = feld_class.calculate_components()\n", "display(feld_comp.head())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Plot these feldspars! \n", "fig, ax = feld_class.plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## GarnetCalculator\n\n**`mm.GarnetCalculator`** performs basic calculations and site allocations associated with garnet minerals. This includes the Droop calculation for determining the proportion of Fe³⁺ and Fe²⁺." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the garnets from the training dataset\n", "gt = df_load[df_load.Mineral=='Garnet']\n", "gt_calc = mm.GarnetCalculator(gt)\n", "gt_comp = gt_calc.calculate_components()\n", "display(gt_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## GlassCalculator, GlassClassifier\n\n**`mm.GlassCalculator`** performs basic calculations for Mg#s and **`mm.GlassClassifier`** works with these data in TAS space. `mineralML` pulls the lines from the `pyrolite` TAS diagram, and further classifies data on the TAS diagram to explicitly return the corresponding `TAS` classification." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the glasses from the training dataset\n", "gl = df_load[df_load.Mineral=='Glass']\n", "gl_class = mm.GlassClassifier(gl)\n", "gl_comp = gl_class.calculate_components()\n", "display(gl_comp.head())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Plot these glasses! \n", "fig, ax = gl_class.plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## KalsiliteCalculator\n\n**`mm.KalsiliteCalculator`** performs basic calculations and site allocations associated with kalsilite minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the kalsilite from the training dataset\n", "kal = df_load[df_load.Mineral=='Kalsilite']\n", "kal_calc = mm.KalsiliteCalculator(gt)\n", "kal_comp = kal_calc.calculate_components()\n", "display(kal_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## LeuciteCalculator\n\n**`mm.LeuciteCalculator`** performs basic calculations and site allocations associated with leucite minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the leucite from the training dataset\n", "lc = df_load[df_load.Mineral=='Leucite']\n", "lc_calc = mm.LeuciteCalculator(gt)\n", "lc_comp = lc_calc.calculate_components()\n", "display(lc_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## MeliliteCalculator\n\n**`mm.MeliliteCalculator`** performs basic calculations and site allocations associated with melilite minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the melilite from the training dataset\n", "ml = df_load[df_load.Mineral=='Melilite']\n", "ml_calc = mm.MeliliteCalculator(ml)\n", "ml_comp = ml_calc.calculate_components()\n", "display(ml_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## MuscoviteCalculator\n\n**`mm.MuscoviteCalculator`** performs basic calculations and site allocations associated with muscovite minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the muscovite from the training dataset\n", "ms = df_load[df_load.Mineral=='Muscovite']\n", "ms_calc = mm.MuscoviteCalculator(ms)\n", "ms_comp = ms_calc.calculate_components()\n", "display(ms_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## NephelineCalculator\n\n**`mm.NephelineCalculator`** performs basic calculations and site allocations associated with nepheline minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the nepheline from the training dataset\n", "ne = df_load[df_load.Mineral=='Nepheline']\n", "ne_calc = mm.NephelineCalculator(ne)\n", "ne_comp = ne_calc.calculate_components()\n", "display(ne_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## OlivineCalculator\n\n**`mm.OlivineCalculator`** performs basic calculations and site allocations associated with olivine minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the olivine from the training dataset\n", "ol = df_load[df_load.Mineral=='Olivine']\n", "ol_calc = mm.OlivineCalculator(ol)\n", "ol_comp = ol_calc.calculate_components()\n", "display(ol_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## RhombohedralOxideCalculator, SpinelCalculator, OxideClassifier\n\n**`mm.RhombohedralOxideCalculator`**, **`mm.SpinelCalculator`**, and **`mm.OxideClassifier`** perform basic calculations associated with oxide and spinel minerals along with site allocations and additionally plots these data in a ternary diagram. This includes the Droop calculation for determining the proportion of Fe³⁺ and Fe²⁺. `mineralML` classifies oxides on the :cite:t:`DHZ` Ti⁴⁺-R³⁺-R²⁺ ternary diagram and explicitly returns the corresponding `Submineral`, or the specific type of oxide, and `Subspinel`, if the mineral is a spinel.\n\n`mm.OxideClassifier` is recommended if you do not know what types of oxides are present. The specific calculators can be used if the type of oxide is known. If there are spinels detected, the data will also be plotted on a spinel classification diagram." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the oxides from the training dataset\n", "ox = df_load[(df_load.Mineral=='Hematite') | (df_load.Mineral=='Ilmenite') | (df_load.Mineral=='Spinel') | (df_load.Mineral=='Magnetite')]\n", "ox_class = mm.OxideClassifier(ox) \n", "ox_comp = ox_class.calculate_components() # mm.OxideClassifier wraps mm.RhombohedralOxideCalculator and mm.SpinelCalculator into the calculation.\n", "display(ox_comp.head())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Plot these oxides! \n", "fig, ax = ox_class.plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## QuartzCalculator\n\n**`mm.QuartzCalculator`** performs basic calculations and site allocations associated with quartz minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the quartz from the training dataset\n", "qz = df_load[df_load.Mineral=='SiO2_Polymorph']\n", "qz_calc = mm.QuartzCalculator(qz)\n", "qz_comp = qz_calc.calculate_components()\n", "display(qz_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## RutileCalculator\n\n**`mm.RutileCalculator`** performs basic calculations and site allocations associated with rutile minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the rutile from the training dataset\n", "rt = df_load[df_load.Mineral=='Rutile']\n", "rt_calc = mm.RutileCalculator(rt)\n", "rt_comp = rt_calc.calculate_components()\n", "display(rt_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## SerpentineCalculator\n\n**`mm.SerpentineCalculator`** performs basic calculations and site allocations associated with serpentine minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the serpentine from the training dataset\n", "srp = df_load[df_load.Mineral=='Serpentine']\n", "srp_calc = mm.SerpentineCalculator(srp)\n", "srp_comp = srp_calc.calculate_components()\n", "display(srp_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## TitaniteCalculator\n\n**`mm.TitaniteCalculator`** performs basic calculations and site allocations associated with titanite minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the titanite from the training dataset\n", "tit = df_load[df_load.Mineral=='Titanite']\n", "tit_calc = mm.TitaniteCalculator(tit)\n", "tit_comp = tit_calc.calculate_components()\n", "display(tit_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## TourmalineCalculator\n\n**`mm.TourmalineCalculator`** performs basic calculations and site allocations associated with tourmaline minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the tourmaline from the training dataset\n", "trm = df_load[df_load.Mineral=='Tourmaline']\n", "trm_calc = mm.TourmalineCalculator(trm)\n", "trm_comp = trm_calc.calculate_components()\n", "display(trm_comp.head())" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## ZirconCalculator\n\n**`mm.ZirconCalculator`** performs basic calculations and site allocations associated with zircon minerals." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Let's select just the zircon from the training dataset\n", "zr = df_load[df_load.Mineral=='Zircon']\n", "zr_calc = mm.ZirconCalculator(zr)\n", "zr_comp = zr_calc.calculate_components()\n", "display(zr_comp.head())" ] } ], "metadata": { "kernelspec": { "display_name": "science", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.14" } }, "nbformat": 4, "nbformat_minor": 2 }