petropandas
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 elementdf.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 Mineralinstances are configuration objects — stateless, reusablestr(mineral)returnsmineral.abbreviation(e.g."Grt", matching the instance variable it's exported under);repr(mineral)returnsmineral.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)
| File | Size | Uploaded | |
|---|---|---|---|
| petropandas-0.2.2.tar.gz | 170.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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}
|