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) -> SPMChannelremove_gwyddion_median_background(channel, radius_px=20) -> SPMChannelanalyze_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.