Releases: michaelaye/isistools
v0.10.0 — csm2map self-contained subpackage
[0.10.0] - 2026-04-14
BREAKING
- csm2map is now a standalone CLI:
csm2map project input.cub output.tif
at the shell, notisistools csm2map .... The oldisistools csm2map
andisistools csm2map-comparesubcommands are removed. The compare
command is nowcsm2map compare. processing/andgeo/packages deleted: all csm2map code moved
tosrc/isistools/csm2map/. Import paths changed:
from isistools.processing.project import project→
from isistools.csm2map import csm2map.project()renamed tocsm2map(): one name for CLI, Python API,
docs, paper.
Added
isistools.csm2mapsubpackage: self-contained, paper-reviewable,
extractable as a standalone product. Clean__init__.pypublic API:
csm2map(),TargetBody,OutputGrid,CoordinateMap,
Interpolation.csm2map/compare.py: comparison logic extracted from CLI into a
library function that returns a dict of statistics. Usable from
notebooks and scripts, not just the CLI.csm2map/cli.py: standalone Typer app withprojectand
comparesubcommands, registered as thecsm2mapentry point in
pyproject.toml.- Standalone
csm2mapentry point inpyproject.toml:
csm2map = "isistools.csm2map.cli:app".
Changed
project.pyrenamed topipeline.py: the file name now describes
what it is (the pipeline orchestrator) rather than what it was
historically named. The public function iscsm2map().geo/projections.pyabsorbed intocsm2map/projections.py: it
had zero callers outside csm2map. No other isistools code is affected.- Main
isistoolsCLI slimmed: csm2map commands removed (~224 lines).
Only viewer/footprint/cnet commands remain. - CLAUDE.md architecture section rewritten to document the new
csm2map subpackage layout.
Extraction path
To ship csm2map as pip install csm2map in the future: copy
src/isistools/csm2map/ to a new repo, vendor read_label() +
read_isis_cube_raw() from isistools.io.cubes (the only shared
deps), add a standalone pyproject.toml. The shared surface is
intentionally just 2 functions + 1 optional (read_footprint).
Tests
- 64 passed, 1 skipped. Csm2map CLI tests updated for the standalone
app. Two csm2map entries removed from isistools CLI parametrized
test; 4 new standalone csm2map CLI tests added.
v0.9.0 — zero-flags csm2map, PVL sidecar, antimeridian fix
[0.9.0] - 2026-04-12
Zero-flags workflow, ISIS-compatible metadata sidecar, and longitude
wraparound fix. csm2map can now be invoked with just an input and
output path — no MAP file, no -r flag, no --minlat/--maxlat
needed. Everything is derived from the cube's own camera model and
ALE ISD.
Added
- Zero-flags workflow:
isistools csm2map input.cub output.tif
now works with no other arguments. Resolution is auto-computed from
the camera model's ground sample distance at the image center
(matching ISIScam2map's default behavior), bounds are derived
from the camera footprint, projection is centered on the image,
shape model and body info come from the cube label and ALE ISD. - Auto-resolution from camera GSD (
compute_ground_sample_distance
incamera.py): evaluatesimageToGroundat the center pixel and
its two neighbors (+1 line, +1 sample), computes the ECEF distance,
and returns the average of the line-direction and sample-direction
GSDs. Verified on F05 CTX: 5.74 m/px vs ISIScamrange's 5.3–5.4
m/px range (ours is at image center; ISIS reports best-case). - ISIS-compatible Mapping PVL sidecar (
write_mapping_pvlin
writers.py): every GeoTIFF output is now accompanied by a.pvl
file containing the same metadata ISIScam2mapwrites into a
projected cube's Mapping group: projection name, body radii,
lat/lon type and direction, ground range, pixel resolution, and
UpperLeftCornerX/Y. This makes the GeoTIFF interoperable with
ISIS workflows and gives users transitioning fromcam2mapa
familiar metadata format. The sidecar also serves as a MAP file
for subsequent csm2map runs on overlapping cubes — it carries the
exact grid origin, so multiple cubes projected with the same
sidecar share a pixel-aligned grid (solving the grid-snap issue
documented earlier in this session). -hhelp shortcut alongside--helpfor all CLI commands via
Typer'scontext_settings.
Fixed
- Auto-projection now centers on the image, not on (0°, 0°).
When no MAP file and no--projectionflag are given, the
equirectangular CRS uses the image's center latitude forlat_ts
(isotropic local pixel scale) and center longitude forlon_0
(small map coordinates). Previouslylat_ts=0, lon_0=0put the
projection origin up to 180° away from the data — massive
distortion and unnecessary numerical range. - Antimeridian crossing in
_derive_ground_range. MRO is a
polar orbiter; CTX routinely images strips that cross the ±180°
boundary. The old code didmin(lons)/max(lons)on raw
arctan2output, producing a 358° grid for a 2° strip. Fixed
via circular statistics: the circular mean of all probe longitudes
is computed, then offsets are measured in wrapped [-180, +180]
space. 6 new regression tests cover antimeridian crossing (sparse- dense), prime meridian crossing, near-pole scattered longitudes,
and an explicit old-bug-would-fail assertion.
- dense), prime meridian crossing, near-pole scattered longitudes,
- PVL sidecar longitude normalization:
MinimumLongitude,
MaximumLongitude, andCenterLongitudeare normalized to the
declared [0, 360) domain via% 360. Previously
_derive_ground_rangecould return values likelon_max=181.5
for an antimeridian-crossing strip, which would violate the
LongitudeDomain = 360declaration in the PVL.
Changed
--resolution/-ris now optional even without a MAP file.
If omitted, auto-computed from the camera GSD (see "Added" above).
The old behavior was to raise"Must specify resolution or use a MAP file".
Performance measurements
The auto-resolution path adds one extra imageToGround call (at the
image center ± 1 pixel) — negligible cost (~0.1 ms). No change to
the projection pipeline itself. F05 wall time is unchanged vs 0.8.1.
Tests
- 6 new longitude-wraparound tests in
test_latlon_conventions.py. - Test count: 56 → 62.
v0.8.1 — fix silent Planetographic latitude bug + refresh GPU plan
[0.8.1] - 2026-04-12
Bug fix + roadmap refresh. Two items land together because they were
identified in the same post-0.8.0 fresh-view code review and both
inform near-term planning decisions.
Fixed
-
Silent Planetographic latitude bug in ISIS MAP files.
grid_from_map_file
previously ignored theLatitudeType,LongitudeDirection, and
LongitudeDomainkeywords in ISIS MAP files: any file that specified
LatitudeType = Planetographicwas silently treated as planetocentric,
producing a ~0.3° latitude shift on Mars at mid-latitudes — a ~17–20 km
ground-location error with no warning.grid_from_map_filenow reads these keywords and converts the
MinimumLatitude/MaximumLatitude/MinimumLongitude/
MaximumLongitudevalues to csm2map's internal convention
(Planetocentric / PositiveEast / 360° domain) using the body's own
EquatorialRadius/PolarRadiusfrom the same Mapping group. When
a conversion happens aUserWarningis emitted so the conversion is
visible in the user's output.Applies to both the explicit
MinimumLatitude/MaximumLatitude
code path and theUpperLeftCornerX/UpperLeftCornerY
code path. Longitude ordering (swapped min/max after a positive-west
flip) is handled correctly.The bug was flagged as a latent risk in
docs/csm2map-design.md§6
of 0.7.0; that §6 entry is now closed and this release's fix is the
reference implementation.The fix is bit-for-bit behavior-preserving for the common case
(Planetocentric / PositiveEast / 360° — which is what csm2map itself
writes in its own MAP files and what the F05 benchmark harness
uses). The F05 regression output is byte-for-byte identical to
0.8.0. Only MAP files using non-default conventions are affected.
Added
-
Pure-geometry conversion helpers in
isistools.geo.projections:planetographic_to_planetocentric(lat_deg, eq_radius, polar_radius)planetocentric_to_planetographic(lat_deg, eq_radius, polar_radius)normalize_longitude(lon_deg, *, direction=..., domain=...)normalize_latitude_from_mapping(lat_deg, mapping, eq, polar)normalize_longitude_from_mapping(lon_deg, mapping)
All helpers are pure Python/numpy, handle both scalar and
vectorized inputs, short-circuit at the poles, and are identity
on spherical bodies. No SPICE dependency. -
tests/test_latlon_conventions.py— 22 new regression tests:- Unit-level: sphere identity, equator/poles edge cases, Mars 45°
known value, scalar/vectorized round-trip tests. - Mapping-level: default-is-planetocentric, explicit-planetocentric
no-op, Planetographic converts, junkLatitudeTyperejected. - Integration-level: the same physical Mars ground patch
described in two different conventions (Planetocentric vs
Planetographic, PositiveEast vs PositiveWest) now produces
identical csm2map-internal grids — the explicit regression
test for the bug being fixed. - Silent-path test: a MAP file in the default convention must
NOT emit any conversion warning.
Test count: 34 → 56.
- Unit-level: sphere identity, equator/poles edge cases, Mars 45°
Documentation
docs/plans/gpu-acceleration.mdrewritten with a current baseline.
The plan now lives indocs/plans/(tracked in git) instead of the
Plans/scratch directory, so it persists across releases and is
discoverable in the repo.
The 0.7.0 draft had stale numbers (11 s total, 3.5 s CSM, 3.0 s
resample) and recommended "thread the CSM loop" as a quick win
that is now known to be a no-op on csmapi (SWIG GIL-held). The
revised plan:- Replaces the baseline with the 0.8.0 F05 warm-cache re-profile
(44.6 s total, 12.2 s coord_transform = 27.3%, 28.7 s resample
= 64.3%, everything else < 5%). - Retires the CSM-threading quick-win section; notes that the
scaffolding is retained in case upstream csmapi releases the
GIL in a future version. - Rerecommends torch MPS
grid_sampleas the single GPU backend
(drops cupy because it has no Apple Silicon support), targeting
resample at 28.7 s → ~3 s for a ~2.4× overall speedup ceiling. - Explicitly prioritizes the hybrid-pattern-prototype prerequisites
(CSM state load/save, 1 day) AHEAD OF GPU work because unlocking
a new capability has higher ROI than 2× speedup on existing one. - Adds open questions about JANUS workload characteristics —
framing camera at 2000×2000 may already be CPU-fast enough,
profile it before committing to GPU engineering.
- Replaces the baseline with the 0.8.0 F05 warm-cache re-profile
Performance measurements
Added to the plan: a freshly-measured 3-run F05 profile on 0.8.0
(warm cache, averaged, discarding the first cold run). Absolute
numbers:
| Stage | Time | % |
|---|---|---|
| load_camera | 1.28 s | 2.9% |
| dem_open | 0.13 s | 0.3% |
| build_grid | 0.01 s | 0.0% |
| coord_transform | 12.2 s | 27.3% |
| read_input | 0.71 s | 1.6% |
| resample | 28.7 s | 64.3% |
| write_output | 1.50 s | 3.4% |
| total | 44.6 s |
Resample at 64% of wall time makes GPU resample the single largest
optimization opportunity; see the revised plan for the follow-through.
v0.8.0 — body-agnostic csm2map (JANUS/LRO/Europa enablement)
[0.8.0] - 2026-04-12
Body-agnostic refactor. Before 0.8.0, csm2map silently assumed Mars —
a JANUS, LRO, or Europa Clipper cube would have been projected with
Mars radii and the user would have had no way to notice. 0.8.0 pulls
the target body's ellipsoid directly from ALE's ISD and threads it
through the pipeline with zero hardcoded literals.
Output on Mars inputs is bit-for-bit identical to 0.7.1. Verified
end-to-end on a full-length F05 CTX cube (557M pixels): cmp -s on the
two GeoTIFFs returns success.
BREAKING
isistools.processing.camera.load_camera()now returns a tuple
(csmapi.RasterGM, TargetBody)instead of just the model. Any code
that unpacksload_camera(cube)must be updated. The CLI is
unaffected. Onlyisistools.processing.project.project()called
this symbol directly, and it has been updated to match.isistools.processing.dem.DemRadiusSampler.__init__'s
fallback_radiusis now a required keyword argument — the old
Mars-specific default (3389526.7) is gone. Callers must pass the
target body's mean radius explicitly.isistools.geo.projections.mapping_to_crs()raises
ValueErrorwhen the Mapping group lacksEquatorialRadius.
Previous versions silently defaulted to Mars radii, which silently
mis-projected any non-Mars Mapping group. If you have a MAP file
without explicit radii, add them.isistools.processing.camera.get_target_radii()has been
removed. Its body-specific information is now carried on the
TargetBodyreturned fromload_camera(). There was no public
caller ofget_target_radiioutside the csm2map pipeline itself.isistools.processing.grid.grid_from_map_file()raises
ValueErrorwhen a MAP file usesScale(pixels/degree) but
lacksEquatorialRadius. Scale→resolution conversion needs the
body's equatorial radius and previously silently used Mars.
Added
isistools.processing.camera.TargetBody(new public
dataclass): frozen dataclass describing a target body's
ellipsoid and identity:name(str, e.g."MARS","EUROPA")naif_id(int, e.g. 499, 502, 301)radius_equatorial_m,radius_polar_m,radius_mean_m
(all in meters)
Built via theTargetBody.from_isd(isd_dict, target_name=...)
classmethod, which parses ALE's native ISD format and converts
km → m automatically. Includes a cross-check that validates the
ISD's top-levelradiidict against
naif_keywords.BODY<code>_RADIIand raisesValueErrorif they
disagree by more than 1 meter (catches stale SPICE blobs and
corrupted cubes).
- csm2map now prints the target body on startup — the output
log now showsTarget: <NAME> (NAIF <id>) radii eq=... polar=...
so the user can confirm the right body is being used. No more
silent Mars assumption. tests/test_target_body.py(12 new regression tests): covers
Mars / Moon / Europa / a hypotheticalBODY999through
TargetBody.from_isd, unit conversion (km vs m), the
ISD-vs-BODY_RADII cross-check, name uppercasing, and the
mapping_to_crshardening. Test count: 22 → 34.
Changed
load_camera()reads and caches the ISD JSON once — the
same string is both handed tocsmapi.Isd()(via the JSON file
on disk) and parsed to a Python dict (forTargetBody). No
duplicate ALE calls, no second SPICE query.project.project()pipeline simplified: the old separate
get_target_radii()stage is gone. TheTargetBodycomes out
ofload_camera()already populated, so thetarget_radii
timing stage has been removed from--profileoutput._build_grid()constructs its default projection string at
runtime frombody.radius_equatorial_m/body.radius_polar_m
instead of the hardcoded Mars ellipsoid. When no MAP file and no
explicit--projectionflag are given, csm2map now picks the
correct body automatically.
Fixed
- Hardcoded Mars radii removed from four sites:
processing/camera.py::get_target_radii(deleted entirely)processing/project.py::_build_griddefault projection stringprocessing/dem.py::DemRadiusSampler.fallback_radiusdefaultgeo/projections.py::mapping_to_crssilent defaultprocessing/grid.py::grid_from_map_fileScale-handling
branch fallback
A repo-wide grep forBODY499,3396190,3376200,3389526
insrc/now returns zero matches except for one comment in
grid.pydocumenting the previous behavior for future readers.
Validation
- F05 CTX bit-identical regression: the 0.8.0 output for the
F05 full-length CTX cube (1.0 GB input, 557M-pixel output) is
byte-for-byte identical to the 0.7.1 reference output. Verified
withcmp -s. The body-agnostic refactor has zero behavior
change on Mars data, which is the only target csm2map has ever
been run against.
v0.7.1 — doc patch: --clip-to-footprint framing
[0.7.1] - 2026-04-12
Documentation-only patch release. No code behavior changes — all
csm2map pipeline outputs are bit-identical to 0.7.0.
Documentation
- Corrected
--clip-to-footprintdocumentation. The flag was
originally documented as an "ISIS cam2map compatibility mode" that
matchedcam2mapby clipping csm2map output to thefootprintinit
polygon. That framing was based on a working hypothesis later
disproved empirically: ISIScam2mapignores the polygon entirely
(stripping the polygon from the cube and re-runningcam2map
produces bit-identical output — seedocs/csm2map.qmd § "The footprintinit polygon precision story"for the full narrative).
All affected documentation has been rewritten to drop the
cam2map-matching claim and reframe the flag as an escape hatch for
downstream tooling that explicitly wants a polygon-shaped output
mask. Affected surfaces:isistools csm2map --help— Typer option help text and
command docstring no longer claim ISIS-compatibility.docs/csm2map.qmd— Purpose paragraph, Usage example comment,
options table row, and the former "Pixel-perfect match"
paragraph rewritten. Cross-references the empirical-disproof
section.docs/csm2map-design.md§6 Known limitations gains a new
entry 7 documenting the flag as a historical
hypothesis-test-reject artifact, flagged as paper-worthy.src/isistools/processing/project.py—clip_to_footprint
parameter docstring, the in-function console message, and
the_rasterize_footprinthelper docstring all corrected.
Known limitations (unchanged from 0.7.0)
The --clip-to-footprint flag itself is retained in 0.7.1 for
backward compatibility. Deprecation or removal is being considered
for a future release.
v0.7.0 — csm2map CSM-based cam2map replacement
[0.7.0] - 2026-04-12
Added
csm2mapCLI command: CSM-based replacement for ISIScam2map.
Map-projects an ISIS cube into a GeoTIFF using the Community Sensor
Model (viaale+usgscsm+csmapi) instead of ISIS's CSPICE camera.
Validated against ISIS 9.0.0 at 99.95% coverage match and 100% agreement
within 0.01 DN on CTX; runs 5–13× faster than ISIScam2mapdepending
on cube length. Optional feature gated behind the[csm]extra — base
isistools users pay no new dependency cost.csm2map-compareCLI command: numeric validation of a csm2map
GeoTIFF against an ISIS cam2map reference cube.- Reads an existing DEM as a shape model during projection
(--shape-model auto|ellipsoid|<path>): csm2map consumes a DEM cube
(e.g. the MOLA radius DEM for Mars) to look up per-pixel body radii when
back-projecting through the CSM sensor.autoreads the input cube's
Kernels.ShapeModeland opens the same DEM ISIScam2mapwould use,
matching ISIS's default behavior. This is a read path — csm2map uses
a DEM, it does not produce one. - Jigsaw-aware SPICE source (
--spice-source isis|naif|auto, default
isis): reads SPICE pointing/position from the cube's embedded blobs
instead of the live NAIF kernels, which is the only correct choice after
jigsaw update=truesince jigsaw updates the blobs but NOT the live
kernels.naifandautoare available for comparisons against
pre-jigsaw geometry. - Stage-timing profiler (
--profile): per-stage wall time breakdown
(camera load, DEM open, coord transform, read input, resample, write). docs/csm2map.qmd: full user chapter covering purpose, installation
(including the Apple Siliconcsmapibuild-from-source gotcha),
usage, options table, pipeline description, and ISIS-compatibility
validation.docs/csm2map-design.md: design document capturing the resolution
decisions, validation methodology, and performance trade-offs (for
citation in an upcoming paper).scripts/benchmark_csm2map.sh: parameterized benchmark harness that
runscam2mapandcsm2mapback-to-back on any CTX cube and emits a
speedup summary. Usespvl.loads()oncamrangeoutput instead of
brittle grep/awk parsing.- Processing layer (
src/isistools/processing/): new subpackage
containing the csm2map pipeline —camera.py(CSM model loader),
grid.py(output raster grid from MAP file or params),transform.py
(coarse-grid coordinate map with vectorized+threaded bilinear upsample),
resample.py(threadedscipy.ndimage.map_coordinates),writers.py
(ZSTD GeoTIFF writer),project.py(pipeline orchestrator),
dem.py(lazy windowed DEM radius sampler). - PyPI classifiers in
pyproject.toml: license, OS, Python versions
3.10–3.13, Scientific/Astronomy, GIS, Image Processing topics. [tool.pytest.ini_options]with a registeredslowmarker.tests/test_cli.py: smoke tests for every registered CLI command's
--help, plus a regression test for theoverlaps --pngNameError bug
(item 3 below).
Changed
- CLI renamed
cam2map→csm2mapto avoid confusion with the ISIS
command and to make clear that this is the CSM-based pipeline. - Default coarse step 16 → 32 in
compute_transform_coarse. Validation
against the dense path shows sub-pixel accuracy at step=32 on CTX,
roughly halving the CSM-call count with no observable quality cost. - ZSTD-compressed tiled GeoTIFF output, written with multi-threaded
encoding (num_threads=ALL_CPUS). - Float32 throughout the resample pipeline — coordinate maps,
interpolated coordinates, and output pixels are allfloat32, halving
the memory bandwidth vs the previousfloat64path. - Bilinear coordinate upsample rewritten: vectorized + thread-striped
_bilinear_upsample_pair()replacingscipy.ndimage.zoom, amortizing
the per-row index math across the line and sample channels. Combined
with the other CPU optimizations above, this delivered a 3× end-to-end
speedup vs the 0.6.0 prototype. - Threaded resample:
scipy.ndimage.map_coordinatesreleases the GIL,
so the per-band resample is now split into horizontal stripes processed
by aThreadPoolExecutor. ~2.5× speedup on the resample stage alone. - Fast PVL label parser (
read_label(fast=True), default): reads the
first 1 MB of the cube and parses it withpvl.loads()instead of
seeking through the whole file. Cut DEM label parsing from 1254 ms to
15 ms. - Removed
knotendependency: the CSM model is now constructed via
csmapi.Isd()+plugin.constructModelFromISD()directly, avoiding
knoten's broken conda packaging.
Fixed
- ISIS SIGSEGV under restrictive sandbox:
campt/cam2map/camrange
and anything else that constructs an ISISCubeManagercrashed with
signal 11 whenRLIMIT_NOFILEwas set toINT64_MAX, because
CubeManager::p_maxOpenFiles = rlim_cur * 0.60overflowed to a garbage
value that corrupted the open-cube cache.scripts/and the benchmark
now setulimit -n 4096up-front. Seescripts/isis_sandbox_fix.md
for the full diagnosis. overlaps --pngcrashed withNameError(cli.py:395):
geopandas(aliased asgpd) was referenced inside the PNG plot
branch but never imported. Ships in 0.6.0 and earlier. Fixed by adding
import geopandas as gpdnext to the lazy matplotlib imports.
Regression test intests/test_cli.pyexercises the PNG branch with
mocked ISIS/geopandas data and asserts exit code 0.- Empty
archiveandclock_lookupdead variables removed from
io/cubes.py:get_serial_number()and
apps/mosaic_review.py:_on_image_selected(). Both were leftovers from
superseded code paths; the enclosing functions already worked
correctly without them. - Ambiguous variable name
linplotting/cnet_overlay.pyrenamed
tolinefor readability.
Documentation
- Research note (
scripts/csm_research.md): CSM governance, the
verification methodology behind the NGA standard, and a roadmap for
adding JANUS (ESA JUICE) to the supported sensor set. - Polygon-dependence investigation (
docs/disagreement_analysis.md):
empirically verified that ISIScam2mapdoes NOT use the footprint
polygon stored byfootprintinit. Two experiments (stripped polygon
and tight polygon) produced bit-identical outputs, refuting the
initial hypothesis that the ~870K-pixel coverage gap was polygon-based. - Residual disagreement analysis: the remaining ~18K-pixel difference
between csm2map and ISIScam2map(after DEM integration) is traced to
a CSM-vs-CSPICE camera-model floor, not a csm2map bug. Documented in
docs/csm2map-design.md§6. - CLAUDE.md: documents the
py312/isistwo-env setup, the
processing layer architecture, and the csm2map command.
Known limitations
get_target_radii()currently hardcodes Mars NAIF ID 499. Needs
generalization for non-Mars targets before JANUS / non-Mars support.csm2map-comparecan produce a 1-row shape mismatch on very long CTX
strips (e.g. F05, 52,212 vs 52,207 rows) from differing half-pixel
handling in the lat/lon → pixel rounding. Affects only the comparison
tool, not the projected output itself.
v0.6.0
New Features
isistools spiceinit: batch-run ISIS spiceinit on all cubes in a list file with parallel execution (-jflag). Web kernel retrieval enabled by default.isistools overlaps: run findimageoverlaps and parse output WKB polygons into a GeoDataFrame. Prints summary table, supports--pngvisualization and--gpkgexport.- New
isistools.io.overlaps.parse_overlap_list()for programmatic access to overlap polygons.
See CHANGELOG for details.
v0.5.3
Fixed
- HoloViews shared-axis linking between map and image plots caused axis conflicts — added
linked_axes=False,shared_axes=False, and renamed image dims from x/y to sample/line.
Changed
- Cnet point styles: smaller markers (size 3), cross markers for registered/unregistered, red circle for ignored points (QA flag).
v0.5.2
Changed
- Streamlined README:
pip install isistoolsas primary install, detailed
API docs moved to documentation site, added PyPI badge and docs link.
Full Changelog: v0.5.1...v0.5.2
v0.5.1
Fixed
- Inline code invisible in dark mode (superhero theme) — added CSS override.
- README rendered Quarto YAML frontmatter as raw text on GitHub — removed it.
Added
- Zenodo DOI badge in README.
- Sandstone (light) / Superhero (dark) theme switching in docs.
Full Changelog: v0.5.0...v0.5.1