This release is a pre-release and may not be stable for production use.
petroplots
Well log plots, crossplots and lithology swatches for matplotlib.
Contents
- What it is
- Install
- A first plot
- Tracks
- Shading
- Facies schemes and lithology swatches
- Crossplots and histograms
- Formation tops
- Small multiples
- Templates
- Loading data
- What it does not do
- Documentation
- Development
- Citing
- Licence
What it is
Plotting a well log in matplotlib takes roughly 150 lines, and they are much the
same every time. Invert the depth axis once on the shared axis. Call
tick_top() on every track. Build a ListedColormap and a BoundaryNorm so a
facies colour means the same thing in every well. Add proxy Patch handles,
because imshow produces no legend entries. Set gridspec_kw width ratios so a
narrow facies column sits beside wide curve tracks.
petroplots does that part. It takes a pandas DataFrame and returns a matplotlib
Figure, so what comes back is an ordinary figure that every matplotlib method
still works on.
Several of those 150 lines are easy to get subtly wrong, and the result is a figure that renders without error and reports something incorrect:
- A
FaciesSchemefixes its colours from the class set you declare, not from the data in front of it.imshowscales its norm to whatever array it is handed, so a well containing no limestone recolours its sandstone. - The depth axis is inverted once, on the shared axis, after every track has drawn. Two tracks each inverting it leaves it the right way up.
- Layer edges sit at sample midpoints, which avoids both a hairline at every contact and an overlap that hides thin beds.
- Crossover shading is computed after both curves are normalised onto their own display limits. Density and neutron are recorded on incompatible scales, so shading computed without that step marks the wrong intervals.
- Limits are applied after drawing. Matplotlib autoscales as artists are added, so a limit set mid-draw is overridden by the next artist on the same axes.
Install
pip install petroplots
Python 3.10 or newer. The core install pulls in matplotlib, numpy and pandas and nothing else. seaborn, lasio and PyYAML are optional and imported only when the feature that needs them is used.
pip install "petroplots[las]" # lasio, for pp.io.from_las()
pip install "petroplots[yaml]" # PyYAML, for templates
pip install "petroplots[docs]" # sphinx, to build the documentation
A first plot
Ask the plot for a track, then add curves to that track:
import petroplots as pp
df = pp.datasets.load_example()
plot = pp.LogPlot(df, top=2000, bottom=2100)
plot.add_depth(interval=10)
gr = plot.add_track()
gr.add_curve("GR", limits=(0, 150), units="API")
nd = plot.add_track()
nd.add_curve("RHOB", limits=(1.9, 2.9), units="g/cm3")
nd.add_curve("NPHI", limits=(0.45, -0.05), units="v/v")
nd.crossover()
plot.add_facies("FACIES", pp.LITHOLOGY_FGDC)
fig, axes = plot.render()
render() hands back the figure and its axes and stops there. Nothing calls
show() or savefig() for you, so the figure is yours to carry on with:
axes["GR"].axvline(75, ls="--", color="0.4") # by track label
axes[0].set_facecolor("#FAFAFA") # by position
axes.depth.set_ylim(2080, 2020) # the shared depth axis
Three ways to build one
The handle form above suits anything where the tracks are not known in advance,
because gr and nd stay editable after you make them. That makes it the form
to reach for when building from a loop, a config file or a user interface.
For a quick look at a frame you have just loaded there is a one-liner. A string becomes one track, a tuple becomes one track carrying several curves, and a column resolving to the facies family becomes a lithology column:
fig, axes = pp.logplot(df, ["GR", ("RHOB", "NPHI"), "FACIES"])
Each add_* method also builds a whole track in a single call and returns the
plot, so calls chain:
fig, axes = (
pp.LogPlot(df, top=2000, bottom=2100)
.add_curves("GR", limits=(0, 200))
.add_fill(["RHOB", "NPHI"],
limits={"RHOB": (1.9, 2.9), "NPHI": (0.45, -0.05)},
crossover=True)
.add_facies("FACIES", pp.LITHOLOGY_FGDC)
.render(figsize=(11, 9))
)
All three produce the same figure and serialise to the same template.
Tracks
| Method | Draws |
|---|---|
add_track() |
an empty track; add curves to the handle it returns |
add_curves() |
one or more curves in one call |
add_fill() |
curves plus shading, including a density-neutron crossover |
add_facies() |
a discrete class column, with or without ornament |
add_zones() |
named depth intervals, labelled in place |
add_flag() |
a boolean column as a presence strip |
add_depth() |
a depth scale as a column of its own |
Limits, scale and units come from the curve family when you do not give them, so
resistivity lands on a logarithmic axis and neutron runs right to left without
being told. Column names resolve through an alias table, which is why a template
written against GR finds a column called SGR.
Header text is fitted to the track it belongs to. On a narrow figure a track name shrinks and then stands upright, tick labels drop from three values to two before they shrink, and keys too wide for their own track move to the figure legend.
Shading
A fill has two sides. Each is either a curve on the track or a number, and you name them the way you see them on the plot:
gr.fill(baseline=0, cmap="YlOrBr") # graded, from zero
phi.fill("PHIE", "PHIT") # between two curves
sw.fill(left="SW", right=1.0) # a curve across to a value
Leave a side out and it falls back to the track's first curve, so
phi.fill(right="PHIE") shades from the curve already there. A number is read
against the limits of the curve on the other side, so left=0 on a (0, 200)
gamma ray track means zero API.
Between two curves the shading is computed in normalised space, which is what lets the two sit on different scales.
Facies schemes and lithology swatches
A FaciesScheme maps class names to colours, and optionally to ornament. It
declares its class set up front, so a class keeps its colour whether or not the
well in front of you contains it.
scheme = pp.FaciesScheme.from_lithologies(
["Shale", "Sandstone", "Chalk", "Limestone", "Dolomite"]
)
scheme = pp.FaciesScheme.from_lithologies(
["Shale", "Sandstone", "Sandstone/Shale"],
swatches={"Sandstone/Shale": pp.swatches.INTERBEDDED},
)
Two schemes ship with the same nine classes, so a figure can be switched between
them without touching the data. pp.LITHOLOGY is colour only and fast.
pp.LITHOLOGY_FGDC adds ornament, which keeps a facies column readable in
greyscale, where a lot of well reviews still happen.
Behind those are thirty parametric patterns and thirty-two named lithologies, modelled on the FGDC Digital Cartographic Standard for Geologic Map Symbolization. The geometry is computed rather than loaded from artwork, so the ornament stays vector in PDF and SVG output and scales with figure size instead of pixel density.
pp.swatches.available() # 30 patterns
pp.swatches.LITHOLOGY_SWATCHES # 32 named lithologies
Register your own with pp.swatches.register().
Crossplots and histograms
The same schemes and the same curve conventions carry across the other plot types, so a colour, a range and a formation mean one thing everywhere.
pp.crossplot(df, "NPHI", "RHOB", color="GR", color_steps=6)
pp.crossplot(df, "PHIT", "PERM", color="FACIES", scheme=pp.LITHOLOGY)
pp.histogram(df, "PHIT", color="FACIES", scheme=pp.LITHOLOGY)
Permeability lands on a logarithmic axis without being told, and a histogram of
it gets logarithmic bins. A gamma ray colour axis runs 0 to 150 in every figure,
so wells drawn side by side are comparable. color_steps cuts the colour map
into bands, which lets a reader assign a point to a class rather than only rank
it against its neighbours.
Formation tops
A tops list rarely arrives in the same shape twice, so read_tops takes a
mapping, a list of pairs, a frame, or a project export in CSV, Excel, JSON or
Parquet. Headers are matched loosely, CSV separators are sniffed, and well=
picks one well out of a field export.
df = pp.add_tops(df, "field_tops.csv", well="15/9-19 A")
The column comes back as an ordered categorical in depth order, so legends, facies tracks and panel grids come out in stratigraphic order. Depths outside the tops list, or inside a gap between two units, are left empty rather than filled from a neighbour.
The same tops drive a formation track on the log plot, where each name is written inside its band:
plot.add_zones()
Small multiples
by= splits a crossplot or a histogram into one panel per class:
pp.crossplot(df, "NPHI", "RHOB", by="FORMATION", color="GR")
Limits, colour ranges and bins are computed over the whole frame before the split. Panels that autoscaled independently would draw a tight cluster and a broad scatter at the same size on the page, which reverses the comparison the grid exists to make.
Templates
A template names curves, never data. Applied to a well that lacks one it degrades predictably instead of raising, so one definition runs over a field:
template = pp.load_template("triple_combo")
for name, well in wells.items():
fig, axes = template.apply(well).title(name).render()
Curves spelled differently are resolved through the alias table. Curves missing entirely have their track dropped. A crossover missing one of its two curves degrades to a plain curve rather than shading everything under the survivor.
Save one from a plot you have built with pp.save_template(plot, "mine.yaml").
Loading data
df = pp.io.from_las("15_9-19A.las") # needs petroplots[las]
df = pp.io.normalise(df, depth="DEPT") # any frame you already have
normalise moves a depth index into a column, replaces null sentinels such as
-999.25 with NaN, and checks that depth is monotonic. pp.io.coverage(df)
reports which curves are present and how complete each one is.
What it does not do
No file parsing, no petrophysical calculations, and no well or project data
model. lasio, welly and dlisio cover those, and pp.io.from_las() is a
convenience that delegates to lasio rather than a parser of its own.
Documentation
Versioned, so a page matches the release you have installed. /en/stable/ is
the latest release and /en/latest/ tracks main.
To build it yourself:
pip install "petroplots[docs]"
python -m sphinx -b html docs docs/_build/html
Development
git clone https://github.com/andymcdgeo/petroplots
cd petroplots
pip install -e ".[test,yaml,las,docs]"
python -m pytest -q # tests
python examples/gallery.py examples/figures # render every example
python -m sphinx -b html docs docs/_build/html -W # docs, warnings fatal
CI runs the tests on Python 3.10 to 3.14, builds the documentation with warnings treated as errors, and renders the gallery so a figure that raises is caught even where no test covers it.
CONTRIBUTING.md covers branching, versioning and how a release is cut.
Citing
If petroplots contributes to work you publish, please cite it. A CITATION.cff
file is included, so GitHub can generate APA and BibTeX through the Cite this
repository link.
@software{mcdonald_petroplots,
author = {McDonald, Andy},
title = {petroplots: well log plots and lithology swatches for matplotlib},
version = {0.1.0},
year = {2026},
url = {https://github.com/andymcdgeo/petroplots}
}
Licence
MIT.
The lithology ornament follows the FGDC Digital Cartographic Standard for Geologic Map Symbolization, published by the USGS. As a US Government work it is in the public domain, and the geometry here is reimplemented parametrically rather than copied from any artwork file. The colours are this library's own: the FGDC 600-series patterns are monochrome and specify no fills.
The British Geological Survey ornament sets were considered and deliberately not used, since BGS sells its map symbol products and its Open Government Licence position covers data rather than cartographic symbols.
Bundled example data is synthetic and represents no real well.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file petroplots-0.1.0rc1.tar.gz.
File metadata
- Download URL: petroplots-0.1.0rc1.tar.gz
- Upload date:
- Size: 2.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f15cf9ebc4bcdf093e3f0d41506938250642f7475cc65c638c2ec7d7ff75f471
|
|
| MD5 |
f93efb91bdc069aebda8f14c81da6cad
|
|
| BLAKE2b-256 |
f825920d3e4c4445be0994978e614901eab60cbac564fa3c44fee9d46a959205
|
Provenance
The following attestation bundles were made for petroplots-0.1.0rc1.tar.gz:
Publisher:
publish.yml on andymcdgeo/petroplots
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
petroplots-0.1.0rc1.tar.gz -
Subject digest:
f15cf9ebc4bcdf093e3f0d41506938250642f7475cc65c638c2ec7d7ff75f471 - Sigstore transparency entry: 2410788480
- Sigstore integration time:
-
Permalink:
andymcdgeo/petroplots@e84bc5324e5e97a77c75ecb29e61dedf15f46618 -
Branch / Tag:
refs/tags/v0.1.0rc1 - Owner: https://github.com/andymcdgeo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e84bc5324e5e97a77c75ecb29e61dedf15f46618 -
Trigger Event:
release
-
Statement type:
File details
Details for the file petroplots-0.1.0rc1-py3-none-any.whl.
File metadata
- Download URL: petroplots-0.1.0rc1-py3-none-any.whl
- Upload date:
- Size: 122.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7ca2c878eca3ae7caec34f97e89de67f64afa743a3bf7f6c1908cf69e349589e
|
|
| MD5 |
702afe2111a2938bf5f731f8644998ab
|
|
| BLAKE2b-256 |
cd5163523a0f3abedb972ad5b431786365622741a4c2ce6ade7b01064e575030
|
Provenance
The following attestation bundles were made for petroplots-0.1.0rc1-py3-none-any.whl:
Publisher:
publish.yml on andymcdgeo/petroplots
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
petroplots-0.1.0rc1-py3-none-any.whl -
Subject digest:
7ca2c878eca3ae7caec34f97e89de67f64afa743a3bf7f6c1908cf69e349589e - Sigstore transparency entry: 2410788651
- Sigstore integration time:
-
Permalink:
andymcdgeo/petroplots@e84bc5324e5e97a77c75ecb29e61dedf15f46618 -
Branch / Tag:
refs/tags/v0.1.0rc1 - Owner: https://github.com/andymcdgeo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e84bc5324e5e97a77c75ecb29e61dedf15f46618 -
Trigger Event:
release
-
Statement type: