Changelog¶
Notable changes, newest first, in the Keep a
Changelog format. The versioning policy
-- and why this project is deliberately pre-1.0 -- is in AGENTS.md.
Unreleased¶
Added¶
sources.read_catalogcaches the parsed catalog, keyed on the file's path, modification time and size, so an all-sky catalog is read once per process rather than once per frame --get_starsruns per frame, and the read was the dominant cost of matching against a large file. A rewritten catalog at the same path is still picked up, the result is a shallow copy so callers cannot corrupt the cached frame, andread_catalog.cache_clear()drops it.
Changed¶
pandasis now floored at>=3. Copy-on-write is unconditional there, which is what makes the shallow copyread_catalogreturns genuinely isolate the cached catalog; on 2.x copy-on-write can be off and a caller writing through.locwould reach the cached frame. The resolved version does not move --uv.lockalready pinned 3.0.5 -- so this declares what was already being tested against.- Documentation at https://panoptes.github.io/panoptes-pipeline/, built with
Zensical from the docstrings and published to GitHub
Pages on every push to
main. A page underdocs/api/is a:::block naming a module, so improving a docstring improves the reference and there is no second description of the API to keep in step.plans/is deliberately not published. create-release.ymlpublishes avX.Y.Ztag to PyPI through Trusted Publishing and creates the GitHub release from the annotated tag message, which is therefore the one place release notes are written. The workflow refuses a lightweight tag rather than publishing a commit subject as the notes.uv build/uv publishby hand is no longer the release process.canary.ymlre-resolves the dependency tree from scratch every Monday and runs the suite against it, so an upstream release that breaks this package surfaces on a schedule rather than in someone's environment.CONTRIBUTING.md,AUTHORS.md,CODE_OF_CONDUCT.mdand a.pre-commit-config.yamlcarrying the same ruff settings CI enforces.pre-commitis in the newlintdependency group, souv run pre-commit installworks from a plainuv sync..gitattributesmarksCHANGELOG.mdmerge=union, so parallel branches appending under## Unreleasedstop conflicting.-
plans/project-standards.mdrecords how all four PANOPTES repositories are built, linted, tested, documented and released, and why each choice is what it is. -
scripts/survey_headers.pysurveys the FITS headers of a local copy of the raw archive intoheaders.parquet, one row per frame. It reads headers and never pixels, so it needs no processed tree and no plate solving -- which is what makes it usable now, and what keeps benchmark selection honest: the processed index is built from output of the implementation being replaced. WCS written back into archived frames by the retired cloud pipeline is excluded for that reason, and kept only asplate_solved/solve_dateprovenance, since the archive's "raw" frames turn out to be raw to different degrees by era. Sharded per sequence, so an interrupted run resumes. panoptes.pipeline.productswrites a frame's metadata document and products to<root>/{unit}/{camera}/{sequence_time}/{image_time}/asmetadata.json,image.fits,extras.fitsandsources.parquet. The pipeline had produced no metadata since the Firestore writer was deleted. Documents are validated against document-store rules before writing, so attaching a store later is an upload rather than a migration.panoptes.pipeline.provenancetags each calibration value with the tier it came from --header,measured,registryordefault-- so a measurement and a fleet-wide fallback no longer serialize alike. Falling back warns.- Real POCS frames under
tests/data/: a raw frame, a solved one, and a header carrying almost nothing, pluswidefield.fits.fz-- a real PANOPTES frame rebinned 4x4 so it keeps its 14.9 x 9.9 degree angular size in 0.92 MB. CI now installsastrometry.netand the Tycho-2 index files and solves it with production options, so plate solving is tested rather than skipped. panoptes.pipeline.indexbuilds the query surface by walking the documents the pipeline wrote:frames.parquet, one row per frame, andobservations.parquet, one row per sequence derived from it. This replaces the Firestore-generatedobservations.csv, which nothing has produced since the cloud removal. It reads no pixels, and it computes what the old summary could not express -- usable frames as distinct from frames, duration,total_exptimeas a sum over frames rather than a number only the index holds, and a flag for a camera uid recording more than one serial across sequences. Aschema.jsonmanifest records the column contract, so a consumer in another repository reads a stated schema rather than reverse-engineering one.NoSourcesDetected, raised when nothing in a frame clears the detection threshold. Clouds, a closed dome and lost tracking all produce such a frame, so it is an ordinary outcome over a survey and callers can distinguish it from a genuine failure.scripts/fetch_catalog.pybuilds a local catalog for a field from a Gaia DR3 cone search. Nothing in this repository could previously produce the catalog the pipeline requires -- the archive's catalog came from a BigQuery table that was deleted with the cloud stack -- so the pipeline could not be run on a frame at all.picidis exactly the Gaia DR3source_id, so no crossmatch is involved. PEP 723, soastroquerystays out of the package. The default filename states the query that produced it, and an existing, usable file is left alone, so re-running for a field already fetched costs nothing.panoptes.pipeline.worklistclassifies every raw frame against an output root as missing, params changed, prior error, incomplete, forced or up to date, without opening any pixels.as_tablerenders the result as a CSV-able table, so a settings change can be shown to have invalidated what was expected and nothing more.PipelineParams.fingerprint, a short digest of every parameter that affects a product. Settings were previously stored in each document but never compared, so a change left stale products in place with no signal.panoptes.pipeline.statusholdsImageStatusandObservationStatus, moved back frompanoptes-data. They describe this pipeline's own stages, so the client that reads its output should not own them.products.record_processingstamps a document with the settings, fingerprint and stage it was produced at;products.read_documentreads one back, returning None rather than raising on a file that will be rewritten anyway.panoptes.pipeline.processingrestores the two entry points as library functions:process_framefor a single FITS,process_observationfor a sequence. Calibration iscalibrate; the sequence document is written once at the end of a batch, by the only writer that exists at that point.
Changed¶
- Lint settings are now shared with
panoptes-utilsand POCS: 110 columns andE,W,F,I,UP. The accompanying reflow touched 24 files and changed no behavior. - Tooling dependencies are split into
lint,testanddocsgroups, withdevincluding all three.uv syncwith no arguments is unchanged. AGENTS.mdis the canonical agent instruction file;CLAUDE.mdandGEMINI.mdare symlinks to it.tests.ymlruns lint as its own job, installs withuv sync --lockedso a stale lockfile fails CI rather than being silently re-resolved, and uploads coverage to Codecov.panoptes.pipeline.utilsis a regular package rather than an implicit namespace package. Imports are unaffected; static readers, including the one that builds the API reference, can now see the four modules in it.scikit-imageis a declared dependency again. v0.4.0 dropped it from the retiredenv.yamlon the grounds that nothing imported it, which was true of this package's own source and missed thatphotutilsimports it at runtime for deblending. A plainuv synctherefore produced an environment where source detection raisedModuleNotFoundError-- the split environment that release set out to eliminate. Checking direct imports is not enough to retire a dependency.extract_metadatareads image dimensions fromNAXIS1/NAXIS2rather thanIMAGEW/IMAGEH, which only plate solving writes -- raw frames silently yielded zero.- The camera id and body serial are now in the
imagedocument as well assequence, so a per-frame measurement can be joined to its camera. extract_metadatatakes an optionalCameraSettingsfor header fallbacks.- A frame's pixels are one multi-extension
image.fits--PRIMARYreduced, plusBACKGROUND,RMSandMASK-- instead ofimage.fitsandextras.fits.extras_filenameis gone. Raw pixels are no longer copied into the processed tree, and the background is stored as the low-resolution mesh photutils actually fits rather than its interpolation, which is four orders of magnitude larger. Roughly 100 MB per frame instead of 312 MB. CameraSettings.saturationis now raw ADU, matchingWHTLVLN, and saturation is masked before bias subtraction. The default moves15872->16384; those are the same number in different domains (15872 = 2**14 - 512), but mixing them would have over-masked by the bias on every frame.plate_solveno longer replaces the file it solves, and honours its owntimeoutargument instead of a hardcoded 300 seconds.match_sourcestakes the frame's dimensions explicitly. They previously came from fleet-wide settings that the caller was expected to overwrite per frame, which would now change the params fingerprint from frame to frame.panoptes-utilsfloor raised to0.3.1, which introducedImagePathInfo; it is now imported frompanoptes.utils.images.fits.
Fixed¶
sources.get_catalog_matchdocuments the function that exists. Its docstring described areturn_unmatchedargument -- inArgs:, aNote:, aWarning:and a paragraph of prose -- and anoriginargument twice, neither of which is in the signature. Passing either went into**kwargsand was silently ignored, so a caller asking for unmatched catalog entries got matched ones and no error. The appending of unmatched entries is not implemented; the docstring now says what the function returns instead of promising a switch for it. It also no longer callswcsoptional --get_xy_positionsneeds it on every call, so passingcatalog_starswithout awcsraisesAttributeErrorafter the matching is done -- and no longer points at alookup_point_sourcesthat does not exist, or imply the stalemeasured_ra/measured_decdefaults are whatdetect_sourcesproduces.-
sources.get_catalog_matchandsources.get_starsno longer claim the catalog is derived from the TESS Input Catalog v8.picidis exactly the Gaia DR3source_id, so a catalog is a Gaia cone search with its columns renamed and no crossmatch step -- which is whatscripts/fetch_catalog.pybuilds. The listed added columns were also wrong: the function addscatalog_sepand the fourcatalog_wcs_{x,y}[_int]columns, notcatalog_sep_arcsec,catalog_x,twomassor thecatalog_measured_diff_*family. -
process_framereports the reduced image it wrote. The path was returned bywrite_imageand then dropped, so the result named every product except the largest one. -
A frame whose header has no
FILENAMEkeyword can be identified.worklist.identifyfell through toImagePathInfo.from_fits_header, which reads that keyword first and catches onlyValueError, so a missing one raisedKeyErrorandprocess_framecould not place the frame at all. -
A catalog carrying only the four documented columns now works.
match_sourcesreadcatalog_gaiabp,catalog_gaiarpandcatalog_gaiamagunconditionally, so a catalog built to the stated contract passed validation, matched, and then raisedAttributeError-- after plate solving and source detection had already run. Those three are optional enrichment: present, three color-excess columns are derived; absent, they are skipped. Nothing reads the excesses yet. -
An absent
CAMSNrecords as null instead of the string"None". - A frame's metadata document is written after its products, not before. It is the marker the work-list walk reads to decide a frame is done, so a failure part way through the products used to leave a frame claiming to be finished with its products missing, and the next walk skipped it.
process_observationonly requiressolve-fieldwhen the work list is not empty, so re-running to confirm there is nothing to do no longer fails on a machine without a solver.- Source detection could not complete on any frame at all. A masked array was
handed to
photutilsasdatawhile its mask was also passed separately;photutilscomputes image moments as a matrix product andnumpy.macannot propagate a mask through@, soSourceCatalograised "operands could not be broadcast together". Detection now returns sources on a real frame. - A frame with no detections raises
NoSourcesDetected, naming the threshold it failed to clear, instead ofTypeError: segmentation_image must be a SegmentationImagefrom insidephotutils-- which said nothing about the frame. - Source detection no longer crashes on a frame with saturated pixels. Its mask guard tested a masked array for truthiness, which raises for any real mask and built a 0-d mask when there was none. It was unreachable only because the fleet-wide saturation default was too high to mask anything; resolving saturation from the header makes real masks appear.
Removed¶
panoptes-datais no longer a dependency: nothing imported it onceImagePathInfomoved topanoptes-utils, and it is downstream of this pipeline. The<0.2pin goes with it, along with 21 transitive packages includingipythonandipywidgets.
v0.4.0 -- 2026-09-12¶
The package stops requiring Google Cloud, Docker, conda and Jupyter. Every
module under src/panoptes/pipeline/ imports from a plain uv sync, and the
algorithm runs against a local directory with no credentials and no cloud
project -- which is what a citizen-science pipeline needs in order for a
contributor to run it at all.
The cost is stated plainly: this discards working archive integration and the only existing path from raw frames to products. Both were producing the un-subtracted stamps the rebuild is trying to get away from, and git history keeps every file.
Added¶
sources.read_catalog, which reads the catalog as parquet, ECSV or CSV, chosen by suffix (.parquet,.pq,.ecsv,.csv,.tsv, each optionally compressed). Parquet is the better default for an all-sky catalog; astropy's ECSV is the one to reach for when the file should stay readable, since its YAML header carries the column types. Plain CSV and TSV are accepted so an existing catalog needs no conversion step.picidis checked as an integer on read and returned as a categorical -- it names a star rather than measuring one. Filtering prunes unused categories, so a field cut from an all-sky catalog does not carry every id in the sky. Note when consuming it:groupbyon a categorical iterates every category unless passedobserved=True. The integer check is what catches plain CSV's missing dtypes, where a single blank turns that column into floats that join as1234.0against integer ids and silently match nothing.tests/test_sources_catalog.py, pinning the catalog filtering semantics the BigQuery query used to own: a half-open[vmag_min, vmag_max)range, an inclusive positional box, and a Right Ascension window that may wrap through zero. The filtering tests run against every accepted format, since the format must not change the answer. 139 tests, up from 78.
Changed¶
sources.get_starsreads a local catalog file instead of querying BigQuery. Callers passcatalog_filename, or setparams.catalog.catalog_filename. There is no network lookup and no fallback, so an unset path raises with a message naming the setting rather than silently reaching for credentials. The file must carry the mapped PIC column names --picid,catalog_ra,catalog_dec,catalog_vmag-- and one missing any of them raises and names them. Those four are the entire contract, so the catalog can be rebuilt without touching this package.images.match_sourcesnow filters the local catalog by the WCS footprint and the configured Vmag limits. It previously read the whole file unfiltered whenever a local catalog was set, applying those bounds only on the BigQuery path -- sovmag_limitswas silently ignored offline, and a full-sky catalog was matched in its entirety rather than over the field. The two routes now do the same thing.- The
bq_client,bqstorage_client,column_mappingandreturn_dataframearguments are gone fromget_stars;catalog_filenamereplaces them. - Coverage configuration moved from
.coveragercinto[tool.coverage.*]inpyproject.toml, alongside the ruff and pytest configuration. Settings are unchanged. README.mdrewritten: it documenteduv sync --extra cloudand--extra notebooks, neither of which exists, so both commands failed. It now describes the install as it is, the catalog file source matching needs, and how to contribute -- branch frommain, name the branch for its issue, run pytest and ruff, move the changelog in the same branch.- Two docstrings in
utils/sources.pydescribed matched columns as coming from source-extractor. They come from photutils.
Removed¶
- The papermill execution path, in full: both tracked notebooks, the
panoptes-pipelineconsole script and theutils/clipackage behind it,utils/notebooks.py,image.py,observation.py, and the FastAPI service inservices/processing.py. The notebooks were the implementation rather than a demonstration of it, and everything above existed only to run them, so with the notebooks gone none of it could execute. There is no path from raw frames to products until the library extraction and the new CLI land. - Google Cloud, in full:
utils/gcp/(BigQuery, Firestore and GCS helpers), the eight cloud packages, and.gcloudignore. Removal was chosen over putting cloud behind adapters, so there is no cloud code path at all rather than one that is off by default -- an offline path that is the only path cannot rot quietly while the cloud path is the one being exercised. - Docker and conda, in full: the
Dockerfile,.dockerignoreandenv.yaml. The image existed to run the FastAPI service, so its entrypoint pointed at a module that no longer exists; it would have built and then failed to start.env.yamlwas that image's conda environment and its only consumer, duplicatingpyproject.tomlimperfectly --astroplan,db-dtypes,jupyterlab_execute_timeandscikit-imagewere listed there and nowhere else, and nothing imports any of them. The image installed conda, then mamba, then pip on top of both.uv syncis the supported install; there is no container build. - All optional-dependency extras. The
cloudandnotebooksextras went with the code they installed, so there is no longer an environment in which part of the package works and part raisesModuleNotFoundError. - The
docs/tree,.readthedocs.ymland thedocsdependency group, plus theDocumentationproject URL.https://panoptes-pipeline.readthedocs.ioreturns 404 -- the project does not exist on Read the Docs -- and what was not being published was generator boilerplate whose landing page still opened with "This is the main page of your project's Sphinx documentation".README.mdandplans/are the documentation. resources/source-extractor/. Nothing referenced it; detection runs throughphotutils.segmentation.AUTHORS.mdandCONTRIBUTING.md, folded intoREADME.md. The latter was a single line pointing at another repository'sdevelopbranch, describing a workflow this project retired.
v0.3.0 -- 2026-09-12¶
Added¶
uv.lock, pinning 238 packages. Precision numbers are compared across weeks, so an unpinned scipy changing a solver default underneath a benchmark is a reproducibility problem, not a convenience one.cloudandnotebooksoptional dependency extras, holding the legacy GCP orchestration and the papermill execution path. Neither is installed by default; both are slated for removal.devanddocsdependency groups (PEP 735) for test, lint and docs tooling.tests/__init__.py, sofrom tests.synthetic import ...resolves under plainpytestrather than only underpython -m pytest.
Changed¶
uv syncproduces an environment where the package imports, so the documented test command isuv run pytest-- noPYTHONPATH=src, no--withlist.pydantic-settings,typer,matplotlib,astropy,tqdmandpython-dateutilare declared dependencies instead of arriving transitively.panoptes-datapinned<0.2, which droppedpanoptes.data.images. A holding action; see issue #176.google-cloud-bigquery-storage[pandas]relaxed from==2.6.2to>=2.6.2.- The
Programming Language :: Python :: 3.8classifier is now3.12, matchingrequires-python. - Read the Docs builds on Python 3.12; it asked for 3.11, which cannot install a
package requiring
>=3.12.
Removed¶
- All six
[tool.hatch.envs.*]blocks.hatchlingandhatch-vcsremain the build backend and version source; only the task-runner layer is gone, replaced by documenteduvinvocations includinguv buildanduv publish. - The unreachable
importlib-metadata; python_version < '3.8'marker and its counterpart, the deadsys.version_info >= (3, 8)branch insrc/panoptes/pipeline/__init__.py. - The
testingextra, folded into thedevdependency group; tooling is not package metadata. mocket, which nothing imports.
v0.2.0 -- 2026-09-12¶
The rebuild's starting line.
First tag since v0.1.0 (January 2020, 166 commits back). Marks the point where the repository was cleaned up and the algorithm rebuild became the work: a pure-numpy lightcurve package with no I/O or cloud dependency, the benchmark harness, the plan documents, a single main branch, and a GitHub tracker with the work broken out.
Pre-1.0 deliberately. The cloud modules are being deleted, the CLI is being rewritten, and the core algorithm is pending a head-to-head test, so the public API is not stable and should not be treated as such.
v0.1.0 -- 2020-01-19¶
WCS footprint improvements.