CrowdSky Time-Block Stacking
The crowdsky module automates the production of fixed-duration stacked
chunks from a Seestar observation session. This enables citizen-science
time-domain astronomy by assembling a consistent, down-sampled dataset
from many Seestars.
Raw sub-frames are grouped into clock-aligned time blocks (default 15
minutes) and each block is batch-stacked independently on the Seestar.
Output files are renamed to a CrowdSky_* naming convention that
encodes the UTC chunk key, HEALPix sky pixel, and filter, making re-runs
fully idempotent.
Discovering targets
Use list_targets() to see which observation
targets have raw sub-frames on the Seestar:
from seestarpy import crowdsky
targets = crowdsky.list_targets()
for t in targets:
print(f"{t['target']:25s} raw={t['raw_files']:>5d} "
f"stacked={t['stacked_files']:>3d}")
Example output:
IC 434 raw= 158 stacked= 1
M 42 raw= 216 stacked= 1
M 81 raw= 2053 stacked= 1
Previewing unstacked blocks
Use find_unstacked_blocks() to see which
15-minute blocks still need stacking:
blocks = crowdsky.find_unstacked_blocks("IC 434")
for b in blocks:
start = b["block_start"].strftime("%H:%M")
end = b["block_end"].strftime("%H:%M")
print(f" {start}-{end} {b['frame_count']} frames "
f"{b['exposure']} {b['filter']}")
Example output:
21:15-21:30 33 frames 10.0s LP
21:30-21:45 69 frames 10.0s LP
21:45-22:00 56 frames 10.0s LP
Dry run
Use dry_run=True to preview what stack_blocks()
would do without actually stacking:
result = crowdsky.stack_blocks("IC 434", dry_run=True)
Output:
Dry run: 3 blocks to stack for IC 434
21:15-21:30 33 frames x 10.0s (LP) = 330s
21:30-21:45 69 frames x 10.0s (LP) = 690s
21:45-22:00 56 frames x 10.0s (LP) = 560s
Stacking
Run without dry_run to actually stack. Each block is processed
sequentially (the Seestar can only do one batch stack at a time):
result = crowdsky.stack_blocks("IC 434")
Output:
Stacking block 21:15-21:30 (33 frames, 10.0s LP)...
Complete: 33 frames stacked
Renamed -> CrowdSky_33_IC 434_10.0s_LP_20260227.81_HP049152.fit
Stacking block 21:30-21:45 (69 frames, 10.0s LP)...
Complete: 69 frames stacked
Renamed -> CrowdSky_69_IC 434_10.0s_LP_20260227.82_HP049152.fit
Stacking block 21:45-22:00 (56 frames, 10.0s LP)...
Complete: 56 frames stacked
Renamed -> CrowdSky_56_IC 434_10.0s_LP_20260227.83_HP049152.fit
The return value is a summary dict:
>>> result["blocks_stacked"]
3
>>> result["blocks_failed"]
0
Re-running is safe — already-stacked blocks are detected by their
CrowdSky_* filenames and skipped automatically.
Output filename convention
Output files on the Seestar are renamed from the firmware’s
DSO_Stacked_* format to:
CrowdSky_<N>_<target>_<exposure>_<filter>_<YYYYMMDD.CC>_HP<nnnnnn>.fit
Where:
<YYYYMMDD.CC>is the UTC chunk key — the UTC date plus a chunk index (0–95) representing which 15-minute slot of the day the block falls in.HP<nnnnnn>is the HEALPix pixel (NSIDE=64, nested) derived from the RA/Dec in the stacked FITS header. This encodes the sky position so that stacks from different Seestars pointing at the same area can be matched. If RA/Dec cannot be read, it falls back toHP000000.
For example:
CrowdSky_33_IC 434_10.0s_LP_20260227.81_HP049152.fit
^^ ^^^^ ^^ ^^^^^^^^^^ ^^^^^^^^
frames exposure filt UTC chunk HEALPix
This encoding makes coverage detection deterministic and idempotent, even when multiple blocks have the same frame count.
Parameters
block_minutes(default 15)Duration of each time block in minutes. Blocks are aligned to clock boundaries — for 15-minute blocks, boundaries fall at
:00,:15,:30, and:45.min_exptime(default 240)Minimum total effective exposure in seconds for a block to be worth stacking. Calculated as
frame_count * exposure_seconds. Blocks below this threshold are skipped.
# Use 10-minute blocks, skip blocks with less than 2 minutes of data
result = crowdsky.stack_blocks("M 81", block_minutes=10, min_exptime=120)
Stacking all targets at once
Use stack_all() to process every target that
has raw sub-frames, without having to name them individually:
result = crowdsky.stack_all(dry_run=True) # preview first
result = crowdsky.stack_all() # then run
print(f"Stacked: {result['total_blocks_stacked']} "
f"Failed: {result['total_blocks_failed']} "
f"Skipped: {result['total_blocks_skipped']} "
f"across {result['targets_processed']} targets")
The same block_minutes, min_exptime, and dry_run parameters
are passed through to each target.
Purging CrowdSky stacks
If you need to re-stack from scratch (e.g. after changing block size),
use purge_crowdsky_stacks() to delete all
CrowdSky_* files from the Seestar:
# Purge one target
crowdsky.purge_crowdsky_stacks("IC 434")
# Purge all targets
crowdsky.purge_crowdsky_stacks()
After purging, stack_blocks() will see all
blocks as unstacked again.
CrowdSky server
The crowdsky module also supports uploading stacked chunks to a
CrowdSky collaboration server for aggregation with other observers.
from seestarpy import crowdsky
# Set your credentials
crowdsky.set_credentials("username", "password")
# Upload a single stacked FITS file
crowdsky.upload_stack("CrowdSky_33_IC 434_10.0s_LP_20260227.81_HP049152.fit")
# List your uploaded stacks
stacks = crowdsky.list_stacks()
for s in stacks:
print(s)
# Download stacks by chunk key
crowdsky.download_stack(["20260227.81_HP049152"], dest="./downloads")
Bulk uploading stacks
Use upload_all_stacks() to upload all
CrowdSky_*.fit files from one or more observation folders on the
Seestar to the CrowdSky server in one call. Files that have already
been uploaded (matched by chunk key) are skipped automatically.
from seestarpy import crowdsky
crowdsky.set_credentials("username", "password")
# Preview what would be uploaded
crowdsky.upload_all_stacks("IC 434", dry_run=True)
# Upload all stacks from one target (downloads to ./downloads/ first)
crowdsky.upload_all_stacks("IC 434", dest="./downloads")
# Upload from all targets at once
crowdsky.upload_all_stacks(dest="./downloads")
If you have already downloaded stacks to a local directory, you can upload from there directly without needing a Seestar connection:
crowdsky.upload_all_stacks(local_dir="./my_stacks")
Full workflow example
A complete script that connects to a Seestar, stacks all targets, and uploads the results to the CrowdSky server:
from seestarpy import connection, crowdsky
connection.DEFAULT_IP = "192.168.1.83"
crowdsky.set_credentials("username", "password")
# Stack all targets
crowdsky.stack_all()
# Upload everything to CrowdSky
crowdsky.upload_all_stacks(dest="./downloads")