Skip to content

Rewrite the binding docstrings in the NumPy style - #58

Merged
moble merged 31 commits into
mainfrom
bugfixes
Oct 28, 2026
Merged

moble merged 31 commits into
mainfrom
bugfixes

Conversation

@moble

@moble moble commented Oct 28, 2026

Copy link
Copy Markdown
Owner

The docstrings were converted mechanically from Doxygen output, with C++
types and a layout that no documentation tool parses. They are now
NumPy-style docstrings with Python types and argument names, and the
roughly 150 functions and methods that had none now have one. Each
overloaded function has a single docstring describing all of its forms,
attached to its last overload, and unnamed arguments now have names.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

moble and others added 30 commits October 27, 2026 21:47
The docstrings were converted mechanically from Doxygen output, with C++
types and a layout that no documentation tool parses.  They are now
NumPy-style docstrings with Python types and argument names, and the
roughly 150 functions and methods that had none now have one.  Each
overloaded function has a single docstring describing all of its forms,
attached to its last overload, and unnamed arguments now have names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The functions defined in Python, in the package itself, in the plotting
module, and in the extrapolation module, now have NumPy-style docstrings
like those of the compiled module, so that they render alike in the
documentation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RunExtrapolation still called the Python 2 builtin `file`, so it raised
a NameError on Python 3 before running anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The documentation is built with Sphinx, the Furo theme, and MyST-NB.
The API reference is generated from the docstrings, with a small
extension that gives each pybind11 overload its own signature.  The old
IPython notebook that accompanied the paper is ported to a MyST notebook
for current GWFrames and Python 3, which is executed on every build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Docs workflow builds the documentation on every push and pull
request, treating warnings as errors, and deploys it to GitHub Pages
from main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A badge and a link near the top of the README point to the documentation,
the usage section points to the tutorial and API reference, and the
development section explains how to build it.  The installation and
change sections are marked so that the documentation can include them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DominantPrincipalValue freed its eigen-system workspace inside the loop over
matrices, so every matrix after the first was diagonalized with freed memory,
and the workspace was freed again on each pass; with two or more matrices the
process aborted.  The workspace is now freed once, after the loop, as in
SubordinatePrincipalValue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Matrix::clear freed its GSL matrix without forgetting it, so the destructor freed it a second time, and every query of an empty Matrix dereferenced a null pointer.  Now clear resets the pointer, an empty Matrix (with zero rows or zero columns) is always represented by a null pointer and reports zero rows and zero columns, and the constructors, assignments, and resize allocate nothing for an empty shape.  Copy assignment also uses copy-and-swap, so self-assignment no longer reads freed memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loops over m that load the mode data of the two factors into the work
arrays ran only from m = -ell to m = -ell, so each factor was represented by
one coefficient per ell, stored at the wrong indices.  The loops now run over
every m from -ell to ell, as the rest of the function assumes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pointwise quotient shared its code with the product, which set the spin
weight of the result to the sum of the spin weights of the inputs.  For a
quotient that is wrong, and the wrong spin weight also made the transform
back to modes produce wrong data.  The quotient now has the difference of the
spin weights, and the Waveform docstring describes the spin weight, the modes,
and the accuracy of the result of both operators.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new tests multiply and divide by constant waveforms, compare products of
spin-weighted fields with the products of their values at points on the
sphere, and check that a quotient undoes a product.  They also check the spin
weight and the modes of each result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With AllowTimesOutsideCurrentDomain, the splines were evaluated at the wrong
entries of the times inside the domain whenever some new times preceded the
data, and the searches for the domain boundaries could read past the end of
NewTime when every new time was outside it.  The index is now offset, the
searches are bounded, the frame outside the domain is held at the first or last
rotor of the original frame, and the boost weight and version history are now
copied to the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The upper bound of the coarse search over time shifts in AlignWaveforms was computed from the number of time steps of B, as if it were a time.  When the times of B were large compared to the number of steps, the search window was empty and the alignment failed, and in other cases the window could extend past the end of the data of B.  The bound now uses the second-to-last time of B, matching the margin of the lower bound, and the docstring describes the search range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Waveform.Compare called Intersection with its default arguments, so the
MinTimeStep and MinTime arguments of Compare were silently ignored.  They
are now passed on to Intersection, and the docstring describes them as
working parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DropTimesOutside used huntRight to find the first time to keep, so it always dropped the first time step and any time equal to ta, kept the first time step when tb preceded all the data, and corrupted memory when tb was less than ta.  The bounds are now found with std::lower_bound and std::upper_bound, so exactly the times with ta <= t <= tb remain, and an empty interval removes every time step.  The docstring and new regression tests describe the corrected behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Waveform::FindModeIndexWithoutError read lm[i_expected] without checking that the index was in range, and NormalizedAntisymmetry and BinaryOp then indexed the data with the invalid index it returns for a missing mode, which crashed or read garbage.  The lookup now checks the bound first, as FindModeIndex does, and its callers treat a missing mode, such as the (ell,-m) partner of a mode that is present, as zero.  The NormalizedAntisymmetry docstring now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a Waveform had no frame rotors, as any Waveform built directly from
times, modes, and data does, Frame(TimeIndex) read the first element of the
empty frame vector, which crashed Python.  An empty frame is the identity, so
the identity rotor is now returned, and the docstring says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SliceOfTimeIndicesWithoutModes copied frame data only when there was one rotor per time step, so a constant (single-rotor) frame was silently dropped, and it also skipped the index checks of the other slicing functions.  It now treats frames exactly as SliceOfTimeIndices and SliceOfTimeIndicesWithEll2 do: it slices a time-dependent frame, copies a constant frame, and rejects any other frame size and any impossible pair of indices.  The docstrings no longer describe the constant frame as dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Modes.bar conjugated each coefficient in place, which does not give the
modes of the complex-conjugate function, so the supermomentum computed
from sigma-bar was wrong.  It now returns (-1)^(s+m) times the conjugate
of the (ell, -m) coefficient, which agrees with conjugating the function
on a grid.  The docstring and the SWIG reference comparison are updated
to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The symmetric and antisymmetric parts under x, y, z, and full parity applied
the same combination (R +/- P R)/2 to the frame rotors as to the modes, which
gave non-unit rotors and, for the full-parity antisymmetric part, an all-zero
frame.  The modes are combined index by index, as in a sum of waveforms, so the
result now keeps the frame of the original waveform, and the docstrings say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DataGrid::pow and Modes::pow kept the spin weight of their input, although a product of grids adds spin weights, so the p-th power of a spin-s function has spin weight p*s.  For Modes this was more than a wrong label, because the powered grid was transformed back to modes with the wrong spin weight.  DataGrid::pow now sets the spin weight to p*s, which Modes::pow inherits, and the docstrings say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pointwise products and quotients of waveforms truncated both inputs to the
smaller of their largest ell values and used a grid that resolved only that
band limit, so the higher modes of a product were aliased into the returned
coefficients.  Every mode of each input is now transformed, and the grid
resolves the sum of the largest ell values of the inputs, so the returned
modes of a product are exact; the returned set of modes is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Waveform::InterpolateInPlace wrote the waveform's whole history back into its own history stream, so each call doubled the history.  It now appends only the single line "this->InterpolateInPlace(NewTime);", as the other in-place methods do.  The docstring says so, and new regression tests check the history and data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dividing a number by a DataGrid of spin weight s returned a grid labeled with
spin weight s, although the quotient has spin weight -s, as DataGrid.pow(-1)
and grid-by-grid division already give.  The quotient now has spin weight -s.
The DataGrid docstring now states the spin weight of each scalar operation, and
notes that sums and differences, with a number or another grid, are meaningful
only for matching spin weights but are not checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Modes::pow sized its grid as 2*EllMax()*p+1, which DataGrid enlarged to 2*EllMax()+1 for p <= 0, so negative powers, which are not band-limited, suffered aliasing errors as large as the first omitted modes.  The grid now has 2*EllMax()*abs(p)+1 points in each direction, as for the power abs(p), and negative powers are still truncated at the original EllMax().  Non-negative powers, which are exact, are unchanged, and the docstring describes both cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both overloads of Modes::EvaluateAtPoint summed over every stored mode from
ell = 0, so for any nonzero spin weight they asked for a Wigner D element with
ell < |s| and raised an error.  The sums now start at ell = |s|, skipping the
modes that vanish for a spin-weighted function, which also makes the boosted
DataGrid constructor usable for spin-weighted data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The caster for vectors of vectors located each row with numpy's bounds-checked
`data(i, 0)`, which raises an IndexError when the rows have length zero, so
`Matrix([[]])` and any argument or result with rows but no columns failed to
convert.  The caster now offsets the base pointer of the contiguous array by
`i * cols` instead, in both directions, which leaves non-empty conversions
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spin-0 data with only the ell=0 mode were put on a 1x1 grid, which
spinsfast cannot handle, so products of such Waveforms, and products and
powers of such Modes, crashed.  The grids for pointwise operations are
now never smaller than 3x3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Matrix(DataIn) took the number of columns from the first row and read
every row to that length, so a shorter row was read past its end.  It
now raises ValueError unless all rows have the same length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SliceModes.MoreschiIteration was never finished: it partly overwrote one
of its arguments and then always raised an error.  It is removed from
the C++ code, the bindings, and the documentation.
SuperMomenta.MoreschiIteration, which works, is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DataGrid + DataGrid and DataGrid - DataGrid did not check that the spin
weights agree, unlike the same operators on Modes; they now raise
ValueError when the spin weights differ.  A number can now also appear on
the right of a DataGrid in all four operators, where before only a number
on the left was bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@moble
moble merged commit 8b53e59 into main Oct 28, 2026
16 checks passed
@moble
moble deleted the bugfixes branch October 28, 2026 02:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant