# Python libraries in v23

Use scientific Python in Log Viewer, Crossplot, Histogram or the standalone Python tool, with or without a LAS.

Danila Karnaukh | v23 | 2026-10-08

## Start with Libraries

1. Open [Python](../../python/index.html), or open **Python editor** in [Log Viewer](../../logs/index.html), [Crossplot](../../crossplot/index.html) or [Histogram](../../histogram/index.html).
2. Choose the **Libraries** tab below the editor. Viewing the catalogue does not start Python or download packages.
3. Find a package and choose **Load**. This starts the kernel if needed, then downloads the package and its dependencies. A LAS is not required. Alternatively, put the package's ordinary import statement in your script and press **Run**.
4. **Insert import** inserts the suggested code at the editor cursor; **Copy import** copies it. These actions do not execute the code.
5. The card reports **Available**, **Loading…**, **Loaded** with the installed version, or **Could not load** with the error. Use **Retry** after a failed load.

Only NumPy and the website's local `petrophysics` module load at kernel startup. The other scientific packages load when requested; opening the editor does not preload the whole catalogue. The Python standard library is part of the runtime. A first run or package download needs an Internet connection. Browser caching can reduce subsequent downloads, but offline availability is not guaranteed.

Package loading keeps editor text, committed curves, output and figures. **Run**, **New script** and **Reset kernel** are disabled while the dedicated loader is busy; **Stop** cancels the operation by resetting the worker. Loading packages does not run your analysis script. **New script** keeps the current kernel and packages. **Reset kernel**, Stop, source removal, loading another LAS and switching LAS datasets create a fresh kernel: optional packages must be loaded again, either from their imports or with Load. Each tool page has its own kernel; Lock dock shares the LAS and selected dataset, not installed packages or variables.

## Catalogue and import syntax

v23 uses the pinned **Pyodide 314.0.7** distribution. Package versions come from that distribution and appear in Libraries after loading. `lasio` is separately pinned to **0.32**. A package's distribution name and Python import name can differ: choose **scikit-learn** in Libraries but write `import sklearn`; choose **PyWavelets** but write `import pywt`.

| Library | Python import | Typical use here | Loading |
| --- | --- | --- | --- |
| NumPy | `import numpy as np` | Curve arrays, masks, numerical calculations | Kernel startup |
| pandas | `import pandas as pd` | Well/table DataFrames, grouping, missing values | Load or import |
| SciPy | `import scipy` | Fitting, interpolation, statistics, signal processing | Load or import |
| Matplotlib | `import matplotlib.pyplot as plt` | Figures in the Python Plots tab | Load or import |
| scikit-learn | `import sklearn` | Regression, clustering, preprocessing | Load or import |
| statsmodels | `import statsmodels.api as sm` | Statistical regression and diagnostics | Load or import |
| SymPy | `import sympy as sp` | Symbolic equations and derivatives | Load or import |
| XGBoost | `import xgboost as xgb` | CPU gradient boosting on tabular data | Load or import |
| PyWavelets | `import pywt` | Wavelet transforms of sampled curves | Load or import |
| xarray | `import xarray as xr` | Arrays with named dimensions and coordinates | Load or import |
| NetworkX | `import networkx as nx` | Graph relationships and paths | Load or import |
| uncertainties | `from uncertainties import ufloat` | Propagate measurement uncertainties | Load or import |
| micropip | `import micropip` | Install additional compatible Python wheels | Load or import |
| lasio 0.32 | `import lasio` | Optional Python LAS reading/writing | Load or static import; installed through micropip |
| petrophysics | `import petrophysics` | Website curve/dataset API; `well` is supplied for each run | Local module at kernel startup |
| Python standard library | `import math`, `json`, `datetime`, `statistics`, etc. | Ordinary Python utilities | Part of the runtime |

Static imports such as `from scipy.optimize import curve_fit` also trigger package loading. A dynamically constructed import such as `importlib.import_module(package_name)` may not be detected in advance: use Libraries → Load first. Dependencies can appear as loaded even if you did not request them separately. The catalogue covers these 14 external packages, rather than every module in Python or every package published on PyPI.

## First script: arrays, tables and a figure

This example needs no LAS. Printed output appears in **Output**; the figure appears in **Plots** after the run succeeds.

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

depth = np.arange(1000.0, 1010.0, 0.5)
gr = 65 + 18 * np.sin(np.arange(depth.size) / 3)
frame = pd.DataFrame({"Depth": depth, "GR": gr})
print(frame.describe())

fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(depth, gr)
ax.set(xlabel="Depth, m", ylabel="GR, API", title="Synthetic example")
fig.tight_layout()
```

## SciPy: fit a numerical model

No LAS is needed. This fits a simple synthetic exponential, then prints the fitted parameters and their approximate standard errors.

```python
import numpy as np
from scipy.optimize import curve_fit

def model(x, amplitude, rate):
    return amplitude * np.exp(-rate * x)

x = np.linspace(0, 5, 30)
rng = np.random.default_rng(7)
y = model(x, 2.5, 0.7) + rng.normal(0, 0.025, x.size)
parameters, covariance = curve_fit(model, x, y, p0=(2.0, 0.5))
print("Amplitude, rate:", parameters)
print("Approximate standard errors:", np.sqrt(np.diag(covariance)))
```

## scikit-learn: regression and clustering

Run this example without a LAS. A fitted cluster number is only a mathematical label; interpreting it as lithology or a rock type requires your own geological calibration.

```python
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

x = np.arange(12.0).reshape(-1, 1)
y = 3.0 + 2.0 * x[:, 0]
regression = LinearRegression().fit(x, y)
print("Slope:", regression.coef_, "Intercept:", regression.intercept_)
print("Prediction at 12:", regression.predict([[12.0]]))

features = np.array([[20, 2.20], [25, 2.23], [30, 2.25],
                     [100, 2.60], [105, 2.63], [110, 2.66]])
scaled = StandardScaler().fit_transform(features)
clusters = KMeans(n_clusters=2, random_state=7, n_init=10).fit_predict(scaled)
print("Synthetic cluster labels:", clusters)
```

## statsmodels and symbolic mathematics

These are separate examples and need no LAS. OLS receives an explicit constant column for its intercept.

```python
import numpy as np
import statsmodels.api as sm

x = np.arange(20.0)
y = 4 + 1.5 * x + 0.2 * np.sin(x)
fit = sm.OLS(y, sm.add_constant(x)).fit()
print("Intercept, slope:", fit.params)
print("R squared:", fit.rsquared)
print("Parameter standard errors:", fit.bse)
```

```python
import sympy as sp

phi, a, rw, rt, m, n = sp.symbols("phi a rw rt m n", positive=True)
sw = (a * rw / (rt * phi**m)) ** (1 / n)
print("Symbolic Archie Sw:", sw)
print("Derivative with respect to porosity:", sp.diff(sw, phi))
```

## XGBoost on the CPU

This is a small, synthetic regression example without a LAS. Use CPU settings and small training sets to begin; browser Python does not provide CUDA/GPU training.

```python
import numpy as np
from xgboost import XGBRegressor

rng = np.random.default_rng(7)
features = rng.normal(size=(64, 2))
target = 2 * features[:, 0] - features[:, 1]
model = XGBRegressor(n_estimators=8, max_depth=2, tree_method="hist",
                     device="cpu", n_jobs=1, random_state=7)
model.fit(features, target)
print("First three predictions:", model.predict(features[:3]))
```

## Wavelets, named arrays, graphs and uncertainty

Each example is independent and needs no LAS.

```python
import numpy as np
import pywt

signal = np.sin(np.linspace(0, 4 * np.pi, 32))
approximation, detail = pywt.dwt(signal, "db2")
print("Wavelet coefficient sizes:", approximation.size, detail.size)
```

```python
import xarray as xr

curve = xr.DataArray([30.0, 45.0, 70.0], dims=["depth"],
                     coords={"depth": [1000.0, 1000.5, 1001.0]}, name="GR")
print(curve)
print("Mean:", float(curve.mean()))
```

```python
import networkx as nx

graph = nx.Graph()
graph.add_edges_from([("Well A", "Layer 1"), ("Layer 1", "Well B")])
print(nx.shortest_path(graph, "Well A", "Well B"))
```

```python
from uncertainties import ufloat

rho_matrix = ufloat(2.65, 0.02)
rho_bulk = ufloat(2.35, 0.01)
rho_fluid = ufloat(1.00, 0.01)
porosity = (rho_matrix - rho_bulk) / (rho_matrix - rho_fluid)
print("Porosity with propagated uncertainty:", porosity)
```

The example uncertainties are illustrative. Their interpretation depends on how the input uncertainties and correlations were estimated. `well.add_curve()` accepts numeric values, so publish nominal values or a separate standard-deviation curve rather than uncertainty objects themselves.

## Work with your loaded LAS

The website supplies `well` automatically at each run. With no LAS, `well` is `None`. Use the existing snapshot directly; installing lasio is not required to calculate curves.

```python
import numpy as np
import pandas as pd

if well is None:
    print("Load a LAS to inspect its tables.")
else:
    print(well.name, well.las_version, well.dataset_id)
    frame = well.to_dataframe()
    print(frame.head())
    print(frame.attrs["units"])
    for table in well.datasets:
        print(table.id, table.name, table.row_count)
    another_table = well.dataset(well.datasets[-1].id)
    print(another_table.to_dataframe().head())
```

For a model, clean missing samples before fitting, and put predictions back into an array with the original row count before publishing. This example requires unique numeric `GR` and `RHOB` curves. Its regression is a coding demonstration; calibrate and validate any model before interpreting predicted density.

```python
import numpy as np
from sklearn.linear_model import LinearRegression

if well is None:
    raise ValueError("Load a LAS with GR and RHOB first.")

gr = well["GR"]
rhob = well["RHOB"]
training = np.isfinite(gr) & np.isfinite(rhob)
if training.sum() < 3:
    raise ValueError("Need at least three valid GR/RHOB pairs.")
model = LinearRegression().fit(gr[training, None], rhob[training])
predicted = np.full(well.row_count, np.nan)
usable = np.isfinite(gr)
predicted[usable] = model.predict(gr[usable, None])
well.add_curve("RHOB_REG", predicted, unit=well.curve("RHOB").unit,
               description="Illustrative linear prediction from GR")
print("Training samples:", training.sum())
```

Only the active table receives published curves. Accessing another table through `well.dataset(...)` does not switch the site's selection. Numeric plots use numeric data; LAS 3.0 text/date fields remain typed object arrays in Python. Convert dates explicitly with a known format if needed. See [LAS formats and datasets](../las-formats/index.html) and the [Python workspace API](../python/index.html).

## Optional lasio 0.32

Use Libraries → **lasio → Load**, or a static `import lasio` statement. v23 installs the pinned compatible wheel through micropip before the script runs. This is an additional Python utility; the website continues to use its shared JavaScript LAS reader for uploads, table selection, Lock dock and export. lasio documents complete LAS 1.2/2.0 support and partial LAS 3.0 support, so it does not replace the site's multiple-table/typed LAS 3.0 handling.

This self-contained example reads LAS text in memory and needs no uploaded LAS:

```python
import io
import lasio

text = """~Version
VERS. 2.0 : LAS version
WRAP. NO : Unwrapped
~Well
STRT.M 1000 : Start
STOP.M 1001 : Stop
STEP.M 1 : Step
NULL. -999.25 : Missing
WELL. SYNTHETIC : Well
~Curve
DEPT.M : Depth
GR.API : Gamma ray
~ASCII
1000 30
1001 75
"""
las = lasio.read(io.StringIO(text))
print("lasio version:", lasio.__version__)
print(las["GR"])
```

An uploaded LAS is supplied as `well` and its tables; it is not automatically mounted as a Python file at `well.filename`. Do not use `lasio.read("C:/Users/.../well.las")` to access your computer. For routine calculations, use `well.to_dataframe()` or `well.curve(...)`. When you already have LAS text in a Python variable, use `io.StringIO` as above. The website's **Download original LAS** and **Export LAS** buttons provide actual browser downloads.

## micropip and additional wheels

The normal loader already installs lasio. An explicit equivalent, useful when learning micropip, is:

```python
import micropip

await micropip.install("lasio==0.32")
import lasio
print(lasio.__version__)
```

The editor supports top-level `await`. micropip can install pure-Python wheels and compatible Pyodide/WebAssembly wheels. A Linux, Windows or macOS native wheel is not compatible just because it works in desktop Python. A custom wheel URL must permit browser cross-origin access (CORS); package repositories and download hosts also need to be reachable from your network. Shell `pip install ...` commands are not Python code for this editor. Additional packages are outside the curated catalogue; their compatibility must be checked separately. Installation affects the current worker kernel and does not add files to your GitHub repository or Render server.

## Browser limits and saved work

- Python and calculations run in a browser Web Worker, not a Python process on Render. Available memory and CPU depend on the user's device. Large DataFrame copies, large models and expensive fitting can exhaust browser resources.
- The workspace does not provide an OS shell, subprocesses, desktop GUI windows or CUDA/GPU computing. Packages may have individual functions that depend on facilities unavailable in browser Python.
- Python file paths refer to its browser virtual filesystem, not your computer's folders. Temporary files disappear with kernel reset/reload unless downloaded explicitly through a supported workflow. Save .py downloads your code; Save project retains code/results with a loaded LAS. Installing packages does not save Python variables or trained models.
- Matplotlib uses the non-interactive Agg backend. Successful runs collect up to four PNG figures of at most 8 MiB each in Plots. Do not close a figure before collection if you want to see it. There is no desktop Matplotlib window.
- Run time limits, output limits and result-curve validation still apply. Published curves must be 1D real numeric arrays with one value per active source row; missing values may be NaN, infinities cannot be published. Save work before changing source or resetting the kernel.
- Run initialization has a separate 180-second limit; a manual Libraries load has 300 seconds. The selected script execution limit starts after its required packages have loaded. Stop remains available if an operation stalls.
- Package versions are pinned through the chosen Pyodide distribution, with lasio fixed at 0.32. Installing a different version manually may conflict with these dependencies. Libraries shows the versions detected in the current kernel; consult the release's validation notes for the packages actually exercised in its checks.

## JavaScript / agent access

```javascript
const catalogue = Petrophysics.get_python_libraries();
console.log(catalogue.runtimeReady, catalogue.packages);

await Petrophysics.load_python_libraries({
  packageIds: ["scikit-learn", "pywavelets", "lasio"]
});
const after = Petrophysics.get_python_libraries();
console.log(after.packages);

await Petrophysics.run_python({code: "import lasio\nprint(lasio.__version__)"});
```

Reading the catalogue does not start Python. Loading uses distribution IDs from that catalogue; it does not run a script. `get_python_status().loadingLibraries` distinguishes library work, while `running` reports that the Python workspace is busy. Wait for loading to finish before running an analysis or changing the source. These commands are available in all four tools.

## Official references

The website-specific instructions above describe v23's loader and snapshot API. General package behavior is documented by the package maintainers:

- [Pyodide package catalogue](https://pyodide.org/en/stable/usage/packages-in-pyodide.html), [package loading](https://pyodide.org/en/stable/usage/loading-packages.html), [browser Python constraints](https://pyodide.org/en/stable/usage/wasm-constraints.html), and [micropip usage](https://micropip.pyodide.org/en/stable/project/usage.html).
- [SciPy curve_fit](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html), [scikit-learn LinearRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html), [KMeans](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html), and [statsmodels OLS](https://www.statsmodels.org/stable/generated/statsmodels.regression.linear_model.OLS.html).
- [SymPy calculus](https://docs.sympy.org/latest/tutorials/intro-tutorial/calculus.html), [XGBoost Python API](https://xgboost.readthedocs.io/en/stable/python/python_api.html), [PyWavelets DWT](https://pywavelets.readthedocs.io/en/latest/ref/dwt-discrete-wavelet-transform.html), and [xarray DataArray](https://docs.xarray.dev/en/stable/generated/xarray.DataArray.html).
- [NetworkX paths](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.shortest_paths.generic.shortest_path.html), [uncertainties guide](https://uncertainties.readthedocs.io/en/latest/user_guide.html), and [lasio documentation](https://lasio.readthedocs.io/en/latest/).
