Skip to main content

petropandas

Python testing codecov

A pandas-accessor library for processing electron microprobe (EMPA) mineral analyses — convert oxide wt% to APFU, compute structural formulas, estimate Fe³⁺/Fe²⁺, calculate end-members, validate stoichiometry, and produce publication-ready plots.

Install

uv sync

Optional dependencies

uv sync --extra lab    # Jupyter notebook workflows
uv sync --extra tests  # running the test suite
uv sync --extra docs   # building the documentation

Contributors should instead run uv sync --dev, which installs all of the above plus ruff and pre-commit.

On Linux, pandas' clipboard I/O (pandas.read_clipboard() / DataFrame.to_clipboard()) needs a system-level clipboard utility — install xclip or xsel via your system package manager (e.g. sudo apt install xclip).

Quick start

from petropandas import pd, Grt
from petropandas.data import minerals as df
# minerals is a DataFrame of example analyses

Filter to oxide columns:

df.oxides()

Re-order columns in standard petrological order (SiO₂ first, volatiles last):

df.oxides.sorted()

Select rows by searching for "Garnet" in column "Mineral":

g = df.oxides.select("Garnet", on="Mineral")

Compute element APFU with Fe³⁺/Fe²⁺ splitting for garnet (12 oxygens):

g.mineral.apfu(Grt)

Site-allocated structural formula:

g.mineral.site_allocations(Grt)

End-member proportions:

g.mineral.end_members(Grt)

Validate analysis quality against ideal stoichiometry:

g.mineral.check_stoichiometry(Grt)

Returns a DataFrame of 0–1 scores per criterion (1 = perfect fit).

Supported minerals

Mineral Instance O₂ Cations Fe method End-members
Garnet Grt 12 8 Droop Prp, Alm, Sps
Garnet (Fe³⁺) GrtFe3 12 8 Matrix inversion Prp, Alm, Sps, Grs, Adr, Uvr
Feldspar Fsp 8 5 — An, Ab, Or
Clinopyroxene Cpx 6 4 Droop Jd, Ae, Wo, En, Fs
Orthopyroxene Opx 6 4 Droop MgTs, Wo, En, Fs
Muscovite Ms 11 7 — Al-Cel, Fe-Al-Cel, Prl, Mrg, Pg, Ms, Trioct
Biotite Bt 11 7 — Phl, Ann, Eas, Sid, Dioct
Staurolite St 48 — — Fe-St, Mg-St, Zn-St, Mn-St
Chlorite Chl 14* — — Clin, Cham, Mg-Sud, Fe-Sud
Epidote Ep 12.5 8 FeO→Fe₂O₃ Czo, Ep, Pmn, Muk, Taw
Amphibole Amp 23 15 Schumacher Tr, Act, Ed, F-Ed, Prg, F-Prg, Tsch, Rct, Win, Glau, F-Glau, Rieb, Mg-Rieb
Titanite Ttn 5 3 FeO→Fe₂O₃ Ttn, Al-Ttn, Fe-Ttn, Mal, Other
Chloritoid Cld 12 8 Droop Cld, Mgcld, Mncld
Cordierite Crd 18 11 — H₂O-Crd, Mg-Crd, Fe-Crd, Mn-Crd
Ilmenite Ilm 3 2 Droop Ilm, Gk, Pph, Hem, Chr
Spinel Spl 4 3 Droop Spl, Herc, Chrm, Mtc, Gahn, Frank, Jac, Ulv, Spss

* Chlorite uses 28-charge normalization (n_oxygens=14 effective).

All 16 instances above are also registered in mdb (from petropandas import mdb), a small lookup registry:

from petropandas import mdb

list(mdb.all())  # every built-in Mineral instance
mdb.names  # ["Garnet", "GarnetFe3", "Feldspar", ...]
mdb.abbreviations  # ["Grt", "GrtFe3", "Fsp", ...]
mdb.by_name("garnet")  # Grt (case-insensitive)
mdb.by_abbreviation("GRT")  # Grt (case-insensitive)
repr(mdb)  # "Mineral database (16 minerals available)"

mdb only covers these 16 built-ins, not the hpxeos a-x phases below.

THERMOCALC activity-composition (a-x) solution models are available through the petropandas.hpxeos subpackage, covering three real THERMOCALC axfiles — hpxeos.metapelite, hpxeos.metabasite, hpxeos.igneous — each exposing ready-to-use TC_<abbreviation> instances (e.g. TC_g, TC_pl4tr) alongside their Phase classes:

from petropandas.hpxeos.metapelite import TC_g

df.mineral.apfu(TC_g)
df.mineral.site_allocations(TC_g)
df.mineral.end_members(TC_g)
df.mineral.check_stoichiometry(TC_g)

Phase subclasses with order-disorder variables (e.g. Biotite Q, Augite Qfm/Qal) accept an optional order_parameters dict, passed through df.mineral.end_members(mineral, order_parameters={...}).

API reference

OxidesAccessor, MolesAccessor, CationsAccessor, and BulkAccessor share a common base with methods, all operating on the accessor's data in whatever petro_units it's currently in (no forced conversion) and tagging the result with that same unit — df.mineral does not have these:

Method Description
df.<accessor>.mean(*, groupby=None, weights=None) Mean across rows; groupby is a column name, weights is a column name or an array-like of numbers (list/numpy.ndarray/pandas.Series, matched to rows by position)
df.<accessor>.sum(*, groupby=None) Sum across rows; groupby is a column name
df.<accessor>.reframe(columns) Exactly the given ordered columns; missing ones filled with 0.0
df.<accessor>.normalize(to=100.0) Normalise rows to sum to to
df.<accessor>.calc(new_col, expr) Add new_col computed from a pandas.eval()-style expression (backtick-quote special-character column names, e.g. "`Fe{2+}` + `Mg{2+}`")

OxidesAccessor (df.oxides)

Method Description
df.oxides() Return copy with only recognised oxide columns (wt%)
df.oxides.sorted() Return oxide wt% with columns in petrological order
df.oxides.split_valence(elem, method, n_oxy, ideal_cat) Split element into low/high charge oxides (wt%)
df.oxides.oxidize(o_excess) Split FeO into FeO/Fe₂O₃ by excess oxygen (THERMOCALC)
df.oxides.reduce() Merge Fe₂O₃ back into FeO equivalent
df.oxides.apatite_correction() Remove CaO bound in apatite, zero P₂O₅

MolesAccessor (df.moles)

Method Description
df.moles() Return oxide columns as molar proportions

CationsAccessor (df.cations)

Method Description
df.cations(n_oxygens=N) Atoms per formula unit (oxygen basis)
df.cations(n_cations=N) Atoms per formula unit (cation basis)
df.cations.total_charge() Total positive charge per row, summed over ion-named columns

All callable accessors auto-convert from the current petro_units attr. Chains work seamlessly: df.oxides().moles().oxides() roundtrips back to wt%. df.cations(n_oxygens=N).oxides() converts APFU back to oxide wt% (ratios preserved).

MineralAccessor (df.mineral)

Method Description
df.mineral.apfu(mineral) Element APFU with valence splits for a mineral; columns ordered by decreasing charge then increasing ionic radius (T → M → A/B/X)
df.mineral.site_allocations(mineral) Site allocations with hierarchical (site, cation) columns
df.mineral.end_members(mineral) End-member proportions (%)
df.mineral.check_stoichiometry(mineral) Stoichiometry validation scores (0–1)
df.mineral.stoichiometry_quality(mineral) Single 0–1 quality score: mean of cation_deviation, site_vacancies, leftover_cations

BulkAccessor (df.bulk)

Method Description
df.bulk() Return cleaned copy in wt%
df.bulk.cipw() Simple CIPW normative mineralogy
df.bulk.alumina_saturation(classify=False) A/NK and A/CNK molar ratios; optional Shand classification
df.bulk.oxide_ratios() Common ratios (Mg#, FeOT, total alkalis, K/Na, etc.)
df.bulk.fractionate(profile, fraction, *, mineral=None, order="core-to-rim") Subtract a fractionating mineral (volume-integrated from a radial EPMA profile) from the bulk via molar mass balance
df.bulk.TCbulk(*, system, ...) THERMOCALC bulk script output
df.bulk.Perplexbulk(*, system, ...) PerpleX thermodynamic component list output
df.bulk.MAGEMin(*, db, ...) MAGEMin bulk input file output

Series accessor (series.mineral)

Method Description
series.mineral.is_oxide True if column is a recognised oxide
series.mineral.element Element symbol
series.mineral.molecular_weight Molecular weight
series.mineral.to_mole() Convert one column to moles
series.mineral.to_cation(n_oxygens, total_oxygens) Convert one column to APFU

Plotting

Three plot classes are available for visualising microprobe and end-member data:

from petropandas import ScatterPlot, TernaryPlot, ProfilePlot

ScatterPlot

X/y scatter plot where axes are pandas.eval() expressions over DataFrame columns.

s = ScatterPlot("Prp", "Sps+Grs")
s.add(garnet_df, label="Garnet 1")
s.add(other_df, label="Garnet 2")
fig, ax = s.render()

TernaryPlot

Ternary diagram built on plain matplotlib (no mpltern dependency). Axes are pandas.eval() expressions.

s = TernaryPlot("Prp", "Sps", "Grs")
s.add(garnet_df, label="Garnet 1")
fig, ax = s.render()

ProfilePlot

Line plot of DataFrame columns against their index, with optional dual y-axes for columns spanning different value scales.

s = ProfilePlot(secondary_columns=["Alm"])
s.add(profile_df, label="Profile 1")
fig, ax = s.render()

All three inherit from BasePlot and share add(), render(), show(), and savefig() methods.

Configuration

Global defaults are stored in the ppconfig singleton (petropandas.PPConfig):

Setting Default Controls
default_system "MnNCKFMASHTO" Default thermodynamic system for TCbulk / Perplexbulk
default_oxygen 0.01 Default ferric oxygen for TCbulk / Perplexbulk / MAGEMin
default_H2O -1.0 Default water wt% (-1 = auto from deficit)
default_db "mp" Default MAGEMin database
default_sys_in "mol" Default MAGEMin unit system
from petropandas import ppconfig

ppconfig.default_db = "ig"
ppconfig.default_system = "KFMASH"
ppconfig.reset()  # restore all defaults

Mutations are global and persist for the session.

Stoichiometry checking

df.mineral.check_stoichiometry(mineral) returns a DataFrame scored 0–1 for each criterion:

Criterion What it checks
analytical_total Oxide wt% sum vs mineral-specific ideal range
cation_deviation Total APFU vs ideal cation count
charge_balance Total positive charge vs expected from oxygen count
fe3+_validity Fe³⁺ and Fe²⁺ non-negative after valence splitting
site_vacancies Mean site occupancy fraction
leftover_cations Fraction of APFU not assigned to any site
tetrahedral_fill T-site sum vs T-site capacity

Inapplicable criteria are dropped (all-NaN columns removed).

df.mineral.stoichiometry_quality(mineral) condenses three of those criteria — cation_deviation, site_vacancies, leftover_cations — into a single 0–1 score (their mean) per analysis, as a pandas.Series. NaN if the mineral doesn't define ideal_cations (so cation_deviation is NaN).

Fe³⁺/Fe²⁺ estimation methods

  • Droop (1987): method="droop" — charge-balance approach on total cations
  • Schumacher (1991): method="schumacher" — oxygen-excess approach
  • FeO→Fe₂O₃ conversion: Epidote and Titanite internally convert all FeO to Fe₂O₃ before normalisation
  • Spinel: Merges Fe₂O₃ into FeO before Droop split (inverse spinel model)
  • df.oxides.split_valence(): General-purpose valence splitting on any element
  • df.oxides.oxidize(o_excess): THERMOCALC excess-oxygen convention (Fe₂O₃ = 2 × o_excess)

Conventions

  • Ion column names use periodictable notation: Fe{2+}, Fe{3+}, Si{4+}, Na{+}
  • Structural formula columns: (site, cation) tuples (e.g. ("Z", "Si{4+}"), ("X", "Fe{2+}"))
  • Unit tracking via df.attrs["petro_units"]: "wt%" (default) → "moles" → "apfu"
  • Callable accessors (df.oxides(), df.moles(), df.cations()) auto-convert from current units
  • Mineral instances are configuration objects — stateless, reusable
  • str(mineral) returns mineral.abbreviation (e.g. "Grt", matching the instance variable it's exported under); repr(mineral) returns mineral.name (e.g. "Garnet")
  • Internal modules are underscore-prefixed (_calc.py, _core.py, _minerals.py, _plotting.py)

Metadata

Release files for petropandas 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for petropandas 0.2.2
File Size Uploaded
petropandas-0.2.2.tar.gz 170.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for petropandas 0.2.2
File Interpreter ABI Platform
petropandas-0.2.2-py3-none-any.whl Python 3 none any Details

Total release size: 343.7 kB

Release files / petropandas-0.2.2.tar.gz

Download URL petropandas-0.2.2.tar.gz
Size 170.4 kB
Tags Source
SHA-256 checksum
How to use checksums
5f01b2c7a9a0358aa4c396cc5f3317ff32fa4bc62586116cb8a74468f9f2a632
BLAKE2b-256 checksum
How to use checksums
71eb351748a0232994eed656d3f36780bfe2050e5747a7767088b29de7bafa35
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / petropandas-0.2.2-py3-none-any.whl

Download URL petropandas-0.2.2-py3-none-any.whl
Size 173.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2a8a8d81bc9d0fceab79ddaf544142870c9069f9dea903207ab621f28ad932d8
BLAKE2b-256 checksum
How to use checksums
b06495c939840aacae895a13bac43b9d42626a92625b93708b2a261eb2e85ea5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.2.3

2 release files

This release

0.2.2 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page