Application architecture
Runtime ownership, module boundaries, lazy widget creation, shared configuration and the data hand-off between WStack and WCCD.
Source-verified Runtime ownership, module boundaries and host integration were checked against the six tab modules, WStack, WCCD and the shared automatic-helper backend.
Runtime entry and initialization
kopr.py is the application entry point. It creates the Qt application, constructs MainWindow and starts the event loop. Before the main window becomes usable, the host:
- checks or creates the required local data/configuration files through
koprhand.filecheck(); - updates comet data through
koprhand.CometUpdate(); - loads observer defaults and equipment lists;
- forces completion of missing observer/location/telescope/camera-or-eyepiece configuration;
- builds menus, placeholder tab positions and the status bar.
The source tree relies on relative paths such as ./config-files/, ./icons/ and local catalogue directories. Therefore the process working directory remains part of the runtime contract: KOPR should be launched from the directory containing kopr.py.
Persistent tab registry and lazy loading
| Index | Displayed tab | Widget created by the host | Implementation module |
|---|---|---|---|
| 0 | Process Visual Obs. | ScrollVisual | wvisual.py |
| 1 | Image Calibration & Stacking | ScrollWStack | wstack.py |
| 2 | Process CCD Obs. | ScrollCCD | wccd.py |
| 3 | Observation Planner | ScrollPlanner | wplanner.py |
| 4 | Light Curve Analyzer | ScrollAnalyzer2 | wanalyzer.py |
| 5 | Light Curve Comparison | ScrollComparison | wcomparison.py |
MainWindow._getOrCreateWidget() is the single widget factory. The host initially inserts placeholders for all six positions, eagerly creates only the Visual tab, and creates every other widget when its tab is first selected. The result is stored in _widget_cache; later selections reuse the same instance.
tab change
└─ MainWindow._onTabChanged(index)
├─ _getOrCreateWidget(index)
│ ├─ return cached widget, or
│ └─ construct Scroll* widget and replace placeholder
└─ update status bar for active module
_widget_cache.Module layers and ownership
| Layer | Modules | Responsibility |
|---|---|---|
| Host shell | kopr.py, settings.py | Main window, menu selection, active settings, tab lifecycle and application shutdown. |
| User workflows | wvisual.py, wplanner.py, wanalyzer.py, wcomparison.py, wccd.py, wstack.py | Qt widgets, dialogs, event handlers and orchestration for each tab. |
| WStack numerical layer | wstack_functions.py | Calibration masters, LIGHT calibration, frame QC, astrometry, relative-WCS checks, reprojection, combination and FITS output. |
| External solver adapter | wstack.py, wstack_functions.py | Native/WSL solver discovery, Windows-to-WSL path translation, argument-safe launch, PID publication and bounded process-group cancellation. |
| WStack I/O/state layer | wstack_common.py | FITS/RAW loading, normalized metadata, saturation metadata, compatibility tests and atomic local pipeline state. |
| Photometry engine | acfcore.py, autophot.py, photom.py, cometcentroid.py, parts of koprfunc.py | Immutable local ACF contexts, PA-adaptive ring analysis, one canonical bounded comet-centroid model, automatic comet-candidate and comparison-star helper analysis, manual aperture photometry, background, curve-of-growth and Afρ computations. |
| Shared legacy services | koprhand.py, koprfunc.py, koprdial.py, koprfig.py, koprclass.py, koprobs.py, datavar.py | Persistent files, catalogue/network access, ephemerides, common calculations, dialogs, plotting and observation data structures. |
| Astronomical routines | alb3.py, comephem.py, npfunc.py | Time/coordinate/ephemeris support and numerical helpers used by the application workflows. |
The current source is intentionally not a Python package with namespaced imports. Modules import each other by root-level names. This makes filenames and launch directory part of compatibility and increases the importance of keeping public cross-module surfaces small.
Shared host settings
koprhand.SettingsRead() returns the historical settings structure and SettingsUpdate() updates its process-level cache. The host passes snapshots/providers into newer widgets instead of duplicating indices throughout the numerical code.
| Index | Content | Primary consumers |
|---|---|---|
0 | Observer code and name | Observation output and window title |
1 | General/CCD defaults | Visual, WCCD and output formatting |
2 | Active location | Planner, topocentric ephemerides, status bar, WStack |
3 | Active telescope | Visual field geometry, CCD scale, WStack seed scale |
4 | Active eyepiece | Visual workflow |
5 | Active camera | WCCD geometry checks and WStack scale estimate |
When a Configuration submenu item changes, MainWindow.SelControl() updates the selected row, refreshes the SettingsRead cache, refreshes an already-open WStack context and persists the active Location/Telescope/Eyepiece/Camera rows. Startup restores those rows without forcing menu index zero. settings.dat is written through a flushed temporary file and atomic replacement. A future schema change should be adapted at the host boundary (for example WStack's _host_context() adapter), not propagated as raw new indices throughout the pipeline.
Processing output setting
The FITS output mode is appended to the second positional row of config-files/settings.dat at index 14: 0 for Scientific and 1 for Compact. Existing indices 0–13 are unchanged. Code must access the value through GetFitsOutputMode() and GetFitsOutputModeIndex(), not by reading the raw index in individual widgets. Settings writes preserve any additional unknown trailing fields.
R76: camera identity/native geometry and observation binning are distinct authorities. Consumers obtain physical scale through the shared priority resolver rather than recomputing incompatible local estimates.
FITS storage and inspection layer
The processing pipeline separates numerical arrays from the final disk representation:
floating-point computation
└─ output policy requested at task start
├─ Scientific writer → float32 / NaN
└─ Compact candidate
├─ robust neighbour-difference noise estimate
├─ safe QSTEP test
├─ scaled int16 writer, or
└─ per-file Scientific fallback
Downstream readers decode real FITS image types to detached C-contiguous float32 arrays. The shared FITS inspector determines science-HDU identity, scaling, core observation metadata and native WStack contracts without conflating storage type with file origin.
Astrometric header updates use a copy-preserving path that does not decode and requantize the primary array. This preserves Scientific/Compact storage and auxiliary HDUs exactly while updating WCS/SIP/QC metadata.
1.0; other floating data use editable non-authoritative 65535. Comet and Stars retain independent state.DSLR RAW materialization boundary
R74 preserves the R69 FITS-only boundary but makes materialization deterministic:
directory discovery (metadata only; no RAW decode)
├─ pending/stale extraction candidates + rawpy available
│ └─ RawMaterializationWorker
│ └─ materialize_raw_records()
│ └─ verified temporary TG FITS → atomic final FITS
├─ pending/stale candidates + rawpy unavailable
│ └─ critical extraction-unavailable result
└─ conflicts
└─ separate warning; no overwrite and no RAW bypass
post-resolution rediscovery
└─ migrate_raw_state_paths_to_fits()
└─ reconcile_session()
└─ FITS-only _session_files
└─ select new/refreshed TG FITS or first verified representation
rawmaterialization.py owns the only production RAW decoder and shared materialization service. wstack.py owns automatic orchestration, worker/progress lifecycle and transactional session commit. wstack_common.py owns discovery/state migration. wstack_functions.py consumes only FITS-stage inputs.
R81 separates representation identity from ancestry. The materializer writes RAWREP=T only on a direct TG FITS. Calibration and astrometry write RAWREP=F; final stack assembly strips direct single-source RAW identity cards while retaining aggregate provenance. Discovery must reject a RAWFILE/RAWEXT claim as direct identity when derived-product cards or safe legacy naming prove that the FITS is downstream.
- Discovery must not decode image mosaics.
- No confirmation or bypass branch may skip a pending/stale extraction candidate.
- No physical RAW path may enter
_session_filesor downstream calibration/astrometry/stacking. - A fatal resolution error occurs before
_base_dirmutation, so the previous session is retained when no usable FITS can open. - Cancellation is checked between source records; the active decode completes safely.
- Temporary output must be verified before atomic replacement of a stale target.
- Conflicting/unrelated target FITS must not be overwritten or converted into extraction candidates.
Persistent state and file ownership
- Global KOPR configuration: text files in
config-files/, managed bysettings.pyandkoprhand.py. - Global comet/observation data:
comet-data/andobs-data/. - WStack session state: a per-working-directory JSON state managed by
wstack_common.load_state(),save_state(),migrate_raw_state_paths_to_fits()andreconcile_session(). R69 migrates historical physical RAW paths to verified TG FITS paths before reconciliation; it owns per-group astrometry automatic/manual override state. General QSettings own the API key and standalone optical fallbacks, not per-group RA/DEC. - WStack manifests/QC: processing manifests, astrometry diagnostics and CSV/JSON reports live with the reduction session and must be treated as provenance, not as global preferences.
- FITS product provenance: primary-header cards plus image/table extensions such as
PROVENANCE,NVALID,NCONTRIB, saturation maps andCOMETTRK.
WStack state writes are atomic. On opening a directory, the JSON record is reconciled with files that actually exist, avoiding a global in-memory catalogue of all previous sessions.
WStack-to-WCCD hand-off
WStack final products
└─ Measure callback → mapping with comet_path, optional star_path,
n_frames and pair metadata
└─ MainWindow._measureStackGroup()
└─ ScrollCCD.w.start_stacked_group_measurement(group)
└─ prefilled New CCD/DSLR observation dialog
The Measure route activates WCCD and opens its ordinary New CCD/DSLR observation dialog with transferred paths and designation as editable defaults. The observation and any selected helpers are committed only after the dialog is accepted and preflight succeeds.
WStack also receives two host callbacks: a provider for the current settings snapshot and a provider for all configured locations. WCCD receives a provider for configured cameras and a callback that activates a camera through the host's normal selection logic.
ACF measurement context and invalidation
acfcore.py separates immutable data preparation from repeatable parameter analysis. ACFMeasurementContext stores only the local image patch and masks as read-only arrays, together with source_id, context_id, run_id and observation_id.
The frozen Review context is owned by the last successful ACF Comet measurement, not by the current GUI image or mode. It survives Stars-image, reference-star, catalogue and photometric-option changes. A new observation/reset/different Comet FITS, a newer successful non-ACF measurement or a structurally incompatible context invalidates it; cancelled or failed replacement attempts preserve it.
This boundary prevents parameter exploration from repeating FITS I/O, candidate detection, catalogue work, comparison-star measurement or zero-point calculation.
WCCD helper architecture and observation-local state
| Module | Boundary |
|---|---|
ephemeris_position.py | Internal KOPR position plus optional lazy JPL Horizons RA/Dec query using topocentric location. It returns position/provenance only, not photometric ephemerides. |
cometfinder.py | Candidate-only paired or single-image backend. Interactive WCCD search uses 120 arcsec; unattended automatic photometry retains 30 arcsec. No WCCD commit, no photometry and no automatic confirmation. |
cometcentroid.py | Single production definition of CometCentroid: the canonical bounded 2D Gaussian model used by manual WCCD, automatic candidates and M1. koprfunc.py re-exports the function and autophot.py calls it. |
starmeasure.py | Non-mutating Auto Star Measure backend and WCCD adapter. Public lazy entry: autophot.run_auto_star_measure_backend(...); GUI owns the atomic commit. R55 keeps one pipeline and owns mask-aware centroid/profile diagnostics, robust stellar FWHM selection, post-aperture blend checks and background fallback state. |
| Slice backend | Bresenham line construction, Through-Comet and Between-Points geometry, comet sample index, profile result and overlay restoration. |
wccd.py | Observation-local controller, named mode identifiers, worker coordination, candidate confirmation and atomic GUI/data commits. |
State machines
- Comet Finder: disabled, pending, running, confirmed/completed, rejected, failed, cancelled, stale.
- Auto Star Measure: disabled, pending, running, completed, failed, cancelled, stale.
- Slice: inactive, waiting for first point, waiting for second point, completed.
Late-result protection
Every worker result validates observation ID, Comet/Stars source identity, run token, current process state and target slot. Reset/new observation invalidates the old state and clears comet_center_meta. Star commits are transactional: validation occurs before mutation and a failed commit restores both data and GUI aliases.
Authoritative comet-centre hand-off
After visual confirmation, the accepted coordinates are the single authoritative centre for WCCD state, the displayed marker, Slice geometry, manual/automatic comet aperture measurement and ACF. Downstream paths must not independently recalculate or replace that centre unless the user starts a new centering action.
R55 stellar-profile isolation
The mask-aware stellar centroid/profile path belongs only to Automatic Star Measure. Its projection guard, compact-profile fit, robust-FWHM selection, final blend gate and balanced-background diagnostics must not replace the established Comet Finder or stellar-trail-contamination centroid/PSF path. This boundary protects previously stabilized candidate scoring while retaining a single Auto Star Measure pipeline across sparse and crowded fields.
Role-specific saturation state
Comet and Stars slots each own an automatic or manual_threshold saturation configuration. Complete NSATVAL provenance is authoritative only in automatic mode. Editing Saturation level changes only the active role; loading a replacement FITS resets only that slot. Any transfer of manual thresholds to autophot.py is guarded by exact source-path equality.
Named modes
Qt itemData carries stable identifiers for Measure Comet, Measure Tail, Measure Stars, Slice Through Comet, Slice Between Points and Fix Astrometry; code must not use fixed combobox indexes.
Canonical reference-star ownership
AutoStars and Manual Stars share one lossless canonical collection and one Reference stars dialog. Observer intent (user_enabled) and derived global consistency (pairwise_accepted) are separate fields. Process obs. and Af-rho consume the same accepted-ID set and final solution.
WCCD viewport ownership
Measurement-mode selection and image activation are separate operations. R81 avoids _activate_image(..., redraw=True) when the required Comet/Stars role is already active; it refreshes only selector/mode state, preserving Matplotlib axis limits and in-memory image data. When the active role is wrong, the normal role switch and one redraw remain required. The same rule applies to repeated comet, star and tail measurement.
Workers, Qt ownership and shutdown
Long WStack operations use worker objects moved to QThread instances. The GUI layer owns progress dialogs, signals and cancellation; numerical functions receive simple log_fn, progress_fn and cancel_fn callbacks. This keeps the calculation layer independent of widget state.
R22 numerical scheduling is thread-based and bounded by fixed automatic resource policies. Master medians use at most four threads with a shared 192 MiB allowance; LIGHT calibration, row reprojection and sigma clipping use at most two threads, with adaptive or shared memory allowances. The prepared-master cache retains at most two recent pairs under a 768 MiB and 25%-of-available-RAM ceiling.
Astrometry is the deliberate exception to in-process numerical threading: the batch may run at most two external solve-field processes, each in an isolated working directory. All paths retain an automatic serial fallback.
Astrometry additionally owns an external-process lifecycle. GUI execution resolves the solver backend before index handling and passes the validated backend into AstrometryBatchWorker; the worker retains a fallback preflight only for direct/API use.
For a WSL backend, wsl.exe is only the Windows launcher. The real Linux solver is started by an argument-safe shell wrapper. Where available, setsid creates an isolated process group; a temporary .solve-field-wsl.pid file exposes the Linux leader PID to the Windows side. Cancellation signals that process group with TERM, waits a bounded interval and escalates to KILL. Only after that does KOPR use bounded launcher termination as a final fallback.
ScrollWStack.shutdown_workers(timeout_ms) is the public shutdown surface. MainWindow.closeEvent() iterates cached widgets, calls any available shutdown_workers method and refuses to close if a worker has not stopped safely within the requested interval.
Astrometry attempt, input and group ownership
The astrometry controller owns a bounded per-frame attempt state and a per-group failure policy. Temporary robust FITS files and solver logs belong to isolated solver work directories and are cleaned with that attempt lifecycle. A failed required anchor commits one failure plus deterministic SKIPPED_ANCHOR_FAILED results under the default policy; it must not silently launch the rest of the group.
R48 keeps online and offline input ownership separate. The online uploader owns a blind payload and success-gated job polling; it must not read GUI position/scale values into upload constraints. The offline group dialog owns displayed automatic values and explicit two-field overrides, while per-frame seed construction remains in the numerical layer. Index discovery is a read-only diagnostic boundary over optional 4100 and established 4200 families. Downloader ownership remains limited to 4206–4214.
Rules for extending the application
- Preserve dependency direction. GUI modules may orchestrate numerical modules; numerical modules should not reach into arbitrary host widgets.
- Add a narrow adapter. Pass providers/callbacks or immutable job records instead of sharing the whole main window.
- Keep tab indices explicit and unique. Update the label/status arrays and lazy factory together.
- Version data contracts. Matched stack jobs already carry a pair schema version and named FITS extension contracts.
- Write provenance with the product. A numerical decision that changes accepted frames, background, aperture or saturation status must be auditable from logs/reports/FITS metadata.
- Add regression tests before refactoring scientific flow. Byte-for-byte code cleanup is secondary to preserving measured outputs and validation behavior.
R73 observation-opening boundary
Observation opening evaluates two shared decision cores before user-visible fallback dialogs:
- camera compatibility reconstructs native geometry and returns exact, unique compatible or unresolved;
- saturation resolution freezes a role-specific source-map or scalar image-domain contract for the loaded FITS.
wccd.py, wccdassist.py and Automatic Star Measure must delegate scalar saturation decisions to saturationpolicy.py. Missing source-frame provenance is retained in diagnostics but must not short-circuit common-aperture selection when scalar resolution succeeds. The camera dialog call belongs after the shared compatibility evaluation, not before it.