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:

  1. checks or creates the required local data/configuration files through koprhand.filecheck();
  2. updates comet data through koprhand.CometUpdate();
  3. loads observer defaults and equipment lists;
  4. forces completion of missing observer/location/telescope/camera-or-eyepiece configuration;
  5. 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

IndexDisplayed tabWidget created by the hostImplementation module
0Process Visual Obs.ScrollVisualwvisual.py
1Image Calibration & StackingScrollWStackwstack.py
2Process CCD Obs.ScrollCCDwccd.py
3Observation PlannerScrollPlannerwplanner.py
4Light Curve AnalyzerScrollAnalyzer2wanalyzer.py
5Light Curve ComparisonScrollComparisonwcomparison.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
Invariant: refresh operations must not instantiate an unopened tab merely to update it. The host explicitly refreshes WStack only when it already exists in _widget_cache.

Module layers and ownership

LayerModulesResponsibility
Host shellkopr.py, settings.pyMain window, menu selection, active settings, tab lifecycle and application shutdown.
User workflowswvisual.py, wplanner.py, wanalyzer.py, wcomparison.py, wccd.py, wstack.pyQt widgets, dialogs, event handlers and orchestration for each tab.
WStack numerical layerwstack_functions.pyCalibration masters, LIGHT calibration, frame QC, astrometry, relative-WCS checks, reprojection, combination and FITS output.
External solver adapterwstack.py, wstack_functions.pyNative/WSL solver discovery, Windows-to-WSL path translation, argument-safe launch, PID publication and bounded process-group cancellation.
WStack I/O/state layerwstack_common.pyFITS/RAW loading, normalized metadata, saturation metadata, compatibility tests and atomic local pipeline state.
Photometry engineacfcore.py, autophot.py, photom.py, cometcentroid.py, parts of koprfunc.pyImmutable 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 serviceskoprhand.py, koprfunc.py, koprdial.py, koprfig.py, koprclass.py, koprobs.py, datavar.pyPersistent files, catalogue/network access, ephemerides, common calculations, dialogs, plotting and observation data structures.
Astronomical routinesalb3.py, comephem.py, npfunc.pyTime/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.

IndexContentPrimary consumers
0Observer code and nameObservation output and window title
1General/CCD defaultsVisual, WCCD and output formatting
2Active locationPlanner, topocentric ephemerides, status bar, WStack
3Active telescopeVisual field geometry, CCD scale, WStack seed scale
4Active eyepieceVisual workflow
5Active cameraWCCD 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.

R47 FLOAT fallback: WCCD resolves complete source maps and detector metadata before applying type-based floating thresholds. Normalized 0–1 data use 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_files or downstream calibration/astrometry/stacking.
  • A fatal resolution error occurs before _base_dir mutation, 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 by settings.py and koprhand.py.
  • Global comet/observation data: comet-data/ and obs-data/.
  • WStack session state: a per-working-directory JSON state managed by wstack_common.load_state(), save_state(), migrate_raw_state_paths_to_fits() and reconcile_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 and COMETTRK.

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

ModuleBoundary
ephemeris_position.pyInternal KOPR position plus optional lazy JPL Horizons RA/Dec query using topocentric location. It returns position/provenance only, not photometric ephemerides.
cometfinder.pyCandidate-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.pySingle 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.pyNon-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 backendBresenham line construction, Through-Comet and Between-Points geometry, comet sample index, profile result and overlay restoration.
wccd.pyObservation-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.

Settings contract: worker counts and memory ceilings are not user-visible settings. A rebase must not expose them or replace thread-based numerical scheduling with Python process pools.

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.

Do not destroy an active worker's owning widget. New tabs or dialogs that start threads or external processes should expose the same bounded shutdown convention so the host can close deterministically.

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

  1. Preserve dependency direction. GUI modules may orchestrate numerical modules; numerical modules should not reach into arbitrary host widgets.
  2. Add a narrow adapter. Pass providers/callbacks or immutable job records instead of sharing the whole main window.
  3. Keep tab indices explicit and unique. Update the label/status arrays and lazy factory together.
  4. Version data contracts. Matched stack jobs already carry a pair schema version and named FITS extension contracts.
  5. 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.
  6. 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:

  1. camera compatibility reconstructs native geometry and returns exact, unique compatible or unresolved;
  2. 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.