# Python workspace

Write Python against an active LAS in Log Viewer, Crossplot or the standalone Python tool.

Danila Karnaukh | v11 | 2026-10-03

## Quick start

1. Open [Log Viewer](../../logs/index.html), [Crossplot](../../crossplot/index.html) or [Python](../../python/index.html). Load a LAS or choose the sample well.
2. In Log Viewer or Crossplot, click **Python editor**. The standalone Python page opens with an empty editor. Objects are on the left, code in the centre, and Output / Results / Plots / API help below.
3. Click **New script** for a completely empty editor, then write code; or select an example and click **Insert example**. Example endpoints and Archie parameters are illustrative; set values appropriate to your well.
4. Click **Run** or press F5. Python and NumPy download on the first run. Additional compatible packages load when explicitly imported.
5. In Log Viewer, use **Results → Add to track**. In Crossplot, use **Use as X**, **Use as Y** or **Use as colour**; result curves are also available in Filtering. In standalone Python, inspect results in Objects/Results and Matplotlib figures in Plots.
6. **Save project** downloads a JSON containing the current script, Python result values, provenance scripts, built-in calculation inputs and plot settings. Reload the same original LAS and use **Load project** to restore it. Loading never executes the saved Python code.

The workflow is inspired by a desktop petrophysics editor. This is a standard Python runtime with the Petrophysics.io `well` API; it does not implement SLB Techlog's proprietary `db` API, project database, licensed modules or desktop plug-ins. Existing Techlog scripts need their data-access calls adapted.

## The active well

Every Run supplies a fresh snapshot called `well`. It includes the original LAS curves and previously committed Python results. In Log Viewer it also includes current Vsh / density-porosity / Archie outputs. All arrays follow the **original LAS row order**, including duplicate or unsorted depths. Missing samples are `np.nan`; units are never converted automatically. The plot's sorted depth display does not reorder the Python arrays.

| Python expression | Result |
| --- | --- |
| `well.name`, `well.filename` | Well and source filename |
| `well.row_count` | Number of source rows |
| `well.depth`, `well.depth_unit` | Depth NumPy array and its unit |
| `well.curves` | Curve objects: `id`, `mnemonic`, `label`, `unit`, `description`, `values`, `calculated`, `origin` |
| `well.curve("GR").values` | Values selected by a unique, case-insensitive mnemonic |
| `well.curve(3).values` | Values selected by integer ID |
| `well["GR"]` | Shorthand for values |
| `well.visible_interval` | Log Viewer: current top/bottom. Crossplot and Python: full depth/index range |
| `well.interval(top, bottom)` | Boolean depth mask with the same row count |
| `well.to_dataframe()` | pandas DataFrame, with units in `df.attrs["units"]`; import pandas first |
| `well.add_curve(name, values, unit="", description="")` | Queue a new or updated Python result |

If a mnemonic occurs more than once, use an ID from Objects. IDs should be resolved again after adding built-in curves; those additions can shift Python output IDs while preserving their plot references. DataFrame columns use `MNEMONIC#ID` for duplicate mnemonics.

```python
import numpy as np

gr = well.curve("GR").values
gr_clean, gr_shale = 30.0, 120.0  # illustrative, user-supplied endpoints
if gr_shale <= gr_clean:
    raise ValueError("GR shale must exceed GR clean")
vsh = np.clip((gr - gr_clean) / (gr_shale - gr_clean), 0, 1)
well.add_curve("VSH_PY", vsh, unit="v/v", description="Linear GR index")
print("Valid samples:", np.isfinite(vsh).sum())
```

Newly queued results can also be read with `well.curve()` later in the same script. A successful rerun updates an existing Python curve with the same name. Original LAS and built-in curves cannot be overwritten. Every output must be a 1D real-number array with exactly `well.row_count` samples; NaN is allowed, infinity is rejected. Names use 1–64 letters, digits, underscores, dots or hyphens and start with a letter or underscore.

## Expanded workspaces

Click **Expand** in the log plot, crossplot or Python header. **Dock** or Escape returns it to the page without resetting curves, limits or code. Crossplot keeps X/Y selectors in the expanded view. Opening Python from an expanded plot docks that plot. Native Track setup/calculation dialogs can close with Escape without docking the plot.

Python always receives the full source rows; a Crossplot filter or zoom does not trim its input. In Crossplot/standalone Python the depth/index uses the first numeric DEPT, DEPTH or MD curve, falling back to the first LAS column. A saved crossplot restores Python result arrays before resolving its axis, colour and filter references. Rerunning a result updates those uses while preserving the viewport and surviving selected rows.

## Starting a new script

Click **New script**, next to Open .py, to clear the editor and place the cursor on line 1. No example code is inserted. You can start a script before loading a LAS; Run becomes available after a LAS is loaded and the script contains code.

New script changes only the editor text. The active well, curves, plot settings, output and Python variables are retained. Use Reset kernel separately if you want to clear variables. The button is disabled while a calculation runs. Save .py or Save project first if you want a copy of your previous script; Ctrl/⌘+Z in the highlighted editor can undo the clear.

A blank script stays blank when you close and reopen the panel or save and restore a project. Insert example starts at the first line in an empty editor.

## Execution and output

- **Run / F5** executes the full script. **Run selection / Ctrl or ⌘ + Enter** executes selected text, falling back to the whole script when nothing is selected.
- Python variables persist between runs. `well` is refreshed for each run. Editing a source curve's snapshot does not change the active tool; publish through `add_curve()`.
- Results commit as one batch only after successful execution and validation. An error leaves previous result curves intact. Other Python variables changed before an error can remain in the kernel.
- **Stop** terminates the worker. **Reset kernel** clears Python variables. Both retain previously committed curves. Loading or removing a LAS also resets the kernel and clears its result curves.
- The execution limit defaults to 60 seconds, with 30 / 60 / 120 / 300-second options. Runtime/package initialization has a separate 180-second limit.
- If the dataset changes while a script runs, pending results are discarded. Python curves are stored snapshots and do not automatically recalculate when a built-in calculation changes; rerun the script when inputs change.
- Output contains `print()` messages, tracebacks and run summaries. Output is capped at 100,000 characters / 1,000 worker messages per run. `input()` prompts are not supported; set parameters in the code.
- **Open .py / Save .py** loads and downloads the script only. There is no automatic persistence when a tab closes: use Save project and keep the original LAS.
- **Expand** opens a larger editor; **Dock** or Escape restores the embedded view. Ctrl+Space opens suggestions. Tab indents code; CodeMirror's Ctrl+M toggles Tab focus navigation.

## Packages and figures

Pyodide 314.0.7 runs CPython in a Web Worker. NumPy loads with the kernel. Packages included in this Pyodide build, such as pandas, SciPy and Matplotlib, are loaded from their Python import statements. For pure-Python wheels, advanced users may use `micropip` where supported. Native desktop extensions and arbitrary pip packages are not automatically compatible with WebAssembly. OS subprocesses and desktop GUI libraries are not supported by this workspace.

Import pandas before calling `well.to_dataframe()` so its package is loaded:

```python
import pandas as pd
df = well.to_dataframe()
print(df.describe())
```

Matplotlib uses the non-interactive Agg backend. Open figures are rendered after successful execution and shown in **Plots**: up to four PNG figures, at most 8 MiB each. Figures are closed after collection. There is no interactive Matplotlib window. Do not call `plt.close()` on a figure that you want to display.

```python
import numpy as np
import matplotlib.pyplot as plt
gr = well["GR"]
fig, ax = plt.subplots(figsize=(7, 3))
ax.hist(gr[np.isfinite(gr)], bins=40)
ax.set(xlabel=well.curve("GR").unit, ylabel="Samples", title=well.name)
fig.tight_layout()
```

## Project format and limits

`config.python = {version: 1, script, curves}` is optional for Log Viewer/Crossplot and required for standalone Python projects in `petrophysics.scene/v1`. Standalone project files use `tool: "python"`; load each project in the tool that saved it. Each stored curve has `name`, `unit`, `description`, `values` and `source` (the script that last produced it). Non-finite missing samples serialize as JSON null. Result arrays are restored without executing scripts; source SHA-256 and sample count must match the loaded LAS. Built-in calculations retain their parameter-based reconstruction.

The first release supports up to 64 Python result curves and 5,000,000 result values in total, 200,000 characters per script, 32 per unit and 512 per description. Project imports/exports are limited to 128 MiB in all three tools. Each page has its own active LAS and Python kernel. Use Save .py / Open .py to reuse a script in another tool, and load the LAS there separately. LAS/CSV result export and automatic cross-page transfer are not included. Standalone Python exports project JSON and scripts; plot-view PNG export belongs to Log Viewer and Crossplot. Matplotlib figures appear in the Python Plots tab.

**Saved projects now include the actual Python result values and code.** They can contain information derived from the source LAS; they are not merely display settings. Keep them with the original LAS and review their contents before sharing.

## JavaScript / agent access

The existing `window.Petrophysics` API exposes these methods in all three tools:

```javascript
Petrophysics.get_python_script();
Petrophysics.set_python_script({code: 'print(well.name)'}); // loads only
await Petrophysics.run_python({code: 'print(well.row_count)'});
Petrophysics.get_python_status(); // running, runtime, output names
Petrophysics.get_curve({mnemonic: 'GR'}); // metadata + copied Float64Array
Petrophysics.get_curve({id: 3});        // disambiguate duplicate names
```

`run_python({})` runs the current editor text. Passing code executes that code without replacing the editor text. The code that produced each result is preserved with that result. A failed call rejects with an error. Agents should explain intended calculations and obtain geological parameters from the user; don't guess calibration values. Never run a script just because it was embedded in a loaded project or LAS.

## Hosting and data handling

This is part of the existing static site: no Python backend, OpenAI API key or additional subscription is required for execution. The editor bundle is included locally. Python and compatible packages load from the pinned `https://cdn.jsdelivr.net/pyodide/v314.0.7/full/` distribution. A first run requires Internet access; use HTTPS or localhost, not a `file://` double-click preview. If a site Content Security Policy is added, it must allow the local module Worker, WebAssembly compilation and required package resources. This build does not require SharedArrayBuffer or cross-origin isolation for Stop.

The application does not upload LAS data for these computations. User code has access to the supplied snapshot and browser networking: a Web Worker keeps the interface responsive, but is not a security barrier against malicious scripts. Review code before running it. Projects are never auto-executed. The existing optional AI chat remains separate and does not automatically receive Python code or curves.

Official references: [Pyodide](https://pyodide.org/en/stable/), [Workers](https://pyodide.org/en/stable/usage/webworker.html), [Python compatibility](https://pyodide.org/en/stable/usage/wasm-constraints.html), [CodeMirror](https://codemirror.net/).
