wellplot is an open-source Python toolkit for building printable and interactive well-log layouts from LAS and DLIS data.
The project is intentionally renderer-first:
- normalize subsurface data into typed channels
- describe the sheet using templates and layout specs
- render the same document to static and interactive backends
This repository currently contains the current MVP baseline:
- normalized data objects for scalar, array, and raster channels
- a printable log document model with tracks, styles, headers, and footers
- YAML template loading
- a physical page layout engine
- optional
matplotlibandplotlyrenderer backends - optional LAS and DLIS ingestion adapters
- optional experimental MCP server support for local logfile validation, previews, starter example export, and canonical YAML save workflows
- DLIS VDL/WF1-style array support with derived micro-time sample axes
- printable VDL density and waveform array rendering
- scale-aware curve fills, including crossover, limit, and baseline modes
- track-header fill indicators that mirror the actual plotted fill behavior
- in-track curve callouts with section-relative repetition and collision avoidance
- reference-track scalar overlay modes (
curve,indicator,ticks) - reference-track event objects for local markers such as casing foot or readings start
- annotation tracks with typed
interval,text,marker,arrow, andglyphobjects - dedicated annotation label lanes for dense tracks
The package separates three layers:
WellDataset: data and metadata normalized from LAS/DLIS inputsLogDocument: page, depth, track, and annotation specifications- renderers: backend-specific drawing implementations that consume the same document
flowchart LR
subgraph Inputs
LAS[LAS / DLIS files]
PD[pandas / numpy results]
YAML[YAML templates / savefiles]
end
subgraph DataLayer[Data layer]
DS[WellDataset]
OPS[alignment / merge / validation]
end
subgraph Compose[Composition layer]
BLD[LogBuilder]
DOC[ProgrammaticLogSpec / LogDocument]
end
subgraph Render[Render layer]
FULL[render_report]
PART[render_section / render_track / render_window]
BYTES[render_png_bytes / render_svg_bytes]
end
subgraph Outputs
PDF[PDF report]
IMG[PNG / SVG / notebook image]
SAVE[report/document YAML]
end
LAS --> DS
PD --> DS
DS --> OPS
OPS --> DS
YAML --> DOC
DS --> BLD
BLD --> DOC
DOC --> FULL
DOC --> PART
DOC --> BYTES
DS --> FULL
DS --> PART
DS --> BYTES
FULL --> PDF
PART --> PDF
BYTES --> IMG
DOC --> SAVE
The intended workflow is:
- ingest or compute channels into
WellDataset - validate, align, and merge the dataset as needed
- compose the layout with YAML or
LogBuilder - render a full report or a partial view
- optionally serialize the layout back to YAML
The public interaction layers now sit on top of that same render pipeline:
- YAML templates and savefiles for declarative jobs
- the Python surfaces exposed through
wellplotandwellplot.api - the experimental stdio MCP server exposed through
wellplot[mcp]
Track types are explicit: reference, normal, array, and annotation
(with compatibility aliases depth, curve, image).
Array tracks can host raster data and scalar overlays, while normal/reference tracks do not accept raster elements.
Reference tracks can host scalar overlay curves and local reference events while still defining the
layout axis.
Annotation tracks host lane-local interval, text, marker, arrow, and glyph objects instead of
channel bindings, and reuse the standard per-track grid configuration when you want the
background grid on or off.
Set page.continuous: true in templates to render a single continuous-depth PDF page.
Set page.track_header_height_mm to reserve a dedicated per-track header band.
Track headers now support explicit object slots (title, scale, legend) with enabled,
reserve_space, and line_units controls to prevent overlap while keeping fixed spacing.
Depth grid density in continuous mode is controlled by depth.major_step and depth.minor_step.
Use top-level markers and zones sections to draw formation and event annotations.
Most users should install the published package from PyPI:
python -m pip install wellplotInstall optional adapters and notebook support as needed:
python -m pip install "wellplot[las]"
python -m pip install "wellplot[dlis]"
python -m pip install "wellplot[pandas]"
python -m pip install "wellplot[interactive]"
python -m pip install "wellplot[notebook]"
python -m pip install "wellplot[mcp]"Common example-workflow installs:
python -m pip install "wellplot[las,notebook]"
python -m pip install "wellplot[dlis,notebook]"
python -m pip install "wellplot[las,dlis,pandas,notebook,interactive,units]"The optional MCP surface is experimental and is exposed only through the optional dependency extra and the stdio entry point.
Install it with:
python -m pip install "wellplot[mcp]"The server root is fixed to the current working directory when wellplot-mcp
starts. Logfile paths, referenced source data, exported example bundles, saved
YAML files, and explicit render outputs must all resolve inside that root.
Typical client registration:
{
"mcpServers": {
"wellplot": {
"command": "wellplot-mcp",
"cwd": "/absolute/path/to/job-root"
}
}
}The MCP tool surface currently supports:
validate_logfileandinspect_logfilepreview_logfile_pngplus explicit section/track/window preview toolsrender_logfile_to_fileexport_example_bundlevalidate_logfile_text,format_logfile_text, andsave_logfile_text
Packaged example resources and guided prompts are also exposed for MCP-aware clients.
This project uses uv for environment management and dependency resolution.
The package continues to support Python >=3.11.
CI validates the project on Python 3.11, 3.12, 3.13, and 3.14.
Create or update the environment:
uv syncInstall with optional LAS ingestion and PDF output:
uv sync --extra las --extra pdf --extra unitsInstall with optional pandas dataset adapters:
uv sync --extra pandasWith all optional backends:
uv sync --all-extrasRun tests:
uv run python -m unittest discover -s tests -v
uv run --with mcp pytest tests/test_mcp_service.py tests/test_mcp_server.pyBuild and smoke-test the wheel:
uv build
python -m venv .smoke-venv
./.smoke-venv/bin/pip install --upgrade pip
./.smoke-venv/bin/pip install dist/*.whl
./.smoke-venv/bin/wellplot --help
MPLBACKEND=Agg ./.smoke-venv/bin/python scripts/smoke_installed_wheel.py
python -m venv .smoke-venv-mcp
./.smoke-venv-mcp/bin/pip install --upgrade pip
./.smoke-venv-mcp/bin/pip install dist/*.whl "mcp>=1,<2"
MPLBACKEND=Agg ./.smoke-venv-mcp/bin/python scripts/smoke_installed_wheel.pyFormat and lint:
uv run ruff format .
uv run ruff check .The public programmatic API supports dataset construction, layout composition, rendering, partial renders, notebook bytes, and YAML serialization.
Implemented surfaces:
- dataset ingestion into
WellDataset - pandas
Series/DataFrameadapters on top of the dataset ingestion API - dataset alignment and normalization helpers:
sort_index(...)convert_index_unit(...)reindex_to(...)
- dataset update/merge helpers:
rename_channel(...)merge(..., collision=\"error|replace|rename|skip\")- merge history recorded in
dataset.provenance["merge_history"]
- in-memory layout composition with
LogBuilder - rendering through the project layout with
render_report(...) - partial renders with:
render_section(...)render_track(...)render_window(...)
- notebook-friendly outputs:
- returned Matplotlib figures when no
output_pathis provided render_png_bytes(...)render_svg_bytes(...)render_section_png(...)render_track_png(...)render_window_png(...)
- returned Matplotlib figures when no
- serialization helpers:
document_to_dict(...)/document_from_dict(...)document_to_yaml(...)/document_from_yaml(...)report_to_dict(...)/report_from_dict(...)report_to_yaml(...)/report_from_yaml(...)save_document(...)/load_document_yaml(...)save_report(...)/load_report(...)
- builder/report persistence helpers:
LogBuilder.save_yaml(...)ProgrammaticLogSpec.to_yaml(...)LogBuilder.add_section(..., source_path=..., source_format=...)
Current public modules:
wellplot.api.datasetwellplot.api.builderwellplot.api.renderwellplot.api.serialize
Current examples:
- examples/api_end_to_end_demo.py
- examples/api_dataset_ingest_demo.py
- examples/notebooks/developer/api_dataset_ingest_demo.ipynb
- examples/api_dataset_alignment_demo.py
- examples/api_dataset_merge_demo.py
- examples/api_layout_render_demo.py
- examples/notebooks/developer/api_layout_render_demo.ipynb
- examples/api_partial_render_demo.py
- examples/api_notebook_bytes_demo.py
- examples/api_serialize_demo.py
- examples/mcp_workflow_demo.py
- examples/notebooks/developer/mcp_workflow_demo.ipynb
Important current boundary:
document_*helpers round-trip the normalizedLogDocumenttemplate shapereport_*helpers round-trip logfile/programmatic layout mappingssave_*/load_*convenience wrappers delegate to those same normalized surfaces- in-memory dataset contents are not embedded into YAML; YAML persists layout/report structure and optional section source references, while datasets remain separate Python objects or file-backed sources
The guiding rule is:
- YAML remains a first-class saved format
- the in-memory model becomes the canonical authoring surface
If you want one script that exercises the whole workflow, start with examples/api_end_to_end_demo.py. It ingests raw curves, computes secondary channels, aligns and merges them, renders a full report plus a window preview, and serializes the report YAML from the same Python session.
The full implementation checklist lives in docs/programmatic-api-plan.md.
For a user-facing workflow explanation, see docs/library-workflow.md.
For the package release workflow and TestPyPI/PyPI publishing process, see docs/release-process.md. Draft public release notes live in CHANGELOG.md.
See examples/triple_combo.yaml. For scale/grid behavior examples, see examples/log_scale_options.log.yaml. For resistivity-style scales and wrapped log demo, see examples/resistivity_scale_conventions.log.yaml. For VDL density, waveform overlay, and feet-based comparison examples, see:
- examples/cbl_vdl_array_mvp.log.yaml
- examples/cbl_vdl_array_overlay.log.yaml
- examples/cbl_comparison_feet.log.yaml
- examples/cbl_comparison_feet_full.log.yaml For fill and callout examples, see:
- examples/fill_modes_showcase.log.yaml
- examples/cbl_feature_showcase_full.log.yaml
- examples/curve_callouts_showcase.log.yaml
- examples/curve_callout_bands_showcase.log.yaml
- examples/curve_callout_bands_full.log.yaml
- examples/reference_track_overlays.log.yaml For annotation-track examples, see:
- examples/annotation_track_showcase.log.yaml
- examples/annotation_track_showcase_no_grid.log.yaml
- examples/annotation_track_objects_showcase.log.yaml For a coherent end-to-end cased-hole packet using heading, remarks, main/repeat sections, reference overlays, thresholded CBL QC, VDL, and restrained interval annotations, see:
- examples/cbl_job_demo.log.yaml
wellplot now supports YAML template inheritance for logfile configs.
- Put reusable layout defaults in template files, for example:
- Create per-job savefiles that reference templates:
Savefiles use:
template:
path: ../templates/wireline_base.template.yamlBehavior:
- Savefile values override template values.
- Tracks are defined in
document.layout.log_sections[*].tracks. - Data sources are section-scoped via
document.layout.log_sections[*].data. If top-leveldatais provided, it acts as a default for sections that do not set one. - Channels are assigned in
document.bindings.channels(channel+track_id). - Curve scales support
linear,log/logarithmic, andtangential. - For log tracks, vertical grid can auto-follow scale bounds with:
grid.vertical.main.spacing_mode: scaleandgrid.vertical.secondary.spacing_mode: scale. This adapts cycles and spacing for ranges like2->200vs2->2000, including non-decade starts. - Use
spacing_mode: countwhen you want fixed/manual line density independent of curve bounds. - Curves support wrapping across curve-capable tracks (
reference,normal,array):wrap: trueto enable with default curve color.wrap: { enabled: true, color: "#ef4444" }to color wrapped segments explicitly.
- Curves support first-class fills:
between_curvesfor same-scale curve-vs-curve fillsbetween_instancesfor fills between specific rendered curve instancesto_lower_limitandto_upper_limitfor fills to the active scale boundsbaseline_splitfor two-color fills around a vertical baseline
- Lower/upper limit fills are tied to the active scale bounds, not to the physical left/right side of the screen. Reversed scales still behave correctly.
- When you need a fill between two rendered copies of the same channel, assign explicit element ids:
id: cbl_0_100fill.other_element_id: cbl_0_10
- Track headers render fill indicators that follow the same semantics as the plotted fill, including crossover splits and baseline orientation.
- Curves support in-track callouts via
callouts:- inline labels at explicit depths
- repeated labels from section
top,bottom, ortop_and_bottom - side, text position, font, arrow, and offset controls
- hard edge avoidance, label-label avoidance, and soft curve-overlap avoidance
- Reference-track curve overlays support
reference_overlay:mode: curvefor slim normalized overlay curvesmode: indicatorfor narrow indicator lanesmode: ticksfor thresholded event-tick rendering from scalar channels
- Reference-track headers can now keep the reference scale row while rendering overlay properties in
the legend slot when
track_header.legend.enabled: true. - Curve header labels can opt into two-line wrapping with
document.bindings.channels[*].header_display.wrap_name: true, which is useful for narrow track headers such as reference-track overlay legends. - Reference tracks support local event objects under
reference.eventsfor one-off markers such as casing shoe, readings start, or tool-state transitions. - Annotation tracks support first-class typed objects under
tracks[*].annotations:intervalfor facies/zone blockstextfor descriptive notes at a depth or over an intervalmarkerfor symbol-based point eventsarrowfor explicit leader/indicator geometryglyphfor compact symbols or short codes
- Annotation
markerandarrowlabels support:priorityfor dense-track placement orderlabel_mode: free | dedicated_lane | nonelabel_lane_start/label_lane_endwhen the label must live in a reserved sub-lane
- Callout repetition is section-relative.
top,bottom, andtop_and_bottomgenerate repeated depths from the log section bounds, then render each label inline at those generated depths. - Raster bindings support display controls:
profile(generic,vdl, orwaveform)normalization(auto,none,trace_maxabs,global_maxabs)colorbar(true/falseor{ enabled, label, position })sample_axis(true/falseor{ enabled, label, unit, ticks, min, max, source_origin, source_step })waveform(true/falseor{ enabled, stride, amplitude_scale, color, line_width, max_traces, fill, positive_fill_color, negative_fill_color, invert_fill_polarity })
- Multiple curves per track are supported by assigning multiple bindings to the same
track_id. - Section placeholders are first-class in YAML:
document.layout.headingdocument.layout.remarksdocument.layout.log_sectionsdocument.layout.tail
- Report heading and tail blocks are rendered from the shared report object:
headingrenders the full cover/detail blocktailreuses the same data in a compact summary block
document.layout.remarksrenders page-level notes/remarks in the lower half of the first page and is intended for disclaimers, acquisition notes, or other summary text.header.report.service_titlesaccepts either plain strings or styled objects:valuefont_sizeauto_adjustbolditalicalignment: left | center | right
- Template YAML files can be partial; the merged savefile result is what gets validated and rendered.
- Page spacing is YAML-configurable:
document.page.margin_left_mm(default:0)document.page.track_gap_mm(default:0)
- Track-header legend space now auto-expands based on curve count in each track.
- For continuous logs in PDF viewers, set
render.continuous_strip_page_height_mmto export depth-continuous strip segments without vertical blank gaps while keeping readability. - Matplotlib visuals can be configured in YAML using
render.matplotlib.styleinstead of hardcoded renderer values. - For DLIS array channels,
sample_axis.min/maxcrops the actual waveform/raster columns to the selected window. It does not relabel the full array. - When DLIS tool metadata exposes micro-time sampling, the loader derives the sample axis
automatically. When vendor output still needs alignment tuning, the final user can override:
sample_axis.source_originsample_axis.source_step
Example VDL binding with explicit user-tunable sample axis:
document:
bindings:
channels:
- section: main
channel: VDL
track_id: vdl
kind: raster
profile: vdl
style:
colormap: gray_r
sample_axis:
enabled: false
unit: us
source_origin: 40
source_step: 10
min: 200
max: 1200
ticks: 7
waveform:
enabled: true
stride: 6
amplitude_scale: 0.28
line_width: 0.16Example report service titles with explicit formatting:
document:
layout:
heading:
provider_name: Company
service_titles:
- value: Cement Bond Log
font_size: 16
auto_adjust: true
bold: true
alignment: left
- value: Variable Density Log
font_size: 15
auto_adjust: true
italic: true
alignment: center
- value: Gamma Ray - CCL
font_size: 14
auto_adjust: true
alignment: rightReport page authoring rules:
document.layout.headinganddocument.layout.tailshare the same report object.headingrenders the full cover/detail block.tailis only a toggle (document.layout.tail.enabled) and reuses the same report data in a compact summary block.document.layout.remarksis a separate first-page report section for free-form notes.- The full heading selects exactly one detail table:
detail.kind: open_holedetail.kind: cased_hole
- Detail rows are fixed-row tables. Missing values stay empty; rows do not collapse.
- Use
label_cellswhen the left label column must be split. - Use
columns[].cellswhen a value column must be split into subcells.
Example report block:
document:
layout:
heading:
enabled: true
provider_name: Company
general_fields:
- key: company
label: Company
value: University of Utah
- key: well
label: Well
value: Forge 78B-32
- key: scale
label: Scale
value: ft 1:240
service_titles:
- value: Cement Bond Log
font_size: 16
auto_adjust: true
bold: true
alignment: left
detail:
kind: open_hole
rows:
- label_cells:
- Density
- Viscosity
columns:
- cells:
- G/L
- S
- cells:
- G/L
- S
- label: Logged Depth
values:
- ""
- ""
remarks:
- title: Remarks
lines:
- Summary note line 1.
- Summary note line 2.
alignment: left
tail:
enabled: trueExample instance-targeted fill between two rendered copies of the same channel:
document:
bindings:
channels:
- section: main
channel: CBL
track_id: cbl_fill
kind: curve
id: cbl_0_100
scale: { kind: linear, min: 0, max: 100 }
fill:
kind: between_instances
other_element_id: cbl_0_10
label: Scale Effect
crossover:
enabled: true
left_color: "#1f9d55"
right_color: "#d64545"
- section: main
channel: CBL
track_id: cbl_fill
kind: curve
id: cbl_0_10
scale: { kind: linear, min: 0, max: 10 }Example curve callout with section-relative repetition:
document:
bindings:
channels:
- section: main
channel: CBL
track_id: cbl
kind: curve
scale: { kind: linear, min: 0, max: 100 }
callouts:
- depth: 672
label: CBL
placement: top_and_bottom
distance_from_top: 500
distance_from_bottom: 500
every: 1000
side: right
text_x: 0.83
font_size: 10.5Example reference-track overlays and local events:
document:
layout:
log_sections:
- id: main
tracks:
- id: depth_overlay
kind: reference
width_mm: 20
reference:
axis: depth
define_layout: true
unit: ft
scale_ratio: 240
events:
- depth: 678
label: Casing Foot
tick_side: right
text_side: left
text_x: 0.72
track_header:
objects:
- kind: scale
enabled: true
line_units: 1
- kind: legend
enabled: true
line_units: 6
bindings:
channels:
- section: main
channel: TT
track_id: depth_overlay
kind: curve
reference_overlay:
mode: curve
lane_start: 0.06
lane_end: 0.24
- section: main
channel: TENS
track_id: depth_overlay
kind: curve
reference_overlay:
mode: indicator
lane_start: 0.78
lane_end: 0.94
- section: main
channel: CBL
track_id: depth_overlay
kind: curve
reference_overlay:
mode: ticks
tick_side: left
tick_length_ratio: 0.08
threshold: 100render:
backend: matplotlib
output_path: ../workspace/renders/job.pdf
dpi: 300
matplotlib:
style:
track_header:
background_color: "#efefef"
track:
x_tick_labelsize: 7.5
grid:
depth_major_linewidth: 0.8Use the master loader (single command for any log-file YAML):
wellplot render examples/cbl_main.log.yamlValidate a log-file against the JSON Schema before rendering:
wellplot validate examples/cbl_main.log.yamlOptional output override:
wellplot render examples/cbl_main.log.yaml -o out.pdfConvenience wrapper:
python examples/real_data_demo.pyOr pass a specific log file:
python examples/real_data_demo.py examples/cbl_main.log.yamlArray-track demo with synthetic VDL data and logfile config:
python examples/cbl_vdl_array_mvp_demo.pyUse templates/wireline_base.template.yaml as a reusable layout template, then create/modify job savefiles like examples/cbl_main.log.yaml. Keep local input/output assets under:
workspace/data/for LAS/DLISworkspace/renders/for generated PDF/HTML/JSON outputs The entireworkspace/folder is excluded from git. Note: DLIS normalization now supports scalar channels plus VDL/WF1-style array channels with derived sample axes. Exact micro-time origin can still be tuned per savefile when matching vendor-generated logs.
- docs/decision-log.md: agreed architectural and product decisions.
- docs/roadmap.md: phased development plan and near-term priorities.
- docs/rendering-workings.md: rendering flow and style-resolution model.
- docs/library-workflow.md: user-facing workflow of the library from data ingestion through rendering and YAML persistence.
- docs/programmatic-api-plan.md: concrete checklist for dataset ingestion, programmatic composition, partial renders, and notebook outputs.
Apache-2.0