Codebase audit: fix wrong-result, correctness, cleanup and performance findings - #439
Merged
Conversation
…hitecture) Consolidated findings from a systematic audit of the core topography classes, Uniform/Nonuniform/Generic analysis modules, all IO readers, Container/Support infrastructure, and the C++ extensions. Findings are severity-ranked with file:line references; cross-cutting themes and a suggested triage order are included at the end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
DecoratedUniformTopography now derives pixel_size and area_per_pt from the physical_sizes and nb_grid_pts of the decorated topography itself instead of forwarding the parent's values. Decorators that change the geometry (transpose, downsample) previously reported the parent's pixel size, silently corrupting every derivative-based quantity (rms_gradient, scale-dependent slope/curvature, PSD normalizations) for transposed topographies with anisotropic pixels and for downsampled topographies. - TransposedUniformTopography additionally swaps nb_subdomain_grid_pts and subdomain_locations and no longer crashes in positions() for line scans. - DownsampledUniformTopography now reports physical_sizes consistent with the retained samples when the downsampling factor does not divide the number of grid points. - ScaledUniformTopography drops its pixel_size/area_per_pt overrides, which would now double-apply the scale factor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
- DI, EZD, PS: The raw data buffer of these formats is stored scan line by scan line, i.e. it has C-order shape (ny, nx). The readers reshaped to (nx, ny), which interleaves scan lines into garbage for non-square maps. All test fixtures for these formats are square, which is why this went unnoticed. - MI: Same reshape fix. In addition, xLength/yLength are stored in meters but were reported in the channel unit (typically um), making all mixed lateral/height quantities (rms slope, PSD, curvature) wrong by ~10^6; the lateral sizes are now converted to the channel unit. The scanUp flag is textual TRUE/FALSE and was evaluated for truthiness, so scan-down images were also flipped. Text/ASCII data now raises UnsupportedFormatFeature instead of NameError. The byte-wise chr()/join()/encode() round trip was replaced by direct buffer slicing. - BCR: 'voidpixels' holds the *count* of void pixels, not the marker value. Files with voidpixels=0 previously masked every pixel with raw value 0; real void pixels (int16 32767 / float FLT_MAX) were never masked; a missing key crashed. Masking now follows the BCR-STM/BCRF specification. - EZD: The format magic check compared bytes against str (always unequal) and had inverted logic, i.e. no validation at all; it now rejects non-EZD files with FileFormatMismatch. - NMM: Every scan was registered with channel index 0, so channels[i].topography() always returned the first scan. - OIR: The channel reader lambda captured loop variables by reference; files with multiple height channels returned the last channel's data for every channel. Loop variables are now bound as default arguments. - POIR: Channels aggregated from multiple OIR sub-files retained their per-file index; selecting a channel could return data from a different file. Channels are now renumbered on concatenation (ChannelInfo gained an index setter for this purpose). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
…line alignment - Uniform/BearingArea: The periodic 2D element bounds called np.roll with a tuple shift and no axis, which flattens the array and rolls by the sum of the shifts. The min/max bounds therefore paired wrong pixels and no longer bracketed the exact bearing area, silently corrupting median(), mad_height() and the robust detrending modes for periodic topographies. bearing_area(median()) is now exactly 0.5. - Generation.fourier_synthesis: The q=0 Fourier mode was populated with a random amplitude equal to C(q=1), giving generated surfaces a random, unit-dependent mean offset that typically dwarfs the requested rms height. The mean is now exactly zero. - Special.make_sphere: The periodic flag was silently dropped (the returned topography was always nonperiodic); kind='paraboloid' with a numeric standoff crashed with UnboundLocalError. The redundant fill_undefined_data() wrapper for numeric standoffs was removed. - Uniform/ScanLineAlignment: direction semantics were inverted relative to the library's x-first array convention: the default direction='x' (fast scan along x) detrended lines running along y. Scan lines along x are columns of the height array and are now transposed accordingly. Tests were updated to construct artifacts matching the documented semantics; direction is now validated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
…tatistics - Nonuniform/Converters.to_uniform: The uniform interpolation grid always starts at zero, but the parent scan's x-coordinates can start anywhere; np.interp clamps outside the data range, so scans with an offset origin produced garbage heights (and thereby garbage FFT-based PSD, ACF, derivatives and reliability cutoffs). The interpolation points are now shifted by the parent's origin. - Nonuniform/Detrending 'slope' mode: The rms-slope-minimizing constant slope is the length-weighted mean of the derivative, (h[-1] - h[0]) / L, not the plain mean of the per-segment slopes; and the offset must remove the mean of the *tilt-corrected* profile, which requires the -a1*<x> correction for scans not starting at x = 0. - Nonuniform/PowerSpectrum: The Hann window was evaluated at absolute x instead of x - x.min(), defeating leakage suppression for scans with an offset origin. - Generic/Curvature: The scale-dependent curvature stencil 8A(lambda) - 2A(2*lambda) was implemented via index arithmetic that is only valid on a linearly spaced ACF grid anchored at zero. The default arguments deliver a log-spaced grid, and reliability trimming shifts the origin, silently pairing wrong distances. A(2*lambda) is now obtained by interpolation, which is exact on linear origin-anchored grids and correct on any other grid. Verified against the analytic curvature of a cosine profile and against artifact-free ground truth in the tip-artifact tests (error drops from 48% to 6%); test tolerances that were calibrated against the old behavior were updated and now additionally compare against the unartefacted topography. - Generic/ScaleDependentStatistics: When called with scale_factor, the reported distance was scale_factor * mean(physical_sizes) instead of scale_factor * n * mean(pixel_size) - too large by a factor of nb_grid_pts/n - and this wrong distance also fed the reliability mask. A list-valued distance no longer crashes the reliability check. - Uniform/Derivative: interpolation='disable' with a fractional scale factor silently truncated the stencil step while normalizing by the fractional pixel size; the guard compared against the misspelled option 'disabled' and was unreachable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
read_container() opens the container reader as a context manager and returns the lazy container after __exit__ has closed the underlying stream. ZAGReader handed ZipExtFile streams bound to that stream to the ZONReaders it constructs, so every element access on the returned container read from a closed file. The readers now receive a callable (ZAGFileOpener) that reopens the ZIP member on each read, following the pattern established by CEFileOpener. The only ZAG test was skipped and pointed at a hardcoded local path; it is replaced by tests that synthesize a minimal ZAG container around the zon-1.zon fixture, including a regression test for container access after the reader has been closed. Also use logging.getLogger instead of instantiating a detached Logger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
…put handling - cpp/moments.h: The generic triangle moment had a sign error and a missing 2/(h3-h1) prefactor, and the order-1..3 specializations were equally wrong: uniform2d_mean of a constant surface at height 5 returned 0, uniform2d_variance of a terraced surface returned 0, and the (unspecialized) order-4 path divided by height differences without a degeneracy guard, returning NaN for quantized data. All triangle moments are now computed from the barycentric integral formula, <h^n> = 2/((n+1)(n+2)) H_n(h1,h2,h3), where H_n is the complete homogeneous symmetric polynomial - exact, stable, no sorting and no division by height differences. The line-scan moments use the same polynomial form for the generic (order >= 4) case, replacing an epsilon guard that returned 0 for flat segments (the correct limit for a constant profile at height 2 is 16, not 0). Tests that had codified the broken values are corrected and extended with analytic references. - SurfaceTopography/Nonuniform/ScalarParameters.moment: same removable 0/0 singularity in pure Python; any two equal adjacent heights (quantized instrument data) propagated NaN into the result. Replaced by the equivalent complete homogeneous polynomial. - cpp/bicubic.cpp: Array inputs were only 'ensure'd, not forced C-contiguous, but the evaluation loop walks the buffer linearly: strided views returned silently wrong values and negative strides read out of bounds. Inputs are now converted with c_style | forcecast. The scalar path also ignored the derivative argument and returned a bare float where callers unpack 3- or 6-tuples. - SurfaceTopography/Uniform/Interpolation.py: the second-order bicubic cross-derivative was overwritten by a copy-paste error (interp_derxx / dx / dy instead of interp_derxy / dx / dy). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
These files are written to the current working directory by the NetCDF writer tests and were inadvertently picked up by an earlier commit on this branch. Remove them and ignore them so future test runs cannot re-add them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
- Support.fold_fft_half: The q=0 entry has no mirrored partner and must not be halved; power_spectrum_from_profile(resampling_method=None) previously returned half the true DC power (Parseval violation in the first bin). The ACF caller was unaffected (A[0] = 0). - Uniform/PowerSpectrum: With log collocation, q=0 is stripped before resampling, so min_value=q[1] discarded the fundamental mode. The resampling range now starts at the smallest positive wavevector. - Uniform/Autocorrelation: The reliability cutoff of the areal ACF now carries the same factor 1/2 as the profile ACF (the cutoff is estimated from the curvature but the ACF is the slope, see 10.1016/j.apsadv.2021.100190), and uses the same two-pixel lower bound; the area path previously discarded a factor-two band of reliable short-distance data. - Uniform/Interpolation: interpolate_fourier halved (and mirrored) the Nyquist rows/columns even when that direction was not enlarged; interpolation onto the same grid was not the identity. The linear interpolator truncated positions toward zero instead of flooring, extrapolating for negative fractional positions on periodic topographies (corrupting scale-dependent derivatives with fractional scale factors near the origin). - Nonuniform/VariableBandwidth: The magnification loop recorded a level before checking the post-subdivision point count, so the last datum could average segments below nb_grid_pts_cutoff (down to two points, which have zero detrended rms height), biasing the smallest bandwidth low. The boundary-point search tolerance is now relative to the subdivision width instead of an absolute 1e-6, which is unit-dependent. - Models/SelfAffine: generate_roughness resolved longcut_wavelength and then ignored it; it now synthesizes from the model PSD (which honors shortcut, rolloff and longcut). power_spectrum_isotropic no longer evaluates the power law at q=0. The order == Hurst branch of variance_derivative uses the same lower integration limit as the general branch. - Generation.self_affine_prefactor: Hurst = 0 with rms_height and Hurst = 1 with rms_slope hit removable 0/0 limits and silently produced all-NaN topographies; they now raise ValueError. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
- Uniform/Detrending.polyfit_line_scan: np.polyfit on a masked array silently fits the raw values stored under the mask; masked points are now excluded (like the 2D counterpart already does). - Uniform/VariableBandwidth: The checkerboard detrend built its normal equations with np.bincount, which strips masks; masked points now enter both the right-hand side and the matrix with zero weight, and retain their mask in the output. - Uniform/ScalarParameters: Rq/Sq normalized by the total number of grid points and computed means over the full grid; sums and means now use the number of defined points. (A half-masked constant profile now has zero rms height; the reference values of two tests changed - notably, the old x3p-1 reference of 9.5e-05 was almost entirely a mean-normalization artifact, the true rms of that surface is 2.4e-07.) - Uniform/GeometryAnalysis.assign_patch_numbers_profile: The periodic wrap merge assigned the id of the *second pixel* instead of the first patch; a masked patch spanning the wrap boundary came back from imputation with one pixel still masked while has_undefined_data reported False. - Uniform/Imputation: The Laplace matrix used unconditional np.roll to find neighbors, wrapping across the boundary of nonperiodic topographies and coupling edge pixels to patch unknown #0. Stencil legs crossing a nonperiodic boundary are now dropped with the diagonal reduced accordingly (natural boundary condition). - IO/FromFile (HGT): SRTM void markers (-32768) are now masked instead of entering statistics as -32768 m spikes; physical sizes are no longer fabricated from the pixel counts, so the user can (and must) provide them. - IO/SDF: The ASCII variant only replaced the literal 'BAD' token; integer invalid markers (-2^15/-2^31) are now mapped to NaN like in the binary variant. Also replaces the deprecated np.fromstring. - IO/X3P: The reader now honors the ISO 5436-2 ValidPointsLink validity file (one bit per point, LSB first) and the CZ Offset; the writer writes a validity file for undefined points and no longer casts NaN to integer for integer data types. Round trips verified for all four data types with masked data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
The tests CI workflow has been failing on master since 2026-06-19: pytest-flake8 is unmaintained and its pytest_collect_file hookimpl is incompatible with modern pytest (PluginValidationError at startup, the plugin breaks pytest merely by being installed). Linting is already covered by the dedicated flake8 workflow, so the plugin and the flake8<8 pin are removed from the test extra. Also replace the dependency on 'tiffile' - the deprecated, misspelled alias package - with 'tifffile', and update the imports in the JPK, PS and LEXT readers accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
…aders
Core classes:
- translate() crashed for 1D line scans (rolled a nonexistent second
axis, even with the default zero offset).
- __eq__ raised (broadcast error / AttributeError) instead of returning
False for mismatched grids or NotImplemented for foreign types, and
defining __eq__ without __hash__ made all topographies unhashable.
- is_reentrant crashed for line scans with fewer than two points.
Analysis:
- FourierFilteredUniformTopography's default filter_function had the
wrong arity for the default isotropic=True (and an operator-precedence
bug had it been called); error paths raised strings instead of
exceptions. topography.filter() now works out of the box.
- MirrorStichedTopography.positions() used the parent's subdomain shape
and returned arrays covering only a quarter of the mirrored domain;
subdomain properties are now consistent with the mirrored grid.
- scale_dependent_statistical_property crashed with TypeError in the 2D
single-distance threshold branch (assignment into a tuple).
- scale_dependent_slope_from_area is no longer registered for
nonuniform line scans, which lack autocorrelation_from_area
(guaranteed AttributeError).
- suggest_length_unit crashed with OverflowError for all-zero data and
nonpositive log bounds; the Angstrom ASCII round trip ('A') is now a
convertible unit; find_length_unit_in_string matches whole tokens
only ('X Axis' no longer detects Angstrom, 'Height (nm)' now detects
nm) - this feeds the XYZ reader's unit detection.
Container:
- scale_dependent_statistical_property: the docstring example crashed
(distance vs distances); scalar distances are now supported; the
inner loop no longer shadows the outer loop variable.
- read_container forwards **kwargs to the reader constructor (e.g.
datafile_keys of CEReader) instead of to container(), which accepted
no kwargs; FileExistsError replaced by FileNotFoundError (also in the
topography open path); read_published_container now checks HTTP
status, applies request_args to both requests and uses a default
timeout.
Readers:
- GWY: files containing object arrays (graphs/spectra) crashed twice
over (missing kwargs pass-through and a missing comma turning
nb_items into a tuple); channel indices with more than one digit
(11+ channels) crashed on a single-digit regex.
- FRT: block 0x00ae contained the invalid struct format 'gint32'
(copied from Gwyddion C source); files with this block failed to
open.
- NC: nonuniform line scans reported nb_grid_pts=(None,) (read from
the absent 'x' dimension instead of 'n'); the MPI guard 'rank > 1'
let ranks 0 and 1 write the same file concurrently.
- PLUX: multi-layer files advertised N channels but refused to read
any channel other than 0.
- H5: files containing HDF5 groups crashed (groups have no .shape);
the reader now walks the full hierarchy and finds nested datasets.
- DATX: converter validation used 'and' where 'or' was needed, letting
malformed converters through; fixed a copy-paste error message.
- Mitutoyo: spreadsheets without roughness-metric cells crashed with
IndexError (height unit now falls back to the lateral unit);
single-point scans crashed on diff; date parsing was locale-dependent
(now dateutil); the workbook handle is closed.
Reference values for FRT/PLUX/DATX/GWY/OPD/X3P fixtures with undefined
data points changed due to the masked-data normalization fix in the
previous commit; the FRT fixtures are ~30% masked and their old rms
references were dominated by the wrong-mean artifact.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
Metadata: - InfoModel now preserves free-form info keys (extra='allow'); the docstrings promise a free-form dictionary for third-party codes, but pydantic's default silently discarded unknown keys. Fields are properly Optional, so explicitly passing None no longer raises; ValueAndUnitModel accepts a missing unit (data without unit information). - The base-class DecoratedTopography.info passed a BaseModel to model_copy(update=...), which requires a dict (AttributeError under pydantic 2); it now merges parent and decorator info once, and the two copy-pasted overrides in the uniform/nonuniform subclasses are gone. Function registration: - register_function uses copy-on-write per-class registries with an MRO-merged lookup: functions registered on a subclass no longer leak into the shared base-class dictionary. Functions previously registered on concrete classes (interpolate_linear/bicubic, mirror_stitch, fourier_derivative, scan_with_rigid_sphere) are registered on the interfaces with explicit dimension guards, so they remain available on decorated topographies and produce clear errors on data of the wrong dimension (scan_with_rigid_sphere now also works on decorated line scans and rejects 2D maps instead of producing garbage via its periodic branch). - The deprecated= flag is honored (DeprecationWarning on call); the inactive flags on the nonuniform rms_*_from_profile aliases were dropped since the same names are primary API for uniform data. - apply() returns the function's result in both dispatch bases. - The doi decorator unwinds its bookkeeping in a finally block (an exception inside an analysis function previously corrupted the class-level state permanently, silently dumping DOIs into the caller's set forever after) and marks wrappers with __has_doi__ (the name check was defeated by functools.wraps and double-wrapped every function). Infrastructure: - ExtendedJSONEncoder overrides iterencode (json.dump bypassed encode and emitted invalid JSON), converts Inf like NaN, and nan_to_none handles nomask and multidimensional masked arrays. - Pipeline.pipeline_function no longer delegates unknown attributes to the parent topography, which silently applied chained pipeline functions to the parent and discarded the wrapped transformation. - logging.getLogger instead of detached Logger instances (Regression, Averaging, Container SDS, XYZ reader). - CEFileOpener/ZAGFileOpener close the containing ZipFile together with the member stream (file-descriptor leak until GC). - DeclarativeReaderBase supports channel_id/height_channel_index selection and no longer calls scale(None) for formats without height-scale metadata. - make_topography_from_function no longer assigns _heights directly, so constructor validation (shape check, float conversion, NaN masking) applies. Readers: - JPK: per-channel raw_metadata no longer contains the last TIFF page's metadata (stale loop variable). - ZON: the mutable default info dictionary is no longer mutated (metadata leaked into all subsequent calls, including via ZAG containers), and user-provided info takes precedence. - MetroPro: metadata fields declared little/native endian in this big-endian format are now read big-endian (heights were unaffected, but raw_metadata values and the instrument serial were garbage); dead header-size constants removed. - OPD: metadata blocks advance by their directory-declared length instead of assuming fixed sizes (silent offset shift), and a missing Wavelength block raises CorruptFile instead of NameError. - VK: physical sizes follow the pixel convention (n * pixel size). Reference values in the Wyko ASC test updated for the masked-data normalization fix (the fixture is half undefined). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
- eigen_helper.h: ArrayXl is a fixed-width int64 (long is 32-bit on Windows/LLP64, where every binding taking an ArrayXl rejected the int64 arrays produced by np.argsort, breaking bearing areas of nonuniform line scans in the Windows wheels). - All input arrays are taken as Eigen::Ref<const ...>: read-only, integer-dtype and Fortran-ordered inputs are now converted instead of rejected with TypeError (a read-only heights array previously broke t.mean() and friends for nonuniform scans). - patchfinder: periodic wrap-around uses while loops; user stencils with offsets larger than the grid dimension previously indexed out of bounds (potential heap corruption). - moments/bearing area: 64-bit triangle counters and Eigen::Index loop variables (int overflows at 32768 x 32768 maps); corrected stale storage-order comments. - stack.h: allocation failures throw std::bad_alloc instead of printing and dereferencing a null pointer; the stdout printf on every stack expansion is gone; copy construction/assignment are deleted (raw buffer, rule of three). - nonuniform_autocorrelation validates that user-supplied distances are smaller than the physical size (division by physical_size - distance previously produced silent NaNs/garbage). - module.cpp: long-running pure-Eigen kernels release the GIL (autocorrelation, patch analysis, distance maps, bearing areas); bearing-area bindings gained py::arg names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
…ns 3 and 4) Section 3 (low severity): - IO readers: correct AL3D row-stride read length and absolute-value mask tolerance; guard WSXM and DZI against flat scans (zero height range); fix ZON multi-entry stride computation and reject non-float32 entries with UnsupportedFormatFeature instead of an assert; replace defaultdict(None) and off-by-one channel bounds check in Text reader; catch IndexError (not KeyError) for out-of-range JPK channel indices; pass explicit little-endian byte order in FRT reader. - Numerics: solve the nonuniform polyfit normal equations in scan-centered coordinates (raw monomials are ill-conditioned for scans far from the origin) and transform coefficients back to the monomial basis; fix operator-direction inference for uniform derivative normalization with anisotropic pixels; correct dsinc tolerance handling and remove dead code in nonuniform power spectrum; correct Nyquist frequency for anisotropic pixels in power_spectrum_from_area; fix geometric-mean parenthesization in scanning-probe reliability analysis. - Misc: cache the communicator alongside the muFFT engine in FFTTricks; simplify TranslatedTopography offset setter; raise ValueError instead of assert in CompoundTopography; add stacklevel to deprecation warnings; numerous docstring corrections (two-sided integrals, polyfit2 coefficient order, curvature vs slope, moment prefactors, derivative trimming semantics, make_grid nb_points semantics). - C++: document the folded-in 1/2 convention in the nonuniform autocorrelation kernel (the old comment showed the product integral of a different function); remove unused iostream include; drop the never-returned nearest-point index array from distance_map; document always-periodic behavior of the patch analysis functions in their Python docstrings. Section 4 (architecture): - Add test/test_invariants.py with property-based invariant tests: transpose/translate/scale invariance of scalar parameters (with anisotropic pixels), decorator geometry consistency, bearing-area CDF properties, and origin invariance of nonuniform detrending. - meson.build: use python3 for version discovery; the bare python alias does not exist on many systems. Deliberately unchanged: the NMS height scale divisor (no reference available) and the latent DATX/LEXT non-square axis pairing (no fixture). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
…wheels pip's isolated build environment provides python.exe but no python3.exe on Windows, so 'python3' resolved to an interpreter outside the build environment that does not have the DiscoverVersion build requirement installed. All Windows wheel builds failed with "No module named DiscoverVersion". 'python' is the only interpreter name guaranteed to exist inside a virtual environment on all platforms; this is now documented in meson.build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
…t section 2.5)
The performance findings were re-assessed against the memory boundary
conditions of the library: a container streams its topographies from disk
and must never hold more than one of them in memory, and readers are
long-lived metadata handles that must not pin file data. Several of the
apparent performance penalties exist precisely because of those
constraints, so the fixes here either reduce memory use or keep it
unchanged; the two items whose speedup would cost memory are documented
as trade-offs rather than changed.
Fixed, with reduced memory use:
- Bicubic: compute the 16 spline coefficients of a cell on demand with a
single-cell cache instead of tabulating them for every pixel, which
cost 128 bytes per pixel (more than 2 GB for a 4096 x 4096 map) before
the first evaluation.
- IBW, MI: keep only metadata (and, for MI, the byte offset of the data
block) on the reader and load the data in `topography()`.
- MNT: store block positions instead of all decompressed blocks and the
raw file copy; decompress on demand.
- patchfinder: allocate the flood-fill stack once per call instead of
16 MB per patch.
- Gaussian process regression: factorize the observation covariance once
with `cho_factor` and reuse it, and compute only the variance diagonal
instead of the full predictive covariance matrix.
Fixed, memory-neutral:
- Container PSD integration: precompute the per-topography bandwidth
intervals in a single streaming pass (two scalars each, heights are
never cached) and answer counting queries with `searchsorted`. This
turns N^2 file reads into 2N, as the pre-existing TODO proposed.
Topographies whose reliability cutoff exceeds their scan length have an
empty bandwidth interval and are excluded, so that the interval
subtraction cannot count them as -1.
- MNT zlib scan: measure candidate streams with a chunked
`decompressobj` over a memoryview and skip past accepted streams,
instead of slicing the remaining buffer and fully decompressing it at
every candidate byte.
- Nonuniform height-height autocorrelation: move the double segment loop
into a C++ kernel that runs in constant additional memory rather than
vectorizing it into an O(N*M) intermediate.
- Rigid sphere scan: vectorize over blocks of scan positions sized so the
temporary stays bounded irrespective of scan length and tip radius.
- XYZ: accumulate columns into `array('d')` buffers during parsing.
Deliberately unchanged: the correlation function's pixel sweep (its FFT
alternative needs several full-map complex arrays) and the bearing area
full-grid sweep per query height (a sorted cache adds another O(N) array
and only pays off when many heights are queried). The XYZ file is still
parsed twice, because caching the columns would pin the point cloud for
the reader's lifetime.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
This plot was written while verifying the derivative normalization fix and should never have been added to the repository. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
The audit report was a working document for this branch, not something the package should ship. The findings it recorded are captured in the commit history, the change log and the regression tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR works through a full audit of the codebase, tier by tier: high-severity wrong-result bugs, medium-severity correctness and metadata issues, low-severity cleanups, and the performance findings. All fixes include regression tests. What was deliberately left unchanged, and why, is recorded in the sections below.
Fixes #282 — the
+q/-qfolding in the power spectrum halved theq = 0entry, which receives no mirrored contribution.fold_fft_halfnow halves only[1:n//2], i.e. exactly theC_all[1:] /= 2the issue asks for. The Nyquist entry is structurally excluded, since the folded output stops atn//2.1. High severity — silent data corruption / wrong results
Geometry and derivatives
TransposedUniformTopographynow swapspixel_size,nb_subdomain_grid_ptsandsubdomain_locationsDownsampledUniformTopographyreports correctpixel_sizeandarea_per_ptDecoratedUniformTopography.pixel_sizederives from the decorator's own geometry instead of the parent'sFile I/O readers
voidpixelsas a count and masks against format markersC++ extensions
_TriangleMomentand_LineScanMomentrewritten in exact polynomial form (complete homogeneous symmetric polynomials), removing sign errors, missing prefactors and division-by-zero crashesBicubic.__call__enforces C-contiguity and respects strides; scalar derivative returns match the requested orderStatistics and analysis
np.rollto_uniform()interpolates at the correct x-positions for scans whose origin is not zero_slope_detrend_coeffsuses a length-weighted slope and the matching offsetfourier_synthesiszeroes the DC mode instead of setting it toC(q=1)scale_dependent_statistical_propertydistance calculation fixed; the container variant no longer shadows its outer loop variable and accepts a scalardistancesmake_sphere(..., periodic=True)passes the periodic flag throughdirectionsemantics corrected (transpose for'x', not'y')Nonuniform.moment()handles equal adjacent heights analyticallyinterp_derxywas assignedinterp_derxxin the bicubic interpolatorfloorrather than truncation, so negative positions on periodic data interpolate instead of extrapolating2. Medium severity
3. Low severity
Reader edge cases (AL3D row stride, WSXM and DZI flat-scan guards, ZON strides and a non-float32 rejection, Text channel bounds, JPK
IndexError, FRT explicit byte order), numerical conditioning (the nonuniformpolyfitnormal equations are solved in scan-centered coordinates, so detrending no longer depends on where the scan sits on the x-axis), a corrected Nyquist frequency for anisotropic pixels,stacklevelon deprecation warnings, and a batch of docstring corrections (two-sided integrals,polyfit2coefficient order, moment prefactors, derivative trimming semantics).Two items were left deliberately unchanged: the NMS height-scale divisor (
2**16 - 2vs2**16 - 1), because no reference implementation or fixture can decide it, and the latent non-square axis pairing in DATX/LEXT, which cannot be proven without a non-square fixture.4. Architecture
test/test_invariants.pyadds property-based invariant tests: transpose/translate/scale invariance of scalar parameters with anisotropic pixels, decorator geometry consistency (pixel_size == physical_sizes / nb_grid_ptsfor every decorator), bearing-area CDF properties, and origin invariance of nonuniform detrending.5. Performance, under the library's memory constraints
The performance findings were re-assessed against the memory boundary conditions of the library: a container streams its topographies from disk and must never hold more than one of them in memory, and readers are long-lived metadata handles that must not pin file data. Several of the apparent performance penalties exist precisely because of those constraints, so these changes either reduce memory use or leave it unchanged.
Fixed, and memory use goes down:
__init__.Fixed, memory-neutral:
searchsorted, turning N² file reads into 2N as the pre-existing TODO proposed.decompressobjover a memoryview and skips past accepted streams, instead of slicing the remaining buffer and fully decompressing it at every candidate byte.Deliberately unchanged, because the speedup would cost memory: the correlation function's pixel sweep (its FFT alternative needs several full-map complex arrays, ~1 GB at 4096², versus O(1) today) and the bearing-area full-grid sweep per query height (a sorted cache adds another O(N) array and only pays off when many heights are queried).
Related issues not closed by this PR
n1_ * n2_is still anint × intproduct incpp/bicubic.h, which overflows at and beyond 46341², including the ~65000² case in the issue body. That needs a wider index type throughout and is out of scope here.Testing
Full suite: 2375 passed, 6 skipped locally.
flake8 SurfaceTopography testis clean. Every numerical change was checked against the previous implementation directly, not only through the test suite — the new ACF kernel matches the old Python loop to 6e-17, the chunked rigid-sphere scan is bit-identical across four tip radii on uniform and nonuniform scans, GP mean and variance match to 1e-11 and 1e-15, and the MI reader returns identical heights via path, open stream, repeated calls and a stream handed over at a nonzero offset.The three tests that require downloading from contact.engineering are excluded locally (sandbox has no network access to that host) and are covered by CI.
https://claude.ai/code/session_01TPEpAGDASUXBVpV5BVDnUZ