Skip to content

Python API

This page describes the public contracts in source version 0.1.5.dev0. SPM-Kit is alpha software: pin a version or commit for reproducible work and consult the format matrix before treating a reader as suitable for a particular instrument variant.

Install

python -m pip install spmkit                    # PyPI 0.1.2
python -m pip install "spmkit[gwy,hdf5]"        # selected optional features

The current source and GitHub-release options are listed in the installation guide.

Load image data

spmkit.load() is the compact built-in image dispatcher. It currently dispatches .nid, .nhf, .gwy, and SPM-Kit .npz bundles by extension.

from spmkit import SPMChannel, SPMData, load

data: SPMData = load("scan.nid")
print(data.names)

height: SPMChannel = data["Z-Axis"]
height_backward = data.get("Z-Axis", direction="backward")

print(height.shape)          # (lines, points)
print(height.unit)           # physical data unit
print(height.x_range)        # metres
print(height.y_range)        # metres
print(height.pixel_size_x)   # metres per pixel
print(height.direction)

SPMData contains an immutable tuple of channels, file metadata, and source_path. SPMChannel.with_data(array) returns a new channel carrying the original axes, unit, direction, group, and copied metadata. A missing channel raises KeyError; SPM-Kit does not silently guess a near-matching name.

Inspect and load by capability

Use the capability-based path when a file may contain image or force data:

from spmkit.core.io import inspect_any, load_any

info = inspect_any("measurement.nid")
print(info.format, info.kinds, info.channels)

payload, kind = load_any("measurement.nid", kind="image")

load_any() returns (payload, kind). The payload is SPMData for "image" or a ForceVolume for "force". Content detection additionally covers demonstrated JPK TIFF and numbered Bruker/Nanoscope files. Optional afmformats readers appear only when the afm extra is installed. This registry is broader than spmkit.load(); see the format matrix for evidence and limitations.

Level and calculate roughness

Analysis functions do not mutate input channels.

from spmkit.core.analysis import leveling, roughness

levelled = leveling.plane_fit(height)
# Alternatives:
# levelled = leveling.polynomial(height, order=2)
# levelled = leveling.align_rows(height, method="median")

stats = roughness.statistics(levelled)
print(stats.Sa, stats.Sq, stats.Sz, stats.unit, stats.n_points)
print(stats.Sp, stats.Sv, stats.Ssk, stats.Sku)
record = stats.to_dict()

Roughness expects a previously levelled spatial image. The result fields use the ISO-style capitalization shown above. The current implementation excludes non-finite values and centres the finite height population before calculating the metrics.

Arc-revolution background

SPM-Kit exposes physical arc-revolution background estimation through the public Python API:

from spmkit.core.analysis import (
    estimate_arc_revolution_background,
    remove_arc_revolution_background,
)

background = estimate_arc_revolution_background(
    height,
    radius=2e-6,
    direction="both",
    side="below",
    border="nearest",
)

corrected = remove_arc_revolution_background(
    height,
    radius=2e-6,
    direction="both",
    side="below",
    border="nearest",
)

radius is expressed in metres. Channel heights must use a supported geometric Z unit. Heights are converted internally to metres and returned in the original unit while preserving the channel context.

direction="horizontal" processes rows, "vertical" processes columns, and "both" applies horizontal followed by vertical. side="above" is defined as the inversion dual of "below".

The current contract accepts finite data and the "nearest" and "reflect" border policies. Masks, CLI and Fathom exposure are not available. The estimated background remains separately inspectable and satisfies corrected + background == original within floating-point tolerance.

This implementation is LEVEL 1 — SOFTWARE_VERIFIED through synthetic tests and an independent test-local one-dimensional oracle. Numerical equivalence with Gwyddion has not been established.

Sphere-revolution background

Sphere Revolution uses a true two-dimensional spherical cap in physical XY coordinates:

from spmkit.core.analysis import (
    estimate_sphere_revolution_background,
    remove_sphere_revolution_background,
)

background = estimate_sphere_revolution_background(
    height,
    radius=2e-6,
    side="below",
    border="nearest",
)

corrected = remove_sphere_revolution_background(
    height,
    radius=2e-6,
    side="below",
    border="nearest",
)

radius is expressed in metres. Geometric Z values are converted internally to metres and returned in the channel's original unit.

The spherical footprint is circular in physical coordinates. With anisotropic pixel spacing it can therefore appear elliptical in array-index coordinates. This operation is genuinely two-dimensional and is not equivalent to applying horizontal and vertical arc openings sequentially.

side="above" is the exact inversion dual of "below". The supported border policies are "nearest" and "reflect". Finite data are required; masks, CLI and Fathom exposure are not available.

The background remains separately inspectable and satisfies corrected + background == original within floating-point tolerance.

This implementation is LEVEL 1 — SOFTWARE_VERIFIED through synthetic tests and independent test-local two-dimensional oracles for both supported border policies. Numerical equivalence with Gwyddion has not been established.

Gwyddion-compatible Sphere-revolution background

SPM-Kit provides data-adaptive background estimation compatible with Gwyddion 2.71's Revolve Sphere module:

from spmkit.core.analysis import (
    analyze_gwyddion_sphere_revolution_background,
    estimate_gwyddion_sphere_revolution_background,
    remove_gwyddion_sphere_revolution_background,
)

background = estimate_gwyddion_sphere_revolution_background(
    channel,
    radius_px=20.0,
    inverted=False,
)

corrected = remove_gwyddion_sphere_revolution_background(
    channel,
    radius_px=20.0,
    inverted=False,
)

result = analyze_gwyddion_sphere_revolution_background(
    channel,
    radius_px=20.0,
    inverted=False,
)

radius_px is expressed in samples (array-index units). The public Gwyddion-compatible range is inclusive from 1.0 through 1000.0. channel must be a real, finite, non-empty SPMChannel.

inverted=False executes Gwyddion 2.71's normal Sphere Revolution route. inverted=True applies the exact dual -B(-data) for background estimation. To avoid the internal crash occurring in Gwyddion 2.71's C module when inverted=True, the corrected channel uses the safe deliberate divergence corrected = original - background, guaranteeing exact reconstruction of the original channel data.

analyze_gwyddion_sphere_revolution_background returns a BackgroundResult with method="gwyddion_sphere_revolution" and parameters={"radius_px": float(radius_px), "inverted": bool(inverted)}.

This estimator is distinct from SPMKit's physical sphere-revolution model (estimate_sphere_revolution_background), which operates with physical metric radii in metres, circular footprints in physical coordinates, and explicit physical border policies.

Gwyddion 2.71 Median Background

SPM-Kit provides the frozen Gwyddion 2.71 Median Background semantics through three public operations:

  • estimate_gwyddion_median_background(channel, radius_px=20) -> SPMChannel
  • remove_gwyddion_median_background(channel, radius_px=20) -> SPMChannel
  • analyze_gwyddion_median_background(channel, radius_px=20) -> BackgroundResult
from spmkit.core.analysis import (
    analyze_gwyddion_median_background,
    estimate_gwyddion_median_background,
    remove_gwyddion_median_background,
)

background = estimate_gwyddion_median_background(
    channel,
    radius_px=20,
)

corrected = remove_gwyddion_median_background(
    channel,
    radius_px=20,
)

result = analyze_gwyddion_median_background(
    channel,
    radius_px=20,
)
print(result.method, result.parameters)

radius_px is an integer pixel radius with default 20 and inclusive range 1..1024. The kernel is Gwyddion's fixed inclusive digital ellipse and exterior samples use fixed nearest-edge gwyddion_border_extend; the public API intentionally exposes no border, shape, rank, or backend option. estimate_gwyddion_median_background() returns the background as an SPMChannel, remove_gwyddion_median_background() returns input - background as an SPMChannel, and analyze_gwyddion_median_background() returns a BackgroundResult.

The result method is "gwyddion_median_background". Its metadata records radius_px, kernel_resolution, kernel_active_count, rank_index, rank_backend_reference, border_policy="gwyddion_border_extend", and kernel_geometry="gwyddion_digital_ellipse". rank_backend_reference identifies the observed Gwyddion reference route, not an SPM-Kit backend.

Inputs must be finite two-dimensional data; NaN and infinite values are rejected. The source channel is not mutated, and output channels preserve its shape, units, ranges, direction, group, and copied metadata according to the SPMChannel contract. The public implementation does not require Gwyddion at runtime.

This capability is CROSS_VALIDATED only within its frozen 36-case Gwyddion 2.71 campaign. Its scope, frozen evidence, semantics, and non-claims are specified in the Gwyddion Median Background compatibility specification.

Gwyddion 2.71 Filter flat-disc morphology

SPM-Kit exposes the frozen Gwyddion 2.71 Filter-tool flat-disc Opening and Closing:

from spmkit.core.analysis import (
    gwyddion_flat_disc_closing,
    gwyddion_flat_disc_opening,
)

opened = gwyddion_flat_disc_opening(channel, size_px=5)
closed = gwyddion_flat_disc_closing(channel, size_px=5)

Both functions accept a pixel-based size_px in the inclusive range 2..31, defaulting to 5, and return a new SPMChannel. The K×K digital ellipse, nearest-edge extension, and Gwyddion executable even-size anchoring are fixed; erosion, dilation, masks, ROI, ASF, and physical-radius options are not public parameters. Inputs must be finite, non-empty, and 2D; the source channel is not mutated, and shape, Z/XY units, ranges, direction, group, and copied metadata are preserved.

This capability is CROSS_VALIDATED only within the frozen 12-field campaign and six sizes 2, 3, 4, 5, 30, 31 (72 Opening and 72 Closing cases). The complete scope, executable tie semantics, evidence identities, and non-claims are recorded in the Gwyddion flat-disc morphology compatibility specification.

Gwyddion 2.71 Path Level

SPM-Kit exposes the frozen Gwyddion Path Level operation as a non-mutating channel transform:

from spmkit.core.analysis import gwyddion_path_level

lines = [(0.0, 0.0, 4.0e-6, 3.0e-6)]
levelled = gwyddion_path_level(channel, lines, thickness_px=1)

gwyddion_path_level(channel, lines, *, thickness_px=1) -> SPMChannel accepts an ordered collection of straight physical-coordinate selections (x0, y0, x1, y1). Duplicates and order are meaningful. thickness_px is an integer in the inclusive range 1..128, defaulting to 1. The operation has fixed endpoint conversion, no interpolation, horizontal-line exclusion, and cumulative row-level correction semantics. It requires finite, non-empty 2D data and finite positive channel ranges.

The result is a new SPMChannel: shape, Z/XY units, ranges, name, direction, group, and copied metadata are preserved, while the input remains unchanged. Masks, ROI, GwySelectionPath, splines, polylines, profiles, and GUI publication parameters are not part of this API. The scope, executable evidence, and non-claims are defined in the Gwyddion Path Level compatibility specification.

Gwyddion 2.71 Align Rows statistics

SPM-Kit exposes four explicit, non-mutating Gwyddion Align Rows statistics transforms. They are separate from the existing generic align_rows, whose semantics are not described as Gwyddion-compatible.

from spmkit.core.analysis import (
    gwyddion_align_rows_median,
    gwyddion_align_rows_median_of_differences,
    gwyddion_align_rows_trimmed_mean,
    gwyddion_align_rows_trimmed_mean_of_differences,
)

median = gwyddion_align_rows_median(channel, mask=mask, mask_mode="include")
differences = gwyddion_align_rows_median_of_differences(channel, direction="vertical")
trimmed = gwyddion_align_rows_trimmed_mean(channel, trim_fraction=0.05)
trimmed_differences = gwyddion_align_rows_trimmed_mean_of_differences(
    channel, trim_fraction=0.05
)

The public signatures are gwyddion_align_rows_median(channel, *, mask=None, mask_mode="ignore", direction="horizontal") and gwyddion_align_rows_median_of_differences(channel, *, mask=None, mask_mode="ignore", direction="horizontal"); the two trimmed variants add the keyword-only trim_fraction=0.05. GwyddionAlignRowsMaskMode is the typed literal "exclude" | "include" | "ignore"; GwyddionAlignRowsDirection is "horizontal" | "vertical". A mask is optional, finite, numeric, and exactly channel-shaped. Without a mask, every stored mode selects all samples. Outputs are independent C-contiguous float64 fields in a new SPMChannel, preserving name, units, physical ranges, direction, group, and copied metadata.

The fixed source semantics are: Exclude = 0, Include = 1, and Ignore = 2; vertical processing is transpose/restore. For absolute methods Include selects mask values > 0.0, Exclude selects values < 1.0, and undersampled rows use the global masked upper-median fallback before mean centring all row shifts. Difference methods require both adjacent mask values > 1.0 (Include) or < 1.0 (Exclude), use +0.0 for undersampled pairs, accumulate from row zero, and remove an unweighted least-squares row-index slope. Median is upper median. Trimmed methods use floor(fraction*n + 0.5) and use upper median when trimming would leave no retained value. The current public boundary returns only the corrected channel; private correction/background diagnostics are intentionally not a new public result architecture.

portable_source_semantics is the production contract. Public end-to-end tests are CROSS_VALIDATED only within the frozen finite 64-case campaign: all 64/64 corrected arrays and 3888/3888 elements are bitwise exact to the independent portable V2 oracle. The secondary installed_gwyddion_2_71_fast_math_profile is bitwise exact in 61/64 arrays and 3757/3888 elements. Its only recorded differences are three signed-zero elements in median__plateaus_signed_zero__10 and 64 finite elements in each of median_of_differences__irregular__11 and trimmed_mean_of_differences__irregular__11, bounded by absolute difference 5.329070518200751e-15. The installed process.so (c21d52375807ae096e34a3469c2f20c4c66ea3197479e13215a6d7b9d465b451) was built with GCC 16.1.1 -ffast-math, associative reassociation, and LTO. SPM-Kit does not emulate that local build; no V3 was justified. The complete evidence, profile policy, and non-claims are in the Gwyddion Align Rows statistics compatibility specification.

KPFM statistics

from spmkit.core.analysis import kpfm

cpd = kpfm.statistics(data["CPD"], tip_work_function=4.8)
print(cpd.mean, cpd.std, cpd.minimum, cpd.maximum, cpd.contrast)
print(cpd.work_function, cpd.work_function_unit)

tip_work_function is expressed in eV. With CPD in volts, the implementation uses sample_work_function = tip_work_function - mean_CPD. This sign convention and the channel's calibration must match the instrument workflow; SPM-Kit cannot infer them from an arbitrary label.

Spectral and grain analysis

from spmkit.core.analysis import grains, spectral

psd = spectral.radial_psd(levelled)
fractal = spectral.fractal_dimension(levelled, q_min=None, q_max=None)
correlation_length_m = spectral.correlation_length(levelled)

segmentation = grains.detect(
    levelled,
    threshold=None,
    min_size=4,
    relative_height=0.5,
)
print(segmentation.n_grains, segmentation.mean_diameter)
print(segmentation.coverage, segmentation.density)

radial_psd() returns q in 1/m. Grain detection uses SciPy, a required SPMKit dependency, uses eight-connected components, and reports density in grains per µm². Automatic thresholding is an algorithmic default, not a scientifically universal segmentation rule; record or override it for a campaign.

Force data

Force curves and force volumes are a separate domain from SPMData images.

from spmkit.core.io import load_force

volume = load_force("curve.jpk-force")
curve = volume.curve(0)
print(volume.grid_shape, curve.position)

load_force() currently covers .nid, .jpk-force, and .jpk. A single curve is wrapped as a 1 × 1 ForceVolume. Raw force segments preserve calibration state; operations that require calibrated force or tip-sample separation fail explicitly if those values have not been produced. Model choice, tip geometry, Poisson ratio, calibration, contact detection, and fit window remain scientific inputs.

The older array-oriented mechanics API is also public in this alpha release:

from spmkit.core.analysis import mechanics

curves = mechanics.extract_curves(data["Deflection"])
fit = mechanics.fit_hertz(
    curves[0],
    tip_radius=10e-9,
    poisson=0.3,
    model="sphere",
    spring_constant=0.3,
    contact_method="rov",
)
print(fit.young_modulus, fit.young_modulus_std, fit.r_squared, fit.rmse)

Do not treat a successful fit as evidence that the chosen contact model or calibration is valid for the sample.

Export

from spmkit.core.export import to_csv, to_hdf5, to_json
from spmkit.core.io import save_gwy

to_csv(stats, "roughness.csv")
to_json(stats, "roughness.json")
to_hdf5(data, "scan.h5")       # requires h5py
save_gwy(data, "scan.gwy")     # requires gwyfile

CSV is a presentation/interchange export and does not preserve the complete source object. HDF5 and GWY output do not establish universal lossless round-trip equivalence; retain the original instrument file, checksums, versions, and processing parameters.

Batch image analysis

from pathlib import Path
from spmkit.core import batch

files = batch.find_files(Path("measurements"))
result = batch.process(files, channel="Z-Axis", cpd_channel="CPD", level="plane")
print(result.n_ok, result.n_failed)
result.to_csv("summary.csv")

find_files() is non-recursive and follows the compact built-in image registry. BatchResult.rows retains an error string for each failed file rather than hiding it.

Reader plugins

Reader plugins implement the versioned Reader protocol and register through the spmkit.plugins.v1 entry-point group.

from pathlib import Path
from spmkit.core.plugins import DatasetInfo, register_reader

class ExampleReader:
    extensions = (".example",)

    def inspect(self, path):
        return DatasetInfo(
            path=Path(path),
            format="example",
            kinds=("image",),
            channels=(),
        )

    def load(self, path, kind=None):
        raise NotImplementedError("return an SPMData instance here")

register_reader(ExampleReader())

inspect() should be inexpensive; load() returns the requested data kind. Production plugins should publish an entry point rather than relying on process-local registration. The plugin contract is versioned, but the surrounding package remains alpha.

Contract summary

Concern Public entry point Result
compact image loading spmkit.load(path) SPMData
capability inspection inspect_any(path) DatasetInfo
capability loading load_any(path, kind) (payload, kind)
force loading load_force(path) ForceVolume
image preprocessing analysis.leveling.*, analysis.background.* new SPMChannel
numerical results analysis.* immutable result dataclasses or arrays
open exports core.export.*, save_gwy() file path/output artifact
extension discovery spmkit.plugins.v1 registered Reader/Domain

For end-to-end examples, continue with the first analysis, manual, and artifact contracts.