diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4170eb223..043b893d9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -282,21 +282,31 @@ jobs: --hidden-import scripts.plot_pos \ --hidden-import scripts.plot_trace_res \ --hidden-import scripts.GinanUI.app \ + --hidden-import scripts.GinanUI.app.main_window \ --hidden-import scripts.GinanUI.app.models \ --hidden-import scripts.GinanUI.app.models.execution \ + --hidden-import scripts.GinanUI.app.models.dl_products \ + --hidden-import scripts.GinanUI.app.models.rinex_extractor \ + --hidden-import scripts.GinanUI.app.models.archive_manager \ --hidden-import scripts.GinanUI.app.controllers \ --hidden-import scripts.GinanUI.app.controllers.input_controller \ + --hidden-import scripts.GinanUI.app.controllers.general_config_controller \ + --hidden-import scripts.GinanUI.app.controllers.constellation_config_controller \ + --hidden-import scripts.GinanUI.app.controllers.output_config_controller \ --hidden-import scripts.GinanUI.app.controllers.visualisation_controller \ --hidden-import scripts.GinanUI.app.utils \ --hidden-import scripts.GinanUI.app.utils.workers \ + --hidden-import scripts.GinanUI.app.utils.logger \ + --hidden-import scripts.GinanUI.app.utils.toast \ + --hidden-import scripts.GinanUI.app.utils.ui_compilation \ --hidden-import scripts.GinanUI.app.utils.cddis_credentials \ - --hidden-import scripts.GinanUI.app.utils.cddis_email \ + --hidden-import scripts.GinanUI.app.utils.cddis_connection \ --hidden-import scripts.GinanUI.app.utils.common_dirs \ --hidden-import scripts.GinanUI.app.utils.gn_functions \ --hidden-import scripts.GinanUI.app.utils.yaml \ --hidden-import scripts.GinanUI.app.views.main_window_ui \ scripts/GinanUI/main.py - + - name: Build GUI with PyInstaller (Windows) if: runner.os == 'Windows' run: | @@ -314,15 +324,25 @@ jobs: --hidden-import scripts.plot_pos ` --hidden-import scripts.plot_trace_res ` --hidden-import scripts.GinanUI.app ` + --hidden-import scripts.GinanUI.app.main_window ` --hidden-import scripts.GinanUI.app.models ` --hidden-import scripts.GinanUI.app.models.execution ` + --hidden-import scripts.GinanUI.app.models.dl_products ` + --hidden-import scripts.GinanUI.app.models.rinex_extractor ` + --hidden-import scripts.GinanUI.app.models.archive_manager ` --hidden-import scripts.GinanUI.app.controllers ` --hidden-import scripts.GinanUI.app.controllers.input_controller ` + --hidden-import scripts.GinanUI.app.controllers.general_config_controller ` + --hidden-import scripts.GinanUI.app.controllers.constellation_config_controller ` + --hidden-import scripts.GinanUI.app.controllers.output_config_controller ` --hidden-import scripts.GinanUI.app.controllers.visualisation_controller ` --hidden-import scripts.GinanUI.app.utils ` --hidden-import scripts.GinanUI.app.utils.workers ` + --hidden-import scripts.GinanUI.app.utils.logger ` + --hidden-import scripts.GinanUI.app.utils.toast ` + --hidden-import scripts.GinanUI.app.utils.ui_compilation ` --hidden-import scripts.GinanUI.app.utils.cddis_credentials ` - --hidden-import scripts.GinanUI.app.utils.cddis_email ` + --hidden-import scripts.GinanUI.app.utils.cddis_connection ` --hidden-import scripts.GinanUI.app.utils.common_dirs ` --hidden-import scripts.GinanUI.app.utils.gn_functions ` --hidden-import scripts.GinanUI.app.utils.yaml ` diff --git a/CHANGELOG.md b/CHANGELOG.md index d697cc503..97d0b0f84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,71 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +# [4.1.2] 2026-06-16 + +## Added + +Ginan core: +- Added RTCM extension work, including receiver metadata support and associated tests. +- Added diagnostics for preprocessor slip detection, including SCDIA diagnostics and clearer slip reason handling. +- Added dry-run options for checking configuration/execution flow without running a full processing job. +- Added support for SINEX PSD annual-frequency corrections. + +GinanUI: +- Added a YAML/HTML inspector path in GinanUI, including generated inspector styling and integration into the UI. +- Added a YAML configuration tab and controls for config overwrite behaviour. +- Added GinanUI support for ocean and atmospheric loading workflows. +- Added visualisation pop-out support. +- Added SINEX output controls to GinanUI. + +## Changed + +Ginan core: +- Improved realtime operation: config reloads now retire removed streams, clear removed inputs, keep trace/RTCM outputs open while appending, and improve sync/reconnect diagnostics. +- Improved stream/file handling by keeping file streams open across parses and reducing unnecessary open/close cycles. +- Improved data handling for multiple input streams, EOF handling, start/end epoch logic, and stream state checks. +- Updated preprocessor so basic preparation still runs when the preprocessor is disabled. + +GinanUI: +- Refactored GinanUI into smaller controllers/models for maintainability and accessibility. +- Improved GinanUI config update paths and visualisation controls. + +Build and dependencies: +- Added Eigen 5 compatibility using a simpler integration path after the initial migration approach proved unsuitable. +- Improved Eigen/BLAS compatibility and Windows portability. +- Added CI/vcpkg updates for dependency unit testing. + +## Fixed + +Ginan core: +- Fixed RTS/chunking output and chunk-parallel transition handling. +- Fixed loading grid longitude handling for 0-360 degree grids. +- Fixed unsafe `nullStream` behaviour in multi-threaded runs. + +GinanUI: +- Fixed duplicate `.pos` plotting for multi-day observations. +- Fixed restoration handling for `igs_satellite_metadata.snx`. + +# [4.1.1] 2026-02-12 + +## Added + +Ginan core: +- Added support for reading GLONASS satellites from RINEX 2 files. + +GinanUI: +- Added apriori position as a configuration option in the interface. +- Added support for running faster-rate clocks, including 1 Hz to 100 Hz workflows. +- Added SINEX downloading and validation support. +- Added download verification against CDDIS checksums. +- Added support for using archived products when they are already available. + +## Fixed + +Ginan core: +- Fixed reading CRLF-ended RINEX files in Windows binaries. +- Fixed configuration parsing so station and receiver names can start with a number, for example `4RMA00AUS`. + # [4.1] 2026-01-30 ## Added diff --git a/README.md b/README.md index 2c3b49367..e9821d7b7 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Ginan: GNSS Analysis Software Toolkit -[![Version](https://img.shields.io/badge/version-v4.1.1-blue.svg)](https://github.com/GeoscienceAustralia/ginan/releases) +[![Version](https://img.shields.io/badge/version-v4.1.2-blue.svg)](https://github.com/GeoscienceAustralia/ginan/releases) [![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE.md) [![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey.svg)](#supported-platforms) [![Docker](https://img.shields.io/badge/docker-available-blue.svg)](https://hub.docker.com/r/gnssanalysis/ginan) @@ -29,8 +29,7 @@ McClusky, Simon; Hammond, Aaron; Maj, Ronald; Allgeyer, Sébastien; Harima, Ken; - [Precompiled binaries](#precompiled-binaries) - [Installation from Source](#installation-from-source) - [Tested Platforms](#tested-platforms) - - [Prerequisites](#prerequisites) - - [Build Process using `vcpkg` + CMake presets (Recommanded)](#build-process-using-vcpkg--cmake-presets-recommanded) + - [Recommended Source Build: `vcpkg` + CMake Presets](#recommended-source-build-vcpkg--cmake-presets) - [Legacy: manual `cmake` + `make` instructions](#legacy-manual-cmake---make-instructions) - [Python Environment Setup](#python-environment-setup) - [Getting Started with the examples](#getting-started-with-the-examples) @@ -57,7 +56,7 @@ The fastest way to get started with Ginan is using Docker: ```bash # Pull and run the latest Ginan container -docker run -it -v $(pwd):/data gnssanalysis/ginan:v4.1.1 bash +docker run -it -v "$(pwd):/data" gnssanalysis/ginan:v4.1.2 bash # Verify installation pea --help @@ -112,26 +111,33 @@ The software consists of three main components: ## Installation -Choose the installation method that best fits your needs: +Choose one of the paths below: + +| Method | Best for | Notes | +|--------|----------|-------| +| Docker | Most users, tutorials, reproducible runs | Includes Ginan and its runtime dependencies. | +| Precompiled binaries | Users who want a native executable without building | Available from GitHub Releases for Linux, macOS, and Windows. | +| Source build with `vcpkg` | Developers and users who need a custom build | Recommended source-build path. | +| Legacy/manual source build | Sites with system-managed dependencies | Best-effort support; use only when `vcpkg` is not suitable. | ### Using Ginan with Docker -**Recommended for most users** - Get started quickly with a pre-configured environment: +**Recommended for most users.** Docker is the fastest way to run Ginan with a known-good environment. -```bash -# Run Ginan container with data volume mounting -docker run -it -v ${pwd}:/data gnssanalysis/ginan:v4.1.1 bash -``` +Prerequisite: install [Docker](https://docs.docker.com/get-docker/). -This command: +```bash +# Run Ginan container with data volume mounting on Linux/macOS +docker run -it -v "$(pwd):/data" gnssanalysis/ginan:v4.1.2 bash -- Mounts your current directory (`${pwd}`) to `/data` in the container -- Provides access to all Ginan tools and dependencies -- Opens an interactive bash shell +# PowerShell equivalent on Windows +docker run -it -v "${PWD}:/data" gnssanalysis/ginan:v4.1.2 bash +``` -**Prerequisites:** [Docker](https://docs.docker.com/get-docker/) must be installed on your system. +The command mounts your current directory to `/data` in the container and opens an interactive shell with Ginan available. **Verify installation:** + ```bash pea --help ``` @@ -146,7 +152,7 @@ We publish builds for the following platforms: - macOS (arm64 and x86_64) - Windows (x86_64) -These artifacts are provided for convenience and have been tested on our CI runners and a subset of target systems. They may not work on every configuration — if you encounter problems please try the Docker image or build from source (see the Build Process section) and open an issue on GitHub with your OS and steps to reproduce. +These artifacts are provided for convenience and have been tested on our CI runners and a subset of target systems. They may not work on every configuration — if you encounter problems please try the Docker image or build from source, and open an issue on GitHub with your OS and steps to reproduce. Note about Windows binaries: We have observed an output file-size limitation on Windows builds where RTS/output files appear limited at about 2.1 GB (roughly equivalent to a PPP processing of two stations over one day at 30 s resolution). If you require larger RTS outputs, run the processing on Linux/macOS (or in the Docker image) or build from source on a platform without this limitation. We plan to implement a permanent solution in a future release. @@ -162,80 +168,84 @@ Note about Windows binaries: We have observed an output file-size limitation on | **macOS** | 10.15+ (x86) | Limited testing | | **Windows** | 10+ | Limited testing| -#### Prerequisites - -##### System Dependencies +#### Recommended Source Build: `vcpkg` + CMake Presets -**Compilers:** +The recommended source build uses the repository's CMake presets and `vcpkg` manifest. This keeps dependency versions close to the CI/release builds. -- GCC/G++ (recommended, tested and supported) or equivalent C/C++ compiler +Prerequisites: -**Required Dependencies:** +- CMake 3.22 or newer +- A C/C++ compiler for your platform +- Git +- `vcpkg`, installed or cloned as shown below -- **CMake** ≥ 3.0 +From the repository root: -- **YAML** ≥ 0.6 - -- **Boost** ≥ 1.75 - - -- **Eigen3** ≥ 3.4 +```bash +export VCPKG_ROOT="$PWD/vcpkg" +export VCPKG_COMMIT="4c5ae6b55f3e3e39d291679f89822f496cf190ee" -- **OpenBLAS** (provides BLAS and LAPACK) +git clone https://github.com/Microsoft/vcpkg.git "$VCPKG_ROOT" +git -C "$VCPKG_ROOT" fetch --depth 1 origin "$VCPKG_COMMIT" +git -C "$VCPKG_ROOT" checkout --detach "$VCPKG_COMMIT" +"$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics +``` -**Optional Dependencies:** +If you already have `vcpkg` installed elsewhere, set `VCPKG_ROOT` to that directory and skip the clone commands. -- **Mongo C Driver** ≥ 1.17.1 +On Windows PowerShell: -- **Mongo C++ Driver** ≥ 3.6.0 (= 3.7.0 for GCC 11+) +```powershell +$env:VCPKG_ROOT = "$PWD\vcpkg" +$env:VCPKG_COMMIT = "4c5ae6b55f3e3e39d291679f89822f496cf190ee" -- **MongoDB** (for database features) +git clone https://github.com/Microsoft/vcpkg.git $env:VCPKG_ROOT +git -C $env:VCPKG_ROOT fetch --depth 1 origin $env:VCPKG_COMMIT +git -C $env:VCPKG_ROOT checkout --detach $env:VCPKG_COMMIT +& "$env:VCPKG_ROOT\bootstrap-vcpkg.bat" -disableMetrics +``` -- **netCDF4** (for tidal loading computation) +Then configure and build from `src` using the preset for your platform: -- **Python** ≥ 3.9 +```bash +cd src +cmake --preset release +cmake --build --preset release +``` -#### Build Process using `vcpkg` + CMake presets (Recommanded) +Common release presets: -We recommend using `vcpkg` for dependency management together with the repository CMake presets. +| Platform | Configure/build preset | +|----------|------------------------| +| Linux x86_64 | `release` | +| macOS Apple silicon | `macos-arm64-release` | +| macOS Intel | `macos-x64-release` | +| Windows native | `windows-release` | +| Windows cross-compile from Linux | `windows-cross-release` | -1. Bootstrap and install `vcpkg` (from repository root): +For example, on Apple silicon: ```bash -# Clone/bootstrap vcpkg (if not present) -./vcpkg/bootstrap-vcpkg.sh - -# Install packages for your target triplet (example: Linux x86_64) -./vcpkg/vcpkg install --triplet x64-linux --x-install-root=./vcpkg_installed -# For macOS: use `arm64-osx` or `x64-osx`. For Windows cross builds (on linux) use `x64-mingw-static`. +cd src +cmake --preset macos-arm64-release +cmake --build --preset macos-arm64-release ``` -2. Configure and build with a CMake preset (run from `src`): +Build outputs are written to the repository-level `bin/` and `lib/` directories. -```bash -cd src -# Choose the preset that matches your platform (examples: `release`, `macos-arm64-release`, `macos-x64-release`, `windows-cross-release`) -cmake --preset release -cmake --build --preset release +Verify the build: -# Or build the preset directory directly (example for Linux): -cmake --build build/linux-Release --parallel $(nproc) +```bash +../bin/pea --help ``` -Note on loading / netCDF: the ocean-tide loading components currently have known problems when built from the `vcpkg` dependency set due to issues with the `netcdf` package in some vcpkg triplets. If you rely on tidal-loading features (the `make_otl_blq` target and related tools), either: - -- Build those components from source using your system `netcdf` (install `netcdf`/`netcdf-c` via the OS package manager and use the legacy `cmake`/`make` flow), or -- Track the vcpkg `netcdf` fixes and retry when upstream provides a compatible package for your target triplet. - -If you need help reproducing or a suggested workaround for your platform, open an issue with your OS/triplet and vcpkg versions. +The CMake presets use platform-specific dependency install roots under `./vcpkg_installed/`. If a configure step fails after changing triplets or branches, remove the relevant preset build directory under `src/build/` and configure again. -Notes: -- The CI uses `--x-install-root=./vcpkg_installed` to install packages locally for reproducible builds. -- If you prefer not to use `vcpkg`, the legacy manual flow below remains supported. +**OpenBLAS threading:** Ginan can use OpenMP while OpenBLAS may also use worker threads. If you see warnings such as `OpenBLAS Warning : Detect OpenMP Loop and this application may hang`, set `OPENBLAS_NUM_THREADS=1` so BLAS/LAPACK calls run single-threaded inside Ginan's OpenMP regions. #### Legacy: manual `cmake` + `make` instructions -##### Quick Installation Scripts (legacy) +Use this path only if you need system-managed dependencies instead of `vcpkg`. Pre-written installation scripts are available in `scripts/installation/` for systems where you prefer distro-specific package installation instead of `vcpkg`: @@ -256,7 +266,7 @@ Pre-written installation scripts are available in `scripts/installation/` for sy cat scripts/installation/generic.md ``` -**Note:** These scripts are maintained as best-effort and may require adjustments for your environment. If you are using the `vcpkg` + CMake presets workflow, follow the `vcpkg` steps in the Build Process section instead. +These scripts are maintained as best-effort and may require adjustments for your environment. The older manual flow is still available for users who prefer it: @@ -294,7 +304,7 @@ cd ../../exampleConfigs Expected output: ``` -PEA starting... (main ginan-v4.1.1 from ...) +PEA starting... (main ginan-v4.1.2 from ...) Options: -h [ --help ] Help -q [ --quiet ] Less output @@ -330,9 +340,10 @@ Congratulations! Ginan is now ready to use. The examples in `exampleConfigs/` pr - **Working directory:** All examples must be run from the `exampleConfigs/` directory due to relative paths - **MongoDB:** If MongoDB is not installed, set `mongo: enable: None` in configuration files -- **Performance tip:** For single-station PPP, limit cores to improve performance: +- **Threading:** Ginan can use OpenMP while OpenBLAS may also use its own worker threads. If you see warnings such as `OpenBLAS Warning : Detect OpenMP Loop and this application may hang`, set `OPENBLAS_NUM_THREADS=1` so BLAS/LAPACK calls run single-threaded inside Ginan's OpenMP regions. `GOTO_NUM_THREADS=1` is also recognised by OpenBLAS-compatible builds. +- **Performance tip:** For single-station PPP, limit OpenMP cores to improve performance: ```bash - OMP_NUM_THREADS=1 ../bin/pea --config ppp_example.yaml + OPENBLAS_NUM_THREADS=1 GOTO_NUM_THREADS=1 OMP_NUM_THREADS=1 ../bin/pea --config ppp_example.yaml ``` @@ -462,4 +473,4 @@ All incorporated code has been preserved with appropriate modifications in the ` --- -**Developed by [Geoscience Australia](https://www.ga.gov.au/)** | **Version 4.1.1** | **[GitHub Repository](https://github.com/GeoscienceAustralia/ginan)** +**Developed by [Geoscience Australia](https://www.ga.gov.au/)** | **Version 4.1.2** | **[GitHub Repository](https://github.com/GeoscienceAustralia/ginan)** diff --git a/debugConfigs/data b/debugConfigs/data deleted file mode 120000 index 85479075e..000000000 --- a/debugConfigs/data +++ /dev/null @@ -1 +0,0 @@ -../inputData/data \ No newline at end of file diff --git a/debugConfigs/net_orbits_rt.yaml b/debugConfigs/net_orbits_rt.yaml deleted file mode 100644 index 8a13cc710..000000000 --- a/debugConfigs/net_orbits_rt.yaml +++ /dev/null @@ -1,474 +0,0 @@ -inputs: - inputs_root: products/ - tides: - ocean_tide_loading_blq_files: [ OLOAD_GO.BLQ ] - atmos_tide_loading_blq_files: [ ALOAD_GO.BLQ ] - ocean_pole_tide_loading_files: [ tables/opoleloadcoefcmcor.txt ] - ocean_tide_potential_files: [ tables/fes2014b_Cnm-Snm.dat ] - - atx_files: [ igs20.atx ] - snx_files: [ igs20.ssc, IGS0OPSSNX_20232600000_01D_01D_CRD.SNX, igs_satellite_metadata.txt.latest ] - egm_files: [ tables/EGM2008.gfc ] - erp_files: [ finals.daily.iau2000.txt ] - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] - satellite_data: - satellite_data_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - rtcm_inputs: - - "BCEP00BKG0" - troposphere: - gpt2grid_files: [gpt_25.grd] - gnss_observations: - gnss_observations_root: "http://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - - "ALIC00AUS0" - - "LAUT00FJI0" - - "PTGG00PHL0" - - "CHTI00NZL0" - - "MAYG00MYT0" - - "COCO00AUS0" - - "KRGG00ATF0" - - "KIRI00KIR0" - - "MAC100AUS0" - - "CAS100ATA0" - - "DGAR00GBR0" - - "ASCG00SHN0" - - "IISC00IND0" - - "FAIR00USA0" - #- "WHU200CHN0" - - "MIZU00JPN0" - - "CKIS00COK0" - - "RGDG00ARG0" - - "SCTB00ATA0" - - "MAW100ATA0" - - "YAR200AUS0" - - "TOW200AUS0" - - "DARW00AUS0" - - "KARR00AUS0" - - "REUN00REU0" - - "CZTG00ATF0" - - "MAL200KEN0" - - "DJIG00DJI0" - #- "KZN200RUS0" - #- "UTQI00USA0" - - "DUBO00CAN0" - - "YELL00CAN0" - - "PDEL00PRT0" - - "STHL00GBR0" - - "NKLG00GAB0" - - "SAVO00BRA0" - - "GAMB00PYF0" - - "GLPS00ECU0" - - "SGOC00LKA0" - - "POAL00BRA0" - - "FALK00FLK0" -# Rb Atomic Clocks - - "TASH00UZB0" - - "AREG00PER0" -# Cs Atomic Clocks - - "HARB00ZAF0" - - "MAS100ESP0" - - "KIRU00SWE0" - - "KOUG00GUF0" -# HM Atomic Clocks - - "BREW00USA0" - - "KOUR00GUF0" - - "MGUE00ARG0" - - "PIE100USA0" - - "MKEA00USA0" - - "STJO00CAN0" - - "TID100AUS0" - - "MSSA00JPN0" - - "ONS100SWE0" -outputs: - outputs_root: outputs/ - trace: - output_receivers: true - output_satellites: true - output_network: true - level: 5 - network_filename: _
--.TRACE - receiver_filename: _
--.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - clocks: - output: true - sinex: - output: true - sp3: - output: true - output_rotation: - period: 1 - period_units: day - decoded_rtcm: - output: true - metadata: - config_description: rt_net_ppp_orbits -mongo: - enable: both - output_measurements: primary - output_states: primary - delete_history: primary - #output_predictions: secondary - - secondary_database: Realtime - - queue_outputs: true - -receiver_options: - #WHU2: - #kill: true - #KZN2: - #kill: true - global: - - #elevation_mask: 10 - #error_model: elevation_dependent - #code_sigma: [0.4] - #phase_sigma: [0.004] - rec_reference_system: gps - - #models: - #attitude: - #sources: [ MODEL, NOMINAL ] - #troposphere: - #models: [gpt2] - #eop: - #enable: true - - GPS: - clock_codes: [ L1W,L2W ] - zero_dcb_codes: [ AUTO,AUTO ] - #GAL: - #zero_dcb_codes: [ L1C,L5Q ] - #mincon_noise: [ 0.01, 0.01, 0.03 ] -satellite_options: - global: - orbit_propagation: - mass: 1000 - area: 15 - srp_cr: 1.75 - central_force: true - planetary_perturbations: [sun, moon, jupiter] - general_relativity: true - solar_radiation_pressure: cannonball - antenna_thrust: true - albedo: cannonball - empirical: true - empirical_dyb_eclipse: [true, false, false] - models: - attitude: - sources: [ MODEL, NOMINAL ] - clock: - sources: [ KALMAN, BROADCAST ] - pos: - sources: [ KALMAN, PRECISE, BROADCAST ] - #GPS: - #clock_codes: [ L1W,L2W ] - #GAL: - #clock_codes: [ L1C,L5Q ] -processing_options: - - predictions: - forward_duration: 25 - offset: 30 - interval: 5 - - process_modes: - ppp: true - epoch_control: - epoch_interval: 60 - #wait_next_epoch: 8 - max_rec_latency: 2 - preprocessor: - cycle_slips: - mw_process_noise: 0 - slip_threshold: 0.05 - spp: - max_lsq_iterations: 12 - outlier_screening: - max_gdop: 30 - postfit: - sigma_check: true - gnss_general: - minimise_sat_clock_offsets: true - sys_options: # Only GPS and Galileo are currently stable - gps: - process: true - code_priorities: [ L1W, L1C, L2W ] - gal: - #process: true - code_priorities: [ L1C, L1X, L5Q, L5X ] - glo: - process: false - code_priorities: [ L1P, L1C, L2P, L2C ] - qzs: - process: false - code_priorities: [ L1C, L2L, L2X ] - bds: - process: false - code_priorities: [ L2I, L6I ] - minimum_constraints: - enable: true - #once_per_epoch: true - translation: - estimated: [ true ] - sigma: [ 1 ] - rotation: - estimated: [ true ] - sigma: [ 1 ] - scale: - estimated: [ true ] - sigma: [ 1 ] - model_error_handling: - ambiguities: - outage_reset_limit: 300 - phase_reject_limit: 2 - reset_on: - gf: true - lli: true - mw: true - scdia: true - meas_deweighting: - deweight_factor: 10000 - state_deweighting: - deweight_factor: 10000 - orbit_errors: - enable: true - pos_process_noise: 1000 - vel_process_noise: 10 - vel_process_noise_trail: 0.1 - vel_process_noise_trail_tau: 360 - exclusions: - gf: false - lli: false - mw: false - scdia: false - ppp_filter: - inverter: LLT - assume_linearity: true - chunking: - size: 25 - outlier_screening: - prefit: - max_iterations: 3 - omega_test: true - sigma_check: false - state_sigma_threshold: 5 - meas_sigma_threshold: 5 - postfit: - max_iterations: 20 - sigma_check: true - state_sigma_threshold: 5 - meas_sigma_threshold: 5 - - chi_square: - mode: NONE - enable: false - - orbit_propagation: - egm_field: true - integrator_time_step: 60 - egm_degree: 15 - indirect_J2: true - solid_earth_tide: true - ocean_tide: true - pole_tide_ocean: true - pole_tide_solid: true - - #itrf_pseudoobs: true -estimation_parameters: - receivers: - global: - pos: - estimated: [true] - sigma: [5] - process_noise: [0.001] - process_noise_dt: DAY - - clock: - estimated: [true] - sigma: [1000] - process_noise: [100] - - ambiguities: - estimated: [true] - sigma: [5000] - - trop: - estimated: [true] - sigma: [0.3] - process_noise: [0.0001] - - trop_grads: - estimated: [true] - sigma: [0.02] - process_noise: [1e-6] - - ion_stec: - estimated: [true] - sigma: [200] - - # phase_bias: - # estimated: [true] - # sigma: [1000] - # proc_noise: [0.0001] - - code_bias: - estimated: [true] - sigma: [10] - tau: [1800] - process_noise: [0.003] - process_noise_dt: DAY - satellites: - GPS-IIA: - emp_d_0: - apriori_value: [36] - emp_y_0: - apriori_value: [0] - emp_b_0: - apriori_value: [0] - emp_d_1: - apriori_value: [0] - emp_b_1: - apriori_value: [0] - emp_d_2: - apriori_value: [0] - GPS-IIF: - emp_d_0: - apriori_value: [-34] - emp_y_0: - apriori_value: [0] - emp_b_0: - apriori_value: [0] - emp_d_1: - apriori_value: [0] - emp_b_1: - apriori_value: [0] - emp_d_2: - apriori_value: [0] - GPS-IIR-A: - emp_d_0: - apriori_value: [9] - emp_y_0: - apriori_value: [0] - emp_b_0: - apriori_value: [0] - emp_d_1: - apriori_value: [0] - emp_b_1: - apriori_value: [0] - emp_d_2: - apriori_value: [0] - GPS-IIR-B: - emp_d_0: - apriori_value: [8] - emp_y_0: - apriori_value: [0] - emp_b_0: - apriori_value: [0] - emp_d_1: - apriori_value: [0] - emp_b_1: - apriori_value: [0] - emp_d_2: - apriori_value: [0] - GPS-IIR-M: - emp_d_0: - apriori_value: [12] - emp_y_0: - apriori_value: [0] - emp_b_0: - apriori_value: [0] - emp_d_1: - apriori_value: [0] - emp_b_1: - apriori_value: [0] - emp_d_2: - apriori_value: [0] - GPS-IIIA: - emp_d_0: - apriori_value: [-22] - sigma: [10] - emp_y_0: - apriori_value: [0] - emp_b_0: - apriori_value: [0] - emp_d_1: - apriori_value: [0] - emp_b_1: - apriori_value: [0] - emp_d_2: - apriori_value: [0] - global: - clock: - estimated: [true] - sigma: [1000] - process_noise: [1] - tau: [100] - - #clk_rate: - #estimated: [true] - #sigma: [10] - #proc_noise: [1e-5] - - phase_bias: - estimated: [false] - sigma: [10] - #proc_noise: [0] - - code_bias: - estimated: [true] - sigma: [100] - tau: [1800] - process_noise: [0.003] - process_noise_dt: DAY - - orbit: - estimated: [true] - sigma: [5 , 5, 5, 0.01] - process_noise: [0] - emp_d_0: - estimated: [true] - sigma: [1] - process_noise: [1] - process_noise_dt: day - emp_p_0: - estimated: [true] - sigma: [5] - process_noise: [1] - process_noise_dt: day - emp_q_0: - estimated: [true] - sigma: [5] - process_noise: [1] - process_noise_dt: day - #emp_y_0: - #estimated: [true] - #sigma: [1] - #proc_noise: [0] - #emp_b_0: - #estimated: [true] - #sigma: [1] - #proc_noise: [0] - #emp_d_1: - #estimated: [false] - #sigma: [1] - #proc_noise: [0] - #emp_y_1: - #estimated: [false] - #sigma: [1] - #proc_noise: [0] - #emp_b_1: - #estimated: [true] - #sigma: [1] - #proc_noise: [0] - #emp_d_2: - #estimated: [true] - #sigma: [1] - #proc_noise: [0] -debug: - #explain_measurements: true - #instrument: true - #instrument_once_per_epoch: true diff --git a/debugConfigs/net_slr_estimation_off.yaml b/debugConfigs/net_slr_estimation_off.yaml deleted file mode 100644 index 79f2cd081..000000000 --- a/debugConfigs/net_slr_estimation_off.yaml +++ /dev/null @@ -1,266 +0,0 @@ -# ex44 - Network SLR Example - -inputs: - - inputs_root: products/ - - # atx_files: [ igs20.atx ] # required - snx_files: [ IGS1R03SNX_20192000000_01D_01D_CRD.SNX, - slr/ecc_une.snx, # SLR station eccentricities - slr/ILRS_Data_Handling_File_2024.02.13.snx, # SLR station biases - slr/ITRF2014-ILRS-TRF-SSC.SNX, # SLR station positions + drifts - tables/igs_satellite_metadata_2203_plus.snx ] # required - erp_files: [ igs19P2062.erp ] - egm_files: [ tables/EGM2008.gfc ] # Earth gravity model coefficients file - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] # JPL planetary and lunar ephemerides file - - satellite_data: - sp3_files: [ slr/ilrsa.orb.lageos1.190720.v71.sp3 ] - # bsx_files: [ IGS2R03FIN_20191990000_01D_01D_OSB.BIA ] - sid_files: [ slr/sp3c-satlist.txt ] - com_files: [ slr/com/com_lageos.txt ] - crd_files: [ slr/obs/lageos1/lageos1_201907.npt ] - - tides: - ocean_tide_loading_blq_files: [ slr/OLOAD_SLR.BLQ ] # required if ocean loading is applied - ocean_tide_potential_files: [ tables/fes2014b_Cnm-Snm.dat ] - - # troposphere: - # orography_files: [ orography_ell_5x5 ] - # gpt2grid_files: [ gpt_25.grd ] - # vmf_files: [ grid5/VMF3_20190718.H00, - # grid5/VMF3_20190718.H06, - # grid5/VMF3_20190718.H12, - # grid5/VMF3_20190718.H18, - # grid5/VMF3_20190719.H00 ] - - # gnss_observations: - # gnss_observations_root: data/ - -outputs: - - metadata: - config_description: slr_est_off - - outputs_root: outputs// - colourise_terminal: false - # warn_once: false - - trace: - output_receivers: true - output_network: true - level: 2 - receiver_filename: --.TRACE - network_filename: --.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - - log: - output: true - directory: ./ - filename: log_.json - - output_rotation: - period: 1 - period_units: day - - # bias_sinex: - # output: false - # code_output_interval: 900.0 - # directory: ./ - # filename: -.BIA - # output_rec_bias: false - # phase_output_interval: 900.0 - - # clocks: - # output: false - # directory: ./ - # filename: .clk - - sinex: - output: true - - erp: - output: false - - sp3: - output: false - output_interval: 1 - output_inertial: false - # output_predicted_orbits: true - output_velocities: true - orbit_sources: [KALMAN] - clock_sources: [PRECISE] - - slr_obs: - output: true - directory: ./slr_obs/ - filename: .slr_obs - -mongo: - - enable: primary - primary_database: - output_config: primary - output_measurements: primary - output_components: primary - output_states: primary - output_test_stats: primary - output_trace: primary - delete_history: primary - primary_uri: mongodb://127.0.0.1:27017 - primary_suffix: "" - -receiver_options: - - global: - elevation_mask: 10 # degrees - error_model: elevation_dependent - # code_sigmas: [0.3333] - # phase_sigmas: [0.0033] - laser_sigma: 0.10 - rec_reference_system: GPS - - models: - # troposphere: - # enable: true - # models: [vmf3] # gpt2 - - eop: - enable: true - -satellite_options: - - global: - models: - pos: - enable: true - sources: [PRECISE] - - # attitude: - # enable: true # (bool) Enables non-nominal attitude types - # sources: [NOMINAL] # List of sourecs to use for attitudes - -processing_options: - - epoch_control: - start_epoch: 2019-07-14 00:00:18 - end_epoch: 2019-07-20 23:58:18 - # max_epochs: 12 # 0 is infinite # comment for full day run - epoch_interval: 60 # seconds - require_obs: true - assign_closest_epoch: true - - process_modes: - ppp: true - slr: true # Process SLR observations - preprocessor: true - spp: false - - gnss_general: - # common_sat_pco: false - # pivot_receiver: "USN7" # if not provided then will be selected automatically - - sys_options: - # gps: - # process: false - # ambiguity_resolution: false - # reject_eclipse: false - # code_priorities: [L1C, L1P, L1Y, L1W, L1M, L1N, L1S, L1L, L1X, - # L2W, L2P, L2Y, L2C, L2M, L2N, L2D, L2S, L2L, L2X, - # # L5I, L5Q, L5X - # ] - - leo: # includes Lageos1 - process: true - - # spp: - # outlier_screening: - # max_gdop: 30 - # raim: true - - ppp_filter: - # ionospheric_components: - # corr_mode: iono_free_linear_combo - # use_if_combo: false - - inverter: ldlt # LLT LDLT INV - - outlier_screening: - prefit: - max_iterations: 10 # Maximum number of measurements to exclude using prefit checks before attempting to filter - - postfit: - max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter - - # rts: - # enable: true - # lag: -1 # -ve for full reverse, +ve for limited epochs - # directory: ./ - # filename: -Netwuseork.rts - - model_error_handling: - meas_deweighting: - deweight_factor: 1000 - - # ambiguities: - # outage_reset_limit: 1 - # phase_reject_limit: 2 - -estimation_parameters: - - global_models: - eop: - estimated: [false] - sigma: [30] - process_noise: [0.0000036] - - eop_rates: - estimated: [false] - sigma: [10] - process_noise: [0] - - receivers: - global: - pos: - estimated: [false] - sigma: [1.0] - process_noise: [0] - - slr_range_bias: - estimated: [false] - sigma: [0.01] - process_noise: [0] - - slr_time_bias: - estimated: [false] - sigma: [0.00001] - process_noise: [0] - - satellites: - global: - orbit: - estimated: [false] - sigma: [5e-1, 5e-1, 5e-1, 5e-3, 5e-3, 5e-3] # posX/Y/Z, velX/Y/Z (final element repeated as necessary) - process_noise: [0] - - # emp_r_0: { estimated: [false], sigma: [5e-6] } - emp_t_0: { estimated: [false], sigma: [5e-6] } - # emp_n_0: { estimated: [false], sigma: [5e-6] } - - # emp_r_1: { estimated: [false], sigma: [5e-6] } - emp_t_1: { estimated: [false], sigma: [5e-6] } - emp_n_1: { estimated: [false], sigma: [5e-6] } - - # emp_r_2: { estimated: [false], sigma: [5e-6] } - # emp_t_2: { estimated: [false], sigma: [5e-6] } - # emp_n_2: { estimated: [false], sigma: [5e-6] } - - # emp_r_3: { estimated: [false], sigma: [5e-6] } - # emp_t_3: { estimated: [false], sigma: [5e-6] } - # emp_n_3: { estimated: [false], sigma: [5e-6] } - - # emp_r_4: { estimated: [false], sigma: [5e-6] } - # emp_t_4: { estimated: [false], sigma: [5e-6] } - # emp_n_4: { estimated: [false], sigma: [5e-6] } diff --git a/debugConfigs/net_slr_estimation_on.yaml b/debugConfigs/net_slr_estimation_on.yaml deleted file mode 100644 index 86372af69..000000000 --- a/debugConfigs/net_slr_estimation_on.yaml +++ /dev/null @@ -1,266 +0,0 @@ -# ex44 - Network SLR Example - -inputs: - - inputs_root: products/ - - # atx_files: [ igs20.atx ] # required - snx_files: [ IGS1R03SNX_20192000000_01D_01D_CRD.SNX, - slr/ecc_une.snx, # SLR station eccentricities - slr/ILRS_Data_Handling_File_2024.02.13.snx, # SLR station biases - slr/ITRF2014-ILRS-TRF-SSC.SNX, # SLR station positions + drifts - tables/igs_satellite_metadata_2203_plus.snx ] # required - erp_files: [ igs19P2062.erp ] - egm_files: [ tables/EGM2008.gfc ] # Earth gravity model coefficients file - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] # JPL planetary and lunar ephemerides file - - satellite_data: - sp3_files: [ slr/ilrsa.orb.lageos1.190720.v71.sp3 ] - # bsx_files: [ IGS2R03FIN_20191990000_01D_01D_OSB.BIA ] - sid_files: [ slr/sp3c-satlist.txt ] - com_files: [ slr/com/com_lageos.txt ] - crd_files: [ slr/obs/lageos1/lageos1_201907.npt ] - - tides: - ocean_tide_loading_blq_files: [ slr/OLOAD_SLR.BLQ ] # required if ocean loading is applied - ocean_tide_potential_files: [ tables/fes2014b_Cnm-Snm.dat ] - - # troposphere: - # orography_files: [ orography_ell_5x5 ] - # gpt2grid_files: [ gpt_25.grd ] - # vmf_files: [ grid5/VMF3_20190718.H00, - # grid5/VMF3_20190718.H06, - # grid5/VMF3_20190718.H12, - # grid5/VMF3_20190718.H18, - # grid5/VMF3_20190719.H00 ] - - # gnss_observations: - # gnss_observations_root: data/ - -outputs: - - metadata: - config_description: slr_est_rec_pos_biases - - outputs_root: outputs// - colourise_terminal: false - # warn_once: false - - trace: - output_receivers: true - output_network: true - level: 2 - receiver_filename: --.TRACE - network_filename: --.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - - log: - output: true - directory: ./ - filename: log_.json - - output_rotation: - period: 1 - period_units: day - - # bias_sinex: - # output: false - # code_output_interval: 900.0 - # directory: ./ - # filename: -.BIA - # output_rec_bias: false - # phase_output_interval: 900.0 - - # clocks: - # output: false - # directory: ./ - # filename: .clk - - sinex: - output: true - - erp: - output: false - - sp3: - output: false - output_interval: 1 - output_inertial: false - # output_predicted_orbits: true - output_velocities: true - orbit_sources: [KALMAN] - clock_sources: [PRECISE] - - slr_obs: - output: true - directory: ./slr_obs/ - filename: .slr_obs - -mongo: - - enable: primary - primary_database: - output_config: primary - output_measurements: primary - output_components: primary - output_states: primary - output_test_stats: primary - output_trace: primary - delete_history: primary - primary_uri: mongodb://127.0.0.1:27017 - primary_suffix: "" - -receiver_options: - - global: - elevation_mask: 10 # degrees - error_model: elevation_dependent - # code_sigmas: [0.3333] - # phase_sigmas: [0.0033] - laser_sigma: 0.10 - rec_reference_system: GPS - - models: - # troposphere: - # enable: true - # models: [vmf3] # gpt2 - - eop: - enable: true - -satellite_options: - - global: - models: - pos: - enable: true - sources: [PRECISE] - - # attitude: - # enable: true # (bool) Enables non-nominal attitude types - # sources: [NOMINAL] # List of sourecs to use for attitudes - -processing_options: - - epoch_control: - start_epoch: 2019-07-14 00:00:18 - end_epoch: 2019-07-20 23:58:18 - # max_epochs: 12 # 0 is infinite # comment for full day run - epoch_interval: 60 # seconds - require_obs: true - assign_closest_epoch: true - - process_modes: - ppp: true - slr: true # Process SLR observations - preprocessor: true - spp: false - - gnss_general: - # common_sat_pco: false - # pivot_receiver: "USN7" # if not provided then will be selected automatically - - sys_options: - # gps: - # process: false - # ambiguity_resolution: false - # reject_eclipse: false - # code_priorities: [L1C, L1P, L1Y, L1W, L1M, L1N, L1S, L1L, L1X, - # L2W, L2P, L2Y, L2C, L2M, L2N, L2D, L2S, L2L, L2X, - # # L5I, L5Q, L5X - # ] - - leo: # includes Lageos1 - process: true - - # spp: - # outlier_screening: - # max_gdop: 30 - # raim: true - - ppp_filter: - # ionospheric_components: - # corr_mode: iono_free_linear_combo - # use_if_combo: false - - inverter: ldlt # LLT LDLT INV - - outlier_screening: - prefit: - max_iterations: 10 # Maximum number of measurements to exclude using prefit checks before attempting to filter - - postfit: - max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter - - # rts: - # enable: true - # lag: -1 # -ve for full reverse, +ve for limited epochs - # directory: ./ - # filename: -Netwuseork.rts - - model_error_handling: - meas_deweighting: - deweight_factor: 1000 - - # ambiguities: - # outage_reset_limit: 1 - # phase_reject_limit: 2 - -estimation_parameters: - - global_models: - eop: - estimated: [false] - sigma: [30] - process_noise: [0.0000036] - - eop_rates: - estimated: [false] - sigma: [10] - process_noise: [0] - - receivers: - global: - pos: - estimated: [true] - sigma: [1.0] - process_noise: [0] - - slr_range_bias: - estimated: [true] - sigma: [0.01] - process_noise: [0] - - slr_time_bias: - estimated: [true] - sigma: [1e4] - process_noise: [0] - - satellites: - global: - orbit: - estimated: [false] - sigma: [5e-1, 5e-1, 5e-1, 5e-3, 5e-3, 5e-3] # posX/Y/Z, velX/Y/Z (final element repeated as necessary) - process_noise: [0] - - # emp_r_0: { estimated: [false], sigma: [5e-6] } - emp_t_0: { estimated: [false], sigma: [5e-6] } - # emp_n_0: { estimated: [false], sigma: [5e-6] } - - # emp_r_1: { estimated: [false], sigma: [5e-6] } - emp_t_1: { estimated: [false], sigma: [5e-6] } - emp_n_1: { estimated: [false], sigma: [5e-6] } - - # emp_r_2: { estimated: [false], sigma: [5e-6] } - # emp_t_2: { estimated: [false], sigma: [5e-6] } - # emp_n_2: { estimated: [false], sigma: [5e-6] } - - # emp_r_3: { estimated: [false], sigma: [5e-6] } - # emp_t_3: { estimated: [false], sigma: [5e-6] } - # emp_n_3: { estimated: [false], sigma: [5e-6] } - - # emp_r_4: { estimated: [false], sigma: [5e-6] } - # emp_t_4: { estimated: [false], sigma: [5e-6] } - # emp_n_4: { estimated: [false], sigma: [5e-6] } diff --git a/debugConfigs/pea b/debugConfigs/pea deleted file mode 120000 index 64c880319..000000000 --- a/debugConfigs/pea +++ /dev/null @@ -1 +0,0 @@ -../bin/pea \ No newline at end of file diff --git a/debugConfigs/pea1.yaml b/debugConfigs/pea1.yaml deleted file mode 100644 index 7cb05e0e8..000000000 --- a/debugConfigs/pea1.yaml +++ /dev/null @@ -1,1053 +0,0 @@ - -outputs: - metadata: - config_description: "PEA1" - - root_directory: outputs// - - trace: - level: 3 - output_stations: true - output_network: true - network_filename: -Network.trace - station_filename: -.trace - #output_residuals: true - #output_residual_chain: true - output_config: false - - bias_sinex: - #output: true - - clocks: - #output: true - - sinex: - output: true - - erp: - #output: true - - log: - #output: true - - trop_sinex: - #output: true - sp3: - #output: true - #directory: ./ - #filename: GAA0GINRAP_00_01D_05M_ORB.SP3 - #output_inertial: false # (bool) Output the entries using inertial positions and velocities - #output_interval: 300 # (int) Update interval for sp3 records - #output_velocities: false - #orbit_sources: [ KALMAN ] - #clock_sources: [ KALMAN ] - - streams: - #root_url: "${OUTPUT_STREAMS__STREAM_ROOT}" - #labels: - #- GAA1 - #GAA1: - #url: "${OUTPUT_STREAMS__MOUNTPOINT_PREFIX}$${OUTPUT_STREAM_NUMBER}" - #messages: - #rtcm_1060: {udi: 10} - #rtcm_1059: {udi: 10} - -mongo: - #enable: true - uri: mongodb://127.0.0.1:27017 - database: - #output_rtcm_messages: true - #output_components: true - output_states: true - #output_measurements: true - #output_test_stats: true - output_trace: true - delete_history: true - suffix: "" - predict_states: false - -remote_mongo: - enable: true - uri: mongodb://127.0.0.1:27017 - database: "MULTI-GINAN" - predict_states: true - prediction_interval: 2 - prediction_offset: 20 - interval_units: seconds - forward_prediction_duration: 20 - duration_units: seconds - delete_history: true - cull_history: true - - - - -estimation_parameters: - - eop: - estimated: - - true - sigma: - - 30 - eop_rates: - estimated: - - true - sigma: - - 30 - satellites: - global: - clk: - estimated: - - true - proc_noise: - - 1 - sigma: - - 1000 - clk_rate: - estimated: - - false - proc_noise: - - 1e-06 - sigma: - - 0.005 - code_bias: - estimated: - - true - proc_noise: - - 0 - sigma: - - 10 - orbit: - estimated: - - true - proc_noise: - - 0 - - 0 - - 0 - - 0 - - 0 - - 0 - sigma: - - 0.5 - - 0.5 - - 0.5 - - 0.001 - emp_dyb_0: - estimated: - - true - - true - - true - proc_noise: - - 0 - sigma: - - 30 - - 1 - - 1 - emp_dyb_1c: - estimated: - - true - - false - - true - proc_noise: - - 0 - sigma: - - 1 - - 1 - - 1 - emp_dyb_1s: - estimated: - - true - - false - - true - proc_noise: - - 0 - sigma: - - 1 - - 1 - - 1 - emp_dyb_2c: - estimated: - - true - - false - - false - proc_noise: - - 0 - sigma: - - 1 - - 1 - - 1 - emp_dyb_2s: - estimated: - - true - - false - - false - proc_noise: - - 0 - sigma: - - 1 - - 1 - - 1 - emp_dyb_3c: - apriori_val: - - 0.0 - estimated: - - false - - false - - false - proc_noise: - - 0 - sigma: - - 1 - - 1 - - 1 - emp_dyb_3s: - estimated: - - false - - false - - false - proc_noise: - - 0 - sigma: - - 1 - - 1 - - 1 - emp_dyb_4c: - estimated: - - true - - false - - false - proc_noise: - - 0 - sigma: - - 1 - - 1 - - 1 - emp_dyb_4s: - estimated: - - true - - false - - false - proc_noise: - - 0 - sigma: - - 1 - - 1 - - 1 - GPS: - L1W: - code_bias: - apriori_val: - - 0 - estimated: - - true - sigma: - - 1e-08 - L2W: - code_bias: - apriori_val: - - 0 - estimated: - - true - sigma: - - 1e-08 - stations: - global: - amb: - estimated: - - true - proc_noise: - - 0 - sigma: - - 5000 - clk: - estimated: - - true - proc_noise: - - 100.0 - sigma: - - 1000 - clk_rate: - estimated: - - false - proc_noise: - - 0.0001 - sigma: - - 0.005 - code_bias: - estimated: - - true - proc_noise: - - 0 - sigma: - - 10 - code_sigmas: - - 0.5 - error_model: elevation_dependent - ion_stec: - estimated: - - true - proc_noise: - - 1.0 - sigma: - - 1000 - phase_sigmas: - - 0.005 - pos: - estimated: - - true - proc_noise: - - 0 - sigma: - - 2.0 - trop: - estimated: - - true - proc_noise: - - 8.3333e-05 - sigma: - - 0.3 - trop_grads: - estimated: - - true - proc_noise: - - 3.6e-06 - sigma: - - 0.01 - #PIVOT: - UNSA: - clk: - estimated: - - false - code_bias: - estimated: - - false -inputs: - root_directory: products/ - atx_files: [ igs20.atx,M20.ATX ] - otl_blq_files: [ OLOAD_GO.BLQ ] - atl_blq_files: [ ALOAD_GO.BLQ ] - opole_files: [ tables/opoleloadcoefcmcor.txt ] - erp_files: - - igu.erp - - IGS0OPSULT_20232420000_02D_01D_ERP.ERP - - IGS0OPSULT_20232400000_02D_01D_ERP.ERP - - IGS0OPSULT_20232410000_02D_01D_ERP.ERP - egm_files: [ tables/EGM2008.gfc ] - egm_files: - - goco05s.gfc - jpl_files: [ tables/DE436.1950.2050 ] - tide_files: [ tables/fes2014b_Cnm-Snm.dat ] - igrf_files: [ tables/igrf13coeffs.txt ] - #erp_files: [ tables/finals.data.iau2000.txt ] - snx_files: - - IGS1R03SNX_20191950000_07D_07D_CRD.SNX - - IGS0OPSSNX_20230220000_07D_07D_CRD.SNX - #- meta.snx - - tables/igs_satellite_metadata_2203_plus.snx - - satellite_data: - inputs_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - - BCEP00BKG0 - - gnss_observations: - inputs_root: ../data/ - rnx_inputs: - #- MCM400ATA_R_20232420000_01D_30S_MO.rnx - - "*.rnx" - #inputs_root: "https://:@ntrip.data.gnss.ga.gov.au/" - #rtcm_inputs: - #- ALIC00AUS0 - #- ANDA00AUS0 - #- ARMC00AUS0 - #- ARUB00AUS0 - #- BALA00AUS0 - #- BBOO00AUS0 - #- BDLE00AUS0 - #- BDVL00AUS0 - #- BEEC00AUS0 - #- BMAN00AUS0 - #- BNDY00AUS0 - #- BRLA00AUS0 - #- BRO100AUS0 - #- BROC00AUS0 - #- BULA00AUS0 - #- BUR200AUS0 - #- BURA00AUS0 - #- CAS100ATA0 - #- CBLA00AUS0 - #- CCPL00AUS0 - #- CEDU00AUS0 - #- CNVN00AUS0 - #- CTWR00AUS0 - #- COCO00AUS0 - #- COEN00AUS0 - #- COOB00AUS0 - #- COOL00AUS0 - #- DARH00AUS0 - #- DARW00AUS0 - #- DAV100ATA0 - #- DGEE00AUS0 - #- DERB00AUS0 - #- DIRA00AUS0 - #- DODA00AUS0 - #- EDS100AUS0 - #- EDSV00AUS0 - #- ERMG00AUS0 - #- ESPA00AUS0 - #- EXMT00AUS0 - #- FLND00AUS0 - #- FROY00AUS0 - ##- GDAM00AUS0 - #- GGT100AUS0 - #- GGTN00AUS0 - #- GROT00AUS0 - #- HNIS00AUS0 - #- HOB200AUS0 - #- HUGH00AUS0 - #- HYDN00AUS0 - #- IGWD00AUS0 - #- ILKA00AUS0 - #- IMKA00AUS0 - #- JAB200AUS0 - #- JERV00AUS0 - #- JLCK00AUS0 - #- KALG00AUS0 - #- KARR00AUS0 - #- KAT100AUS0 - #- KAT200AUS0 - #- KELN00AUS0 - #- KGIS00AUS0 - #- KILK00AUS0 - #- KMAN00AUS0 - #- KNDH00AUS0 - #- KOWN00AUS0 - #- KUNU00AUS0 - #- LAMB00AUS0 - #- LARR00AUS0 - #- LAVE00AUS0 - #- LIAW00AUS0 - #- LONA00AUS0 - #- LORD00AUS0 - #- LURA00AUS0 - #- MAC100AUS0 - #- MAIN00AUS0 - #- MAW100ATA0 - #- MCHL00AUS0 - #- MEDO00AUS0 - #- MILD00AUS0 - - troposphere: - gpt2grid_files: gpt_25.grd - - - gnss_observations: - inputs_root: ../data/ - jpl_files: - - DE436.1950.2050 - root_directory: products/ - satellite_data: - #bsx_files: - #- code_monthly.bia - #inputs_root: ./products - nav_files: - - brdc2420.23n - - brdc2400.23n - - brdc2410.23n - sp3_files: - - IGS0OPSULT_20232410000_02D_15M_ORB.SP3 - snx_files: - - igs_satellite_metadata.snx - - meta.snx - - IGS0OPSSNX_20232250000_07D_07D_CRD.SNX - tide_files: - - fes2004_Cnm-Snm.dat - troposphere: - gpt2grid_files: gpt_25.grd -processing_options: -<<<<<<< HEAD -======= - - epoch_control: - require_obs: false - epoch_interval: 2 - #wait_next_epoch: 3600 # Wait up to an hour for next data point - When processing RINEX causes PEA to wait a long as need for last epoch to be processed. - wait_all_stations: 1 - #fatal_message_level: 1 - - process_modes: - ppp: true - - gnss_general: - rec_reference_system: gps - sys_options: - gps: - process: true - reject_eclipse: false - code_priorities: - - L1C - #- L1W - - L2W - #- L5Q - #- L5X - ambiguity_resolution: false - zero_receiver_dcb: true - gal: - #process: true - reject_eclipse: false - code_priorities: - - L1C - #- L1W - #- L2W - - L5Q - #- L5X - - elevation_mask: 10 - raim: true - pivot_station: "ALIC" - interpolate_rec_pco: false - auto_fill_pco: true - max_gdop: 30 - ->>>>>>> assumeLinear - gnss_models: - troposphere: - model: gpt2 - ionospheric_component: - enable: true - use_if_combo: true - - model_error_checking: - deweighting: - deweight_factor: 10000 - reject_on_state_error: true - ambiguities: - outage_reset_limit: 5 - phase_reject_limit: 2 - reinit_on_all_slips: true # (bool) Any detected slips cause removal and reinitialisation of ambiguities - clocks: - reinit_on_clock_error: true # (bool) Any clock "state" errors cause removal and reinitialisation of the clocks and all associated ambiguities - - cycle_slips: - slip_threshold: 0.05 - preprocessor_options: - preprocess_all_data: false - - filter_options: - outlier_screening: - max_filter_iterations: 20 - max_prefit_removals: 3 - rts: - enable: false - #assume_linearity: true - - minimum_constraints: - enable: true - once_per_epoch: false # (bool) Perform minimum constraints on a temporary filter and output results once per epoch - translation: - estimated: [true] - rotation: - estimated: [true] - scale: - estimated: [false] - inverter: LDLT - max_filter_iterations: 20 - max_prefit_removals: 3 # (int) Maximum number of measurements to exclude using prefit checks before attempting to filter - outlier_screening: - chi_square_mode: none # (enum) Chi-square test mode - innovation, measurement, state {NONE,INNOVATION,MEASUREMENT,STATE} - chi_square_test: false # (bool) Enable Chi-square test - sigma_check: true # (bool) Enable prefit and postfit sigma check - sigma_threshold: 3.000000 # (float) sigma threshold - w_test: false # (bool) Enable w-test - station_noise: - global: [0.005, 0.005, 0.01] - - epoch_control: - epoch_interval: 300 - filter_options: - inverter: LLT - outlier_screening: - max_filter_iterations: 50 - max_prefit_removals: 5 - gnss_general: - rec_reference_system: GPS - elevation_mask: 15 - error_model: elevation_dependent - max_gdop: 30 - #pivot_station: - raim: true - sys_options: - gps: - ambiguity_resolution: false - code_priorities: - - L1W - - L1C - - L2W - process: true - reject_eclipse: false - zero_receiver_dcb: true - gnss_models: - ionospheric_component: - enable: true - common_ionosphere: true # Code and Phase measurment share the same ionosphere - use_if_combo: false - corr_mode: IONO_FREE_LINEAR_COMBO - orbits: - enable: true - eop: - enable: true - minimum_constraints: - once_per_epoch: false - enable: true - rotation: - estimated: - - true - scale: - estimated: - - true - station_noise: - ABMF: - - 0.01 - ALBH: - - 0.01 - ALIC: - - 0.01 - AREQ: - - 0.01 - CAS1: - - 0.01 - CHPI: - - 0.01 - CHUR: - - 0.01 - CUSV: - - 0.01 - DAEJ: - - 0.01 - DARW: - - 0.01 - DAV1: - - 0.01 - DGAR: - - 0.01 - FAIR: - - 0.01 - FALK: - - 0.01 - FLIN: - - 0.01 - GLPS: - - 0.01 - HOB2: - - 0.01 - IISC: - - 0.01 - KIRI: - - 0.01 - KIT3: - - 0.01 - KOKV: - - 0.01 - LAUT: - - 0.01 - LPGS: - - 0.01 - MAC1: - - 0.01 - MATE: - - 0.01 - MAW1: - - 0.01 - MBAR: - - 0.01 - MCM4: - - 0.01 - MOBS: - - 0.01 - PIE1: - - 0.01 - POL2: - - 0.01 - QAQ1: - - 0.01 - RIO2: - - 0.01 - SANT: - - 0.01 - SCH2: - - 0.01 - SCOR: - - 0.01 - STHL: - - 0.01 - STJO: - - 0.01 - SUTM: - - 0.01 - THU2: - - 0.01 - TID1: - - 0.01 - TOW2: - - 0.01 - UNSA: - - 0.01 - VACS: - - 0.01 - WHIT: - - 0.01 - XMIS: - - 0.01 - YAR2: - - 0.01 - YELL: - - 0.01 - translation: - estimated: - - true - model_error_checking: - ambiguities: - outage_reset_limit: 10 - phase_reject_limit: 2 - reinit_on_all_slips: true - cycle_slips: - slip_threshold: 0.05 - clocks: - reinit_on_clock_error: false - deweighting: - deweight_factor: 10000 - reject_on_state_error: true - orbit_propagation: -<<<<<<< HEAD - albedo: true - antenna_thrust: true - central_force: true - degree_max: 15 - egm_field: true - empirical: true - empirical_dyb_eclipse: [true] - general_relativity: true - indirect_J2: true - integrator_time_step: 60 - itrf_pseudoobs: true - #ocean_tide: true #quite slow - planetary_perturbation: true - pole_tide_ocean: true - pole_tide_solid: true - sat_area: 15 - sat_mass: 1000 - solar_radiation_pressure: true - solid_earth_tide: true - srp_cr: 1.75 - process_modes: - ppp: true - preprocessor: true - spp: true -======= - central_force: true - planetary_perturbation: true - indirect_J2: true - egm_field: true - solid_earth_tide: true - #ocean_tide: true #quite slow - general_relativity: true - pole_tide_ocean: true - pole_tide_solid: true - solar_radiation_pressure: true - antenna_thrust: true - empirical: true #true/false => false/ecom/srf - albedo: true - integrator_time_step: 60 - sat_mass: 1000 - sat_area: 15 - srp_cr: 1.75 - degree_max: 15 - - itrf_pseudoobs: true - ->>>>>>> assumeLinear - -satellite_options: - E05: { exclude: true } - E06: { exclude: true } - E10: { exclude: true } - E16: { exclude: true } - E17: { exclude: true } - E23: { exclude: true } - E28: { exclude: true } - E29: { exclude: true } - E32: { exclude: true } - E34: { exclude: true } - E35: { exclude: true } - G04: { exclude: true } - G01: - exclude: true - G28: - exclude: true - - global: - clock: - sources: [KALMAN, PRECISE, BROADCAST] - enable: true - pos: - sources: [KALMAN, PRECISE, BROADCAST] - attitude: - enable: true - sources: [PRECISE, MODEL, NOMINAL] - pco: - enable: true - pcv: - enable: true - code_bias: - default_bias: 0 - enable: true - undefined_sigma: 0 - phase_bias: - default_bias: 0 - enable: false - undefined_sigma: 0 - - -station_options: - global: - ant_delta: - enable: true - attitude: - enable: true - sources: [PRECISE, MODEL, NOMINAL] - clock: - enable: true - pco: - enable: true - pcv: - enable: true - code_bias: - default_bias: 0 - enable: true - undefined_sigma: 0 - phase_bias: - default_bias: 0 - enable: false - undefined_sigma: 0 - pos: - enable: true - -<<<<<<< HEAD -station_options: - global: - ant_delta: - enable: true - attitude: - enable: true - sources: - - PRECISE - - MODEL - - NOMINAL - clock: - enable: true - pco: - enable: true - pcv: - enable: true - code_bias: - default_bias: 0 - enable: true - undefined_sigma: 0 - phase_bias: - default_bias: 0 - enable: false - undefined_sigma: 0 - pos: - enable: true -======= -estimation_parameters: - - stations: - error_model: elevation_dependent - code_sigmas: [0.4] - phase_sigmas: [0.002] - global: - pos: - estimated: [true] - sigma: [1] - proc_noise: [0.001] - proc_noise_dt: day - clk: - estimated: [true] - sigma: [1000] - proc_noise: [10] - amb: - estimated: [true] - sigma: [100] - trop: - estimated: [true] - sigma: [0.3] - proc_noise: [0.0001] - trop_grads: - estimated: [true] - sigma: [0.03] - proc_noise: [1.0E-6] - ion_stec: - estimated: [true] - sigma: [100] - proc_noise: [0.2] - code_bias: - estimated: [true] - sigma: [20] - PIVOT: - clk: - estimated: [false] - code_bias: - estimated: [false] - satellites: - pseudo_sigmas: [1e6] - global: - clk: - estimated: [true] - sigma: [1000] - proc_noise: [10] - code_bias: - estimated: [true] - sigma: [10] - proc_noise: [0] - orbit: - estimated: [true] - sigma: [0.5, 0.5, 0.5, 0.001] - proc_noise: [0] - emp_dyb_0: - estimated: [true, true, true] - sigma: [1e-8, 1e-9, 1e-9] - apriori_val: [0] - proc_noise: [0] - #emp_dyb_1c: - #estimated: [true, false, true] - #sigma: [1e-9, 1e-9, 1e-9] - #apriori_val: [0.0] - #proc_noise: [0] - #emp_dyb_1s: - #estimated: [true, false, true] - #sigma: [1e-9, 1e-9, 1e-9] - #apriori_val: [0.0] - #proc_noise: [0] - #emp_dyb_2c: - #estimated: [true, false, false] - #sigma: [1e-9, 1e-9, 1e-9] - #apriori_val: [0.0] - #proc_noise: [0] - #emp_dyb_2s: - #estimated: [true, false, false] - #sigma: [1e-9, 1e-9, 1e-9] - #apriori_val: [0.0] - #proc_noise: [0] - #emp_dyb_3c: - #estimated: [false, false, false] - #sigma: [1e-9, 1e-9, 1e-9] - #apriori_val: [0.0] - #proc_noise: [0] - #emp_dyb_3s: - #estimated: [false, false, false] - #sigma: [1e-9, 1e-9, 1e-9] - #apriori_val: [0.0] - #proc_noise: [0] - #emp_dyb_4c: - #estimated: [true, false, false] - #sigma: [1e-9, 1e-9, 1e-9] - #apriori_val: [0.0] - #proc_noise: [0] - #emp_dyb_4s: - #estimated: [true, false, false] - #sigma: [1e-9, 1e-9, 1e-9] - #apriori_val: [0.0] - #proc_noise: [0] - GPS-IIA: - emp_dyb_0: - apriori_val: [+3.6476e-08, -8.6715e-10, -1.1070e-09] - emp_dyb_1c: - apriori_val: [2.3015e-09, 0.0, 4.9846e-10] - emp_dyb_1s: - apriori_val: [8.2564e-11, 0.0, 1.4100e-09] - emp_dyb_2c: - apriori_val: [2.6432e-10, 0.0, 0.0] - emp_dyb_2s: - apriori_val: [-1.4925e-09, 0.0, 0.0] - emp_dyb_4c: - apriori_val: [1.5456e-09, 0.0, 0.0] - emp_dyb_4s: - apriori_val: [3.8351e-10, 0.0, 0.0] - GPS-IIR-A: - emp_dyb_0: - apriori_val: [+8.53e-09, 8.97e-10, 1.5e-09] - emp_dyb_1c: - apriori_val: [-1.75e-09, 0, 8.02e-10] - emp_dyb_1s: - apriori_val: [-6.16e-11, 0, 1.42e-10] - emp_dyb_2c: - apriori_val: [-1.50e-09, 0, 0] - emp_dyb_2s: - apriori_val: [+2.20e-10, 0, 0] - emp_dyb_4c: - apriori_val: [+1.86e-09, 0, 0] - emp_dyb_4s: - apriori_val: [-5.52e-11, 0, 0] - GPS-IIR-B: - emp_dyb_0: - apriori_val: [+7.97e-09, -8.94e-11, 2.25e-09] - emp_dyb_1c: - apriori_val: [-2.97e-09, 0, -8.82e-10] - emp_dyb_1s: - apriori_val: [+1.47e-11, 0, +4.28e-11] - emp_dyb_2c: - apriori_val: [-8.06e-10, 0, 0] - emp_dyb_2s: - apriori_val: [+7.33e-10, 0, 0] - emp_dyb_4c: - apriori_val: [+2.50e-09, 0, 0] - emp_dyb_4s: - apriori_val: [+4.37e-10, 0, 0] - GPS-IIR-M: - emp_dyb_0: - apriori_val: [+1.15e-08, 6.01e-10, 1.82e-09] - emp_dyb_1c: - apriori_val: [-1.75e-09, 0, 1.33e-09] - emp_dyb_1s: - apriori_val: [+5.01e-11, 0, 4.60e-10] - emp_dyb_2c: - apriori_val: [-1.95e-09, 0, 0] - emp_dyb_2s: - apriori_val: [-6.13e-10, 0, 0] - emp_dyb_4c: - apriori_val: [+1.69e-09, 0, 0] - emp_dyb_4s: - apriori_val: [-9.45e-10, 0, 0] - GPS-IIF: - emp_dyb_0: - apriori_val: [-3.44e-08, -4.32e-11, 3.07e-10] - emp_dyb_1c: - apriori_val: [+5.13e-10, 0, 6.85e-10] - emp_dyb_1s: - apriori_val: [-4.86e-13, 0, 2.51e-10] - emp_dyb_2c: - apriori_val: [-9.48e-10, 0, 0] - emp_dyb_2s: - apriori_val: [+3.29e-10, 0, 0] - emp_dyb_4c: - apriori_val: [+5.11e-10, 0, 0] - emp_dyb_4s: - apriori_val: [+3.95e-10, 0, 0] - GPS: - L1W: - code_bias: - sigma: [1e-8] # this implements B(s,GPS-L1W)=0 - process_noise: [0] - apriori_value: [0] - L2W: - code_bias: - sigma: [1e-8] # this implements B(s,GPS-L2W)=0 - process_noise: [0] - apriori_value: [0] - eop: - #estimated: [true] - sigma: [10] ->>>>>>> assumeLinear - - diff --git a/debugConfigs/pod_rt_example1.yaml b/debugConfigs/pod_rt_example1.yaml deleted file mode 100644 index 461457fc4..000000000 --- a/debugConfigs/pod_rt_example1.yaml +++ /dev/null @@ -1,444 +0,0 @@ -inputs: - - include_yamls: [ products/boxwing.yaml ] # required if using boxwing model - - inputs_root: ./products/ - - atx_files: [ igs20.atx ] # required - egm_files: [ tables/EGM2008.gfc ] # Earth gravity model coefficients file - igrf_files: [ tables/igrf13coeffs.txt ] - # erp_files: [ /latestProducts/finals.data.iau2000.txt ] - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] - - pseudo_observations: - # filter_files: [/pseudo.filter] - - troposphere: - gpt2grid_files: [ tables/gpt_25.grd ] - - tides: - ocean_tide_loading_blq_files: [ /latestProducts/tables/OLOAD_GO.BLQ ] # required if ocean loading is applied - atmos_tide_loading_blq_files: [ /latestProducts/tables/ALOAD_GO.BLQ ] # required if atmospheric tide loading is applied - ocean_pole_tide_loading_files: [ tables/opoleloadcoefcmcor.txt ] # required if ocean pole tide loading is applied - ocean_tide_potential_files: [ tables/fes2014b_Cnm-Snm.dat ] - - snx_files: - # - "*.SNX" # use a wild card to include all files matching the description in the directory - # - igs20.ssc - - "/latestProducts/*.SNX" - - "/latestProducts/*.snx" - - tables/sat_yaw_bias_rate.snx - - tables/qzss_yaw_modes.snx - - tables/bds_yaw_modes.snx - - satellite_data: - #satellite_data_root: "" - rtcm_inputs: # This section specifies how State State Representation (SSR) corrections are applied after they are downloaded from an NTRIP caster - #rtcm_inputs_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs_root: "https://:@ntrip.data.gnss.ga.gov.au/" # Root path to be added to all other rtcm inputs (unless they are absolute) - rtcm_inputs: - - "BCEP00BKG0" - gnss_observations: - #gnss_observations_root: "https://:@ntrip.data.gnss.ga.gov.au/" - gnss_observations_root: "https://:@ntrip.data.gnss.ga.gov.au/" # Root path to be added to all other gnss data inputs (unless they are absolute) - rtcm_inputs: - - "ALIC00AUS0" - - "LAUT00FJI0" - - "PTGG00PHL0" - - "CHTI00NZL0" - - "MAYG00MYT0" - - "COCO00AUS0" - - "KRGG00ATF0" - - "KIRI00KIR0" - - "MAC100AUS0" - - "CAS100ATA0" - - "DGAR00GBR0" - - "ASCG00SHN0" - - "IISC00IND0" - - "FAIR00USA0" - # - "ULAB00MNG0" - #- "WHU200CHN0" - #- "JFNG00CHN0" - #- "CHBN00MNG0" - - "MIZU00JPN0" - - "CKIS00COK0" - - "RGDG00ARG0" - - "SCTB00ATA0" - - "MAW100ATA0" - - "YAR200AUS0" - - "TOW200AUS0" - - "DARW00AUS0" - # - "KARR00AUS0" - - "REUN00REU0" - - "CZTG00ATF0" - - "MAL200KEN0" - - "DJIG00DJI0" - # - "KZN200RUS0" - - "UTQI00USA0" - # - "DUBO00CAN0" - - "YELL00CAN0" - - "PDEL00PRT0" - - "STHL00GBR0" - - "STR100AUS0" - - "STR200AUS0" - - "NKLG00GAB0" - - "SAVO00BRA0" - - "GAMB00PYF0" - - "GLPS00ECU0" - - "SGOC00LKA0" - - "POAL00BRA0" - - "FALK00FLK0" -# Rb Atomic Clocks - - "TASH00UZB0" - - "AREG00PER0" -# Cs Atomic Clocks - - "HARB00ZAF0" - - "MAS100ESP0" - - "KIRU00SWE0" - - "KOUG00GUF0" -# HM Atomic Clocks - - "BREW00USA0" - - "KOUR00GUF0" - - "MGUE00ARG0" - - "PIE100USA0" - - "MKEA00USA0" - - "STJO00CAN0" - - "TID100AUS0" - - "MSSA00JPN0" - - "ONS100SWE0" - -outputs: - - outputs_root: outputs/ - - metadata: - config_description: dual_slow - time_system: G # (string) Time system - e.g. "G", "UTC" - - trace: - level: 5 - output_receivers: true - output_network: true - receiver_filename: _.TRACE - network_filename: _.TRACE - output_residuals: true - output_residual_chain: true - output_predicted_states: true - output_config: true - - streams: - labels: - - GAA1 - GAA1: - url: https://ginan-isg-testing:p4_bL8-ctt7tn4ddZ@ntrip.test-data.gnss.ga.gov.au/SSRA00ISG8 - messages: - rtcm_1060: {udi: 10} - rtcm_1059: {udi: 10} - -satellite_options: - - global: - - #clock_codes: [AUTO, AUTO] - - models: - clock: - enable: true - sources: [KALMAN, PRECISE, BROADCAST] - pos: - enable: true - sources: [KALMAN, PRECISE, BROADCAST] - attitude: - enable: true - sources: [MODEL, PRECISE, NOMINAL] - pco: - enable: true - pcv: - enable: true - code_bias: - enable: true - phase_bias: - enable: false - - orbit_propagation: - albedo: cannonball - antenna_thrust: true - empirical: true - empirical_dyb_eclipse: [true,false,false] - planetary_perturbations: [moon,sun,mercury,venus,mars,jupiter,saturn,uranus,neptune,pluto] - pseudo_pulses: - enable: false - solar_radiation_pressure: boxwing - mass: 1000 - area: 15 - srp_cr: 1.75 - power: 20 - - GPS: - clock_codes: [ L1W,L2W ] - - #G04: - # exclude: true - - E05: { exclude: true } - E06: { exclude: true } - E10: { exclude: true } - E16: { exclude: true } - E17: { exclude: true } - E23: { exclude: true } - E28: { exclude: true } - E29: { exclude: true } - E32: { exclude: true } - E34: { exclude: true } - E35: { exclude: true } - - -receiver_options: # Options to configure individual stations or global configs - - STJO: - aliases: [PIVOT] - - global: - - error_model: elevation_dependent # uniform, elevation_dependent - elevation_mask: 10 - code_sigma: 0.4 - phase_sigma: 0.002 # F0, F1, F2, F5, F6, F7, F8 - clock_codes: [AUTO, AUTO] - zero_dcb_codes: [NONE,NONE] - rec_reference_system: gps - models: - eccentricity: - enable: true # (bool) Enable modelling of antenna eccentricities - attitude: - enable: true # (bool) Enables non-nominal attitude types - sources: [MODEL, NOMINAL] # List of sourecs to use for attitudes - clock: - enable: true # (bool) Enable modelling of clocks - pco: - enable: true # (bool) Enable modelling of phase center offsets - pcv: - enable: true # (bool) Enable modelling of phase center variations - code_bias: - enable: true # (bool) Enable modelling of code biases - phase_bias: - enable: false # (bool) Enable modelling of phase biases - pos: - enable: true # (bool) Enable modelling of position - ionospheric_components: # Ionospheric models produce frequency-dependent effects - enable: true # Enable ionospheric modelling - # use_2nd_order: true - # use_3rd_order: true - troposphere: - enable: true - models: [gpt2] - eop: - enable: true - - #mincon_scale_apriori_sigma: 0 # Use ALL fixed and/or SINEX file sigma's (!! first preference to the fixed sigma's !!) - apriori_sigma_enu: [0.003, 0.003, 0.009] # Use these fixed igma'sfor sites listed below - mincon_scale_apriori_sigma: 1 # Use ALL fixed and/or SINEX file sigma's (!! first preference to the fixed sigma's !!) - mincon_scale_filter_sigma: 0 - -processing_options: - - process_modes: - preprocessor: true - spp: true - ppp: true - ionosphere: false - - epoch_control: - epoch_interval: 20 - max_rec_latency: 5 - - gnss_general: - minimise_sat_clock_offsets: true - # minimise_sat_orbit_offsets: true - minimise_ionosphere_offsets: true - adjust_rec_clocks_by_spp: true - sys_options: - gps: - process: true - ambiguity_resolution: false - reject_eclipse: false - code_priorities: [ L1C, L2W ] - spp: - always_reinitialise: false - max_lsq_iterations: 12 - outlier_screening: - max_gdop: 30 - postfit: - sigma_check: true - - ppp_filter: - advanced_postfits: true - ionospheric_components: - common_ionosphere: true - # use_if_combo: true - outlier_screening: - prefit: - max_iterations: 2 - sigma_check: true - state_sigma_threshold: 5 - meas_sigma_threshold: 5 - omega_test: false - postfit: - max_iterations: 10 - sigma_check: true - state_sigma_threshold: 3 - meas_sigma_threshold: 3 - - model_error_handling: - meas_deweighting: - deweight_factor: 10000 - state_deweighting: - deweight_factor: 10000 - ambiguities: - outage_reset_limit: 300 - phase_reject_limit: 2 - reset_on: - gf: true - lli: true - mw: true - scdia: true - exclusions: - gf: true - lli: true - mw: true - scdia: true - eclipse: false - ionospheric_components: - outage_reset_limit: 300 - orbit_errors: - enable: false - pos_process_noise: 10 - vel_process_noise: 1 - vel_process_noise_trail: 0 - vel_process_noise_trail_tau: 0 - - orbit_propagation: - integrator_time_step: 30 # Timestep for the integrator, must be smaller than the processing time step, might be adjusted if the processing time step isn't a integer number of time steps - central_force: true - egm_field: true # Acceleration due to the high degree model of the Earth gravity model (exclude degree 0, made by central_force) - egm_degree: 15 # J2 acceleration perturbation due to the Sun and Moon - solid_earth_tide: true # Model accelerations due to solid earth tides - ocean_tide: true # Model accelerations due to ocean tides model - pole_tide_solid: true # Model accelerations due to solid pole tide (degree 2 only) - pole_tide_ocean: true - general_relativity: true - indirect_J2: true - - predictions: - offset: 20 - interval: 5 - forward_duration: 20 - -estimation_parameters: - - global_models: - eop: - estimated: [true] - sigma: [10,10,1e-9] - eop_rates: - estimated: [true] - sigma: [10] - - receivers: - - PIVOT: - #clock: - # estimated: [true] - # process_noise: [0] - # sigma: [1e-9] - code_bias: - estimated: [false] - - global: - pos: - estimated: [true] - sigma: [1] - process_noise: [0.0] - # process_noise_dt: second - clock: - estimated: [true] - sigma: [10] - process_noise: [10] # [100] - ambiguities: - estimated: [true] - sigma: [1000] - process_noise: [0] - # process_noise_dt: day - trop: - estimated: [true] - sigma: [0.3] - process_noise: [0.0001] - # process_noise_dt: second - trop_grads: - estimated: [true] - sigma: [0.03] - process_noise: [1.0E-6] - # process_noise_dt: second - ion_stec: - estimated: [true] - sigma: [500] - process_noise: [10] - # tau: [3600] - code_bias: - estimated: [true] - sigma: [20] - process_noise: [0] - # USN7: - # clk: - # estimated: [false] # Set reference (pivot) station clock - # code_bias: - # estimated: [false] - - satellites: - global: - clock: - estimated: [true] - sigma: [1000] - process_noise: [1] - tau: [30] - #mu: [10000] - code_bias: - estimated: [true] - sigma: [10] - process_noise: [0] - orbit: # Orbital state - estimated: [true] # [bools] Estimate state in kalman filter - sigma: [1, 1, 1, 0.01] # [floats] Apriori sigma values - if zero, will be initialised using least squares - process_noise: [0] - tau: [1000] - - emp_d_0: { estimated: [true], sigma: [10]} - emp_y_0: { estimated: [true], sigma: [1]} - emp_b_0: { estimated: [true], sigma: [1]} - - emp_b_1: { estimated: [true], sigma: [1]} - - emp_d_2: { estimated: [true], sigma: [1]} - - -mongo: - enable: both - #enable: none - output_components: primary - output_states: primary - output_measurements: primary - output_cumulative: primary - output_test_stats: none - delete_history: both - - - output_predictions: secondary - # sent_predictions: [all] - secondary_database: XFER - - -debug: - check_broadcast_differences: true - #output_mincon: true - #mincon_filename: preMinconState.bin - #mincon_only: true diff --git a/debugConfigs/pod_rt_example2.yaml b/debugConfigs/pod_rt_example2.yaml deleted file mode 100644 index 8464509e7..000000000 --- a/debugConfigs/pod_rt_example2.yaml +++ /dev/null @@ -1,76 +0,0 @@ -inputs: - - include_yamls: [ pod_rt_example1.yaml ] # required if using boxwing model - -outputs: - metadata: - config_description: dual_fast - -satellite_options: - - global: - - models: - clock: - sources: [KALMAN, REMOTE] - pos: - sources: [REMOTE] - -receiver_options: - - global: - # exclude: true - models: - pos: - sources: [REMOTE] - # run both with same epoch interval so they have the same epochs and just look for differencess - - GLPS: - # exclude: false -processing_options: - gnss_general: - adjust_rec_clocks_by_spp: false - - epoch_control: - # epoch_interval: 2 - max_rec_latency: 6 - - minimum_constraints: - enable: false - -estimation_parameters: - - receivers: - global: - pos: - estimated: [false] - code_bias: - estimated: [false] - clock: - use_remote_sigma: [true] - # sigma: [100] - process_noise: [-1] # [100] - ambiguities: - use_remote_sigma: [true] - trop: - use_remote_sigma: [true] - trop_grads: - use_remote_sigma: [true] - ion_stec: - use_remote_sigma: [true] - satellites: - global: - code_bias: - estimated: [false] - orbit: - estimated: [false] - clock: - use_remote_sigma: [true] - -mongo: - - output_predictions: none - use_predictions: secondary - used_predictions: [all] - delete_history: primary - diff --git a/debugConfigs/products b/debugConfigs/products deleted file mode 120000 index 5450abd31..000000000 --- a/debugConfigs/products +++ /dev/null @@ -1 +0,0 @@ -../inputData/products \ No newline at end of file diff --git a/debugConfigs/record_ssr_stream.yaml b/debugConfigs/record_ssr_stream.yaml deleted file mode 100644 index 40982949a..000000000 --- a/debugConfigs/record_ssr_stream.yaml +++ /dev/null @@ -1,107 +0,0 @@ -# Record and decode SSR stream - -inputs: - - inputs_root: ./products/latest/ - - atx_files: - - igs20.atx - - snx_files: - - igs_satellite_metadata.snx - - tables/sat_yaw_bias_rate.snx - - tables/bds_yaw_modes.snx - - tables/qzss_yaw_modes.snx - - gnss_observations: - gnss_observations_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - - DUMMY # needs something here to force the pea running at given epoch interval - - satellite_data: - satellite_data_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - ssr_antenna_offset: APC - rtcm_inputs: - - BCEP00BKG0 - - SSRA00BKG0 - -outputs: - - colourise_terminal: false - - metadata: - config_description: SSRA00BKG0 - - outputs_root: ./outputs/record_ssr_streams// - - output_rotation: - period: 86400 - - log: - output: true - filename: __LOG.json - - rtcm_nav: - output: true - filename: __NAV.rtcm - - decoded_rtcm: - output: true - filename: __DEC.json - - rinex_nav: - output: true - filename: __NAV.rnx - - clocks: - output: true - filename: __CLK.clk - receiver_sources: [ NONE ] - satellite_sources: [ SSR ] - output_interval: 30 - - sp3: - output: true - filename: __ORB.sp3 - orbit_sources: [ SSR ] - clock_sources: [ SSR ] - output_interval: 300 - -satellite_options: - - global: - models: - pos: - enable: true - sources: [ SSR ] - clock: - enable: true - sources: [ SSR ] - -processing_options: - - process_modes: - spp: false - - epoch_control: - require_obs: false - epoch_interval: 10 - wait_next_epoch: 10 - max_rec_latency: 1 - sleep_milliseconds: 1 - - gnss_general: - common_sat_pco: true - delete_old_ephemerides: true - sys_options: - gps: - process: true - gal: - process: true - glo: - process: true - bds: - process: true - qzs: - process: true diff --git a/debugConfigs/rt_net_ppp_clocks_large.yaml b/debugConfigs/rt_net_ppp_clocks_large.yaml deleted file mode 100644 index 1ebf7d017..000000000 --- a/debugConfigs/rt_net_ppp_clocks_large.yaml +++ /dev/null @@ -1,52 +0,0 @@ - -inputs: - include_yamls: - - rt_net_ppp_orbits_large.yaml -outputs: - metadata: - config_description: rt_net_ppp_clocks - trace: - level: 5 -satellite_options: - global: - attitude: - sources: [ MODEL, NOMINAL ] - clock: - sources: [ KALMAN, BROADCAST ] - pos: - sources: [ REMOTE ] -processing_options: - epoch_control: - epoch_interval: 5 - wait_all_stations: 2 - gnss_general: - minimise_sat_clock_offsets: true - minimise_rec_bias_offsets: false - sys_options: - gps: - process: true - gal: - process: false - gnss_models: - ionospheric_component: - use_if_combo: true - minimum_constraints: - enable: false - -remote_mongo: - use_predictions: true - predict_states: false -estimation_parameters: - stations: - global: - pos: - estimated: [false] - satellites: - global: - code_bias: - estimated: [false] - orbit: - estimated: [false] -debug: - #instrument: true - #instrument_once_per_epoch: true diff --git a/debugConfigs/rt_sisnet_input.yaml b/debugConfigs/rt_sisnet_input.yaml deleted file mode 100644 index 791b4f5b3..000000000 --- a/debugConfigs/rt_sisnet_input.yaml +++ /dev/null @@ -1,229 +0,0 @@ -inputs: - - inputs_root: ./products/ - - atx_files: [ igs20.atx ] - egm_files: [ tables/EGM2008.gfc ] - igrf_files: [ tables/igrf13coeffs.txt ] - erp_files: [ finals.data.iau2000.txt ] - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] - - troposphere: - # gpt2grid_files: [ gpt_25.grd ] - - tides: - # ocean_tide_loading_blq_files: [ OLOAD_GO.BLQ ] - # atmos_tide_loading_blq_files: [ tables/ALOAD_GO.BLQ ] - # ocean_pole_tide_loading_files: [ tables/opoleloadcoefcmcor.txt ] - # ocean_tide_potential_files: [ tables/fes2014b_Cnm-Snm.dat ] - - snx_files: [ tables/igs_satellite_metadata_2203_plus.snx ] - - gnss_observations: - gnss_observations_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - - ALIC00AUS0 - - MAW100ATA0 - - DARW00AUS0 - - STR200AUS0 - - - satellite_data: - rtcm_inputs_root: "https://:@ntrip.data.gnss.ga.gov.au/" - rtcm_inputs: - ssr_antenna_offset: APC - rtcm_inputs: - - BCEP00BKG0 - - SSRA00BKG0 - sisnet_inputs: - sisnet_inputs: ["sisnet://:@sisnet.data.gnss.ga.gov.au:61001/"] - sbas_prn: 122 - sbas_carrier_frequency: 1 - # net_inputs: ["sisnet://:@sisnet.data.gnss.ga.gov.au:61005/"] - # sbas_prn: 122 - # sbas_carrier_frequency: 5 - -outputs: - metadata: - config_description: sisnet_monitor - - outputs_root: ./outputs/ - - trace: - level: 2 - output_receivers: false - output_network: true - receiver_filename: __.TRACE - network_filename: __.TRACE - output_residuals: false - output_residual_chain: false - output_config: true - - sbas_ems: - output: true - directory: OS-L1-INT/y/d/ - filename: h.ems - - - output_rotation: - period: 3600 - -satellite_options: - - global: - models: - pos: - enable: true - sources: [SSR] - clock: - enable: true - sources: [SSR] - code_bias: - enable: true - undefined_sigma: 3 - phase_bias: - enable: true - undefined_sigma: 3 - - -receiver_options: - - global: - elevation_mask: 15 # degrees - error_model: ELEVATION_DEPENDENT # {uniform,elevation_dependent} - code_sigma: 0.3 # Standard deviation of code measurements, m - phase_sigma: 0.003 # Standard deviation of phase measurmeents, m - rec_reference_system: GPS - models: - phase_bias: - enable: true - - ALIC: - receiver_type: "SEPT POLARX5" # (string) - antenna_type: "LEIAR25.R3 NONE" # (string) - apriori_position: [-4052052.8638, 4212835.9618,-2545104.4038] # [floats] - models: - eccentricity: - enable: true - offset: [0.0000, 0.0000, 0.0015] # [floats] - - MAW1: - receiver_type: "SEPT POLARX5" # (string) - antenna_type: "AOAD/M_T AUST" # (string) - apriori_position: [ 1111287.2209, 2168911.1847,-5874493.6128] # [floats] - models: - eccentricity: - enable: true - offset: [0.0000, 0.0000, 0.0035] # [floats] - - DARW: - receiver_type: "SEPT POLARX5" # (string) - antenna_type: "JAVRINGANT_DM NONE" # (string) - apriori_position: [-4091359.7273, 4684606.3705,-1408578.9291] # [floats] - models: - eccentricity: - enable: true - offset: [0.0000, 0.0000, 0.0000] # [floats] - - STR2: - receiver_type: "TRIMBLE ALLOY" # (string) - antenna_type: "LEIAR25.R3 NONE" # (string) - apriori_position: [-4467075.3642, 2683011.8533,-3667006.8945] # [floats] - models: - eccentricity: - enable: true - offset: [0.0000, 0.0000, 0.0000] # [floats] - - -processing_options: - - process_modes: - ppp: true - - epoch_control: - epoch_interval: 30 - max_rec_latency: 1 - # max_epochs: 180 - - gnss_general: - # use_rtk_combo: true - # common_atmosphere: true - sys_options: - gps: - process: true - reject_eclipse: false - # clock_codes: [ L1W, L2W ] - code_priorities: [ L1W, L1C, L2W ] - ambiguity_resolution: false - -estimation_parameters: - - satellites: - global: - clock: - estimated: [true] - sigma: [1000] - process_noise: [10] - phase_bias: - estimated: [true] - sigma: [10] - # process_noise: [-1] - - - receivers: - BASE: - pos: - estimated: [false] - clock: - # estimated: [false] - - global: - pos: - estimated: [true] - sigma: [100] - process_noise: [0] - clock: - estimated: [true] - sigma: [1000] - process_noise: [100] - ambiguities: - estimated: [true] - sigma: [1000] - process_noise: [0] - ion_stec: # Ionospheric slant delay - estimated: [true] # Estimate state in kalman filter - sigma: [200] # Apriori sigma values - if zero, will be initialised using least squares - process_noise: [10] # Process noise sigmas - trop: - estimated: [true] - sigma: [0.3] - process_noise: [0.0001] - trop_grads: - estimated: [true] - sigma: [0.03] - process_noise: [1.0E-6] - code_bias: - estimated: [true] # false - sigma: [30] - process_noise: [0] - phase_bias: - # estimated: [true] - sigma: [10] - process_noise: [0] - - -mongo: # Mongo is a database used to store results and intermediate values for later analysis and inter-process communication - - enable: primary # Enable and connect to mongo database {none,primary,secondary,both} - primary_uri: mongodb://localhost:27017 # Location and port of the mongo database to connect to - primary_database: - output_components: primary # Output components of measurements {none,primary,secondary,both} - output_states: primary # Output states {none,primary,secondary,both} - output_measurements: primary # Output measurements and their residuals {none,primary,secondary,both} - output_test_stats: primary # Output test statistics {none,primary,secondary,both} - delete_history: primary # Drop the collection in the database at the beginning of the run to only show fresh data {none,primary,secondary,both} - - -debug: - # explain_measurements: true - # instrument: true diff --git a/debugConfigs/slr_est_off_gal.yaml b/debugConfigs/slr_est_off_gal.yaml deleted file mode 100644 index 453d20a2b..000000000 --- a/debugConfigs/slr_est_off_gal.yaml +++ /dev/null @@ -1,170 +0,0 @@ -inputs: - - inputs_root: products/ - - snx_files: [ slr/meta/ecc_une.snx, # SLR station eccentricities - slr/ILRS_Data_Handling_File_2024.02.13.snx, # SLR station biases - slr/meta/ITRF2014-ILRS-TRF-SSC.SNX, # SLR station positions + drifts - tables/igs_satellite_metadata_2203_plus.snx, - tables/sat_yaw_bias_rate.snx ] - erp_files: [ tables/EOP_14_C04_IAU2000A_one_file_1962-now.txt ] - egm_files: [ tables/goco05s.gfc ] # Earth gravity model coefficients file - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] # JPL planetary and lunar ephemerides file - - satellite_data: - sp3_files: [ IGS2R03FIN_20191950000_01D_05M_ORB.SP3, - IGS2R03FIN_20191960000_01D_05M_ORB.SP3, - IGS2R03FIN_20191970000_01D_05M_ORB.SP3, - IGS2R03FIN_20191980000_01D_05M_ORB.SP3, - IGS2R03FIN_20191990000_01D_05M_ORB.SP3, - IGS2R03FIN_20192000000_01D_05M_ORB.SP3, - IGS2R03FIN_20192010000_01D_05M_ORB.SP3 ] - sid_files: [ slr/meta/sp3c-satlist.txt ] - com_files: [ slr/com/com_lageos.txt ] - crd_files: [ slr/obs/galileo/galileo101_201907.npt, - slr/obs/galileo/galileo102_201907.npt, - slr/obs/galileo/galileo103_201907.npt, - slr/obs/galileo/galileo104_201907.npt, - slr/obs/galileo/galileo201_201907.npt, - slr/obs/galileo/galileo202_201907.npt, - slr/obs/galileo/galileo203_201907.npt, - slr/obs/galileo/galileo204_201907.npt, - slr/obs/galileo/galileo205_201907.npt, - slr/obs/galileo/galileo206_201907.npt, - slr/obs/galileo/galileo207_201907.npt, - slr/obs/galileo/galileo208_201907.npt, - slr/obs/galileo/galileo209_201907.npt, - slr/obs/galileo/galileo210_201907.npt, - slr/obs/galileo/galileo211_201907.npt, - slr/obs/galileo/galileo212_201907.npt, - slr/obs/galileo/galileo213_201907.npt, - slr/obs/galileo/galileo214_201907.npt, - slr/obs/galileo/galileo215_201907.npt, - slr/obs/galileo/galileo216_201907.npt, - slr/obs/galileo/galileo217_201907.npt ] - - tides: - ocean_tide_loading_blq_files: [ slr/meta/OLOAD_SLR.BLQ ] # required if ocean loading is applied - ocean_tide_potential_files: [ tables/fes2014b_Cnm-Snm.dat ] - -outputs: - - metadata: - config_description: slr_est_off_gal - - outputs_root: outputs// - colourise_terminal: false - # warn_once: false - - trace: - output_receivers: true - output_network: true - level: 2 - receiver_filename: ---.TRACE - network_filename: ---.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - - log: - output: true - directory: ./ - filename: log_.json - - output_rotation: - period: 1 - period_units: day - - sinex: - output: false - - erp: - output: false - - sp3: - output: false - output_interval: 1 - output_inertial: false - # output_predicted_orbits: true - output_velocities: true - orbit_sources: [PRECISE] - clock_sources: [PRECISE] - - slr_obs: - output: true - directory: ./slr_obs/ - filename: .slr_obs - -mongo: - - enable: primary - primary_database: - output_config: primary - output_measurements: primary - output_components: primary - output_states: primary - output_test_stats: primary - output_trace: primary - delete_history: primary - primary_uri: mongodb://127.0.0.1:27017 - primary_suffix: "" - -receiver_options: - - global: - elevation_mask: 10 # degrees - error_model: elevation_dependent - laser_sigma: 0.10 - - models: - eop: - enable: true - -satellite_options: - - global: - models: - pos: - enable: true - sources: [PRECISE] - attitude: - enable: true - sources: [MODEL, PRECISE, NOMINAL] - -processing_options: - - epoch_control: - start_epoch: 2019-07-14 00:00:00 - end_epoch: 2019-07-20 23:55:00 - epoch_interval: 30 # seconds - require_obs: false - assign_closest_epoch: true - - process_modes: - ppp: true - slr: true # Process SLR observations - preprocessor: true - spp: false - - gnss_general: - require_apriori_positions: true - require_site_eccentricity: true - require_reflector_com: true - - sys_options: - gal: - process: true - - ppp_filter: - inverter: ldlt # LLT LDLT INV - - outlier_screening: - prefit: - max_iterations: 10 # Maximum number of measurements to exclude using prefit checks before attempting to filter - - postfit: - max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter - - model_error_handling: - meas_deweighting: - deweight_factor: 1000 diff --git a/debugConfigs/slr_est_off_lag.yaml b/debugConfigs/slr_est_off_lag.yaml deleted file mode 100644 index 99f6f1c22..000000000 --- a/debugConfigs/slr_est_off_lag.yaml +++ /dev/null @@ -1,167 +0,0 @@ -inputs: - - inputs_root: products/ - - snx_files: [ slr/meta/ecc_une.snx, # SLR station eccentricities - slr/ILRS_Data_Handling_File_2024.02.13.snx, # SLR station biases - slr/meta/ITRF2014-ILRS-TRF-SSC.SNX ] # SLR station positions + drifts - erp_files: [ tables/EOP_14_C04_IAU2000A_one_file_1962-now.txt ] - egm_files: [ tables/goco05s.gfc ] # Earth gravity model coefficients file - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] # JPL planetary and lunar ephemerides file - - satellite_data: - sp3_files: [ slr/orbits/lageos1/ilrsa.orb.lageos1.190720.v71.sp3 ] - sid_files: [ slr/meta/sp3c-satlist.txt ] - com_files: [ slr/com/com_lageos.txt ] - crd_files: [ slr/obs/lageos1/lageos1_201907.npt ] - - tides: - ocean_tide_loading_blq_files: [ slr/meta/OLOAD_SLR.BLQ ] # required if ocean loading is applied - ocean_tide_potential_files: [ tables/fes2014b_Cnm-Snm.dat ] - -outputs: - - metadata: - config_description: slr_est_off_lag - - outputs_root: outputs// - colourise_terminal: false - # warn_once: false - - trace: - output_receivers: true - output_network: true - level: 2 - receiver_filename: ---.TRACE - network_filename: ---.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - - log: - output: true - directory: ./ - filename: log_.json - - output_rotation: - period: 1 - period_units: day - - sinex: - output: false - - erp: - output: false - - sp3: - output: false - output_interval: 1 - output_inertial: false - # output_predicted_orbits: true - output_velocities: true - orbit_sources: [PRECISE] - clock_sources: [PRECISE] - - slr_obs: - output: true - directory: ./slr_obs/ - filename: .slr_obs - -# debug: -# instrument: true -# instrument_once_per_epoch: true - -mongo: - - enable: primary - primary_database: - output_config: primary - output_measurements: primary - output_components: primary - output_states: primary - output_test_stats: primary - output_trace: primary - delete_history: primary - primary_uri: mongodb://127.0.0.1:27017 - primary_suffix: "" - -receiver_options: - - global: - elevation_mask: 10 # degrees - error_model: elevation_dependent - laser_sigma: 0.10 - - models: - eop: - enable: true - -satellite_options: - - global: - models: - pos: - enable: true - sources: [PRECISE] - - orbit_propagation: - mass: 400 - area: 0.28 - srp_cr: 1.75 - planetary_perturbations: [sun, moon, jupiter] - solar_radiation_pressure: cannonball - antenna_thrust: false - albedo: cannonball - empirical: true - empirical_rtn_eclipse: [false, false, false] - -processing_options: - - epoch_control: - start_epoch: 2019-07-14 00:00:18 - end_epoch: 2019-07-20 23:58:18 - epoch_interval: 30 # seconds - require_obs: false - assign_closest_epoch: true - - process_modes: - ppp: true - slr: true # Process SLR observations - preprocessor: true - spp: false - - gnss_general: - require_apriori_positions: true - require_site_eccentricity: true - require_reflector_com: true - - sys_options: - leo: # includes Lageos1 - process: true - - orbit_propagation: - integrator_time_step: 60 - egm_field: true - egm_degree: 60 - central_force: true - general_relativity: true - indirect_J2: true - solid_earth_tide: true - ocean_tide: true - atm_tide: true - pole_tide_ocean: true - pole_tide_solid: true - - ppp_filter: - inverter: ldlt # LLT LDLT INV - - outlier_screening: - prefit: - max_iterations: 10 # Maximum number of measurements to exclude using prefit checks before attempting to filter - - postfit: - max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter - - model_error_handling: - meas_deweighting: - deweight_factor: 1000 diff --git a/debugConfigs/slr_orb_fit.yaml b/debugConfigs/slr_orb_fit.yaml deleted file mode 100644 index 441c4b80e..000000000 --- a/debugConfigs/slr_orb_fit.yaml +++ /dev/null @@ -1,176 +0,0 @@ -inputs: - - inputs_root: products/ - - snx_files: [ slr/meta/ecc_une.snx, # SLR station eccentricities - slr/ILRS_Data_Handling_File_2024.02.13.snx, # SLR station biases - slr/meta/ITRF2014-ILRS-TRF-SSC.SNX ] # SLR station positions + drifts - erp_files: [ tables/EOP_14_C04_IAU2000A_one_file_1962-now.txt ] - egm_files: [ tables/goco05s.gfc ] # Earth gravity model coefficients file - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] # JPL planetary and lunar ephemerides file - - satellite_data: - sp3_files: [ slr/orbits/lageos1/ilrsa.orb.lageos1.190720.v71.sp3 ] - sid_files: [ slr/meta/sp3c-satlist.txt ] - com_files: [ slr/com/com_lageos.txt ] - - tides: - ocean_tide_loading_blq_files: [ slr/meta/OLOAD_SLR.BLQ ] # required if ocean loading is applied - ocean_tide_potential_files: [ tables/fes2014b_Cnm-Snm.dat ] - - pseudo_observations: - eci_pseudoobs: false - sp3_inputs: [ slr/orbits/lageos1/ilrsa.orb.lageos1.190720.v71.sp3 ] - -outputs: - - metadata: - config_description: slr_orb_fit_7d_no_eop - - outputs_root: outputs// - colourise_terminal: false - # warn_once: false - - trace: - output_receivers: true - output_network: true - level: 2 - receiver_filename: --.TRACE - network_filename: --.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - - log: - output: true - directory: ./ - filename: log_.json - - output_rotation: - period: 1 - period_units: day - - erp: - output: false - - orbit_ics: - output: true - directory: ./orbit_ics/ - filename: __orbits.yaml - - sp3: - output: true - output_interval: 1 - output_inertial: false - # output_predicted_orbits: true - output_velocities: true - orbit_sources: [KALMAN] - clock_sources: [PRECISE] - -mongo: - - enable: primary - primary_database: - output_config: primary - output_measurements: primary - output_states: primary - output_test_stats: primary - delete_history: primary - primary_uri: mongodb://127.0.0.1:27017 - primary_suffix: "" - -receiver_options: - - global: - models: - eop: - enable: true - -satellite_options: - - global: - pseudo_sigma: 1 - - orbit_propagation: - mass: 400 - area: 0.28 - srp_cr: 1.75 - planetary_perturbations: [sun, moon, jupiter] - solar_radiation_pressure: cannonball - antenna_thrust: false - albedo: cannonball - empirical: true - empirical_rtn_eclipse: [false, false, false] - - models: - pos: - enable: true - sources: [KALMAN, PRECISE, BROADCAST] - -processing_options: - - epoch_control: - start_epoch: 2019-07-14 00:00:18 - end_epoch: 2019-07-20 23:58:18 - epoch_interval: 60 # seconds - require_obs: true - assign_closest_epoch: true - - process_modes: - ppp: true - - gnss_general: - sys_options: - leo: - process: true # includes Lageos1 - - orbit_propagation: - integrator_time_step: 60 - egm_field: true - egm_degree: 60 - central_force: true - general_relativity: true - indirect_J2: true - solid_earth_tide: true - ocean_tide: true - atm_tide: true - pole_tide_ocean: true - pole_tide_solid: true - -estimation_parameters: - - global_models: - eop: - estimated: [false] - sigma: [10] - - eop_rates: - estimated: [false] - sigma: [10] - - satellites: - - global: - orbit: - estimated: [true] - sigma: [1] # posX/Y/Z, velX/Y/Z (final element repeated as necessary) - - # emp_r_0: { estimated: [false], sigma: [1e3] } - emp_t_0: { estimated: [true], sigma: [1e3] } - # emp_n_0: { estimated: [false], sigma: [1e3] } - - # emp_r_1: { estimated: [false], sigma: [1e3] } - emp_t_1: { estimated: [true], sigma: [1e3] } - emp_n_1: { estimated: [true], sigma: [1e3] } - - # emp_r_2: { estimated: [false], sigma: [1e3] } - # emp_t_2: { estimated: [false], sigma: [1e3] } - # emp_n_2: { estimated: [false], sigma: [1e3] } - - # emp_r_3: { estimated: [false], sigma: [1e3] } - # emp_t_3: { estimated: [false], sigma: [1e3] } - # emp_n_3: { estimated: [false], sigma: [1e3] } - - # emp_r_4: { estimated: [false], sigma: [1e3] } - # emp_t_4: { estimated: [false], sigma: [1e3] } - # emp_n_4: { estimated: [false], sigma: [1e3] } diff --git a/debugConfigs/slr_pod_with_pobs_gal.yaml b/debugConfigs/slr_pod_with_pobs_gal.yaml deleted file mode 100644 index 5a936a7c1..000000000 --- a/debugConfigs/slr_pod_with_pobs_gal.yaml +++ /dev/null @@ -1,248 +0,0 @@ -inputs: - - include_yamls: [ products/boxwing.yaml ] # required if using boxwing model - - inputs_root: products/ - - snx_files: [ slr/meta/ecc_une.snx, # SLR station eccentricities - slr/ILRS_Data_Handling_File_2024.02.13.snx, # SLR station biases - slr/meta/ITRF2014-ILRS-TRF-SSC.SNX, # SLR station positions + drifts - tables/igs_satellite_metadata_2203_plus.snx, - tables/sat_yaw_bias_rate.snx ] - erp_files: [ tables/EOP_14_C04_IAU2000A_one_file_1962-now.txt ] - egm_files: [ tables/goco05s.gfc ] # Earth gravity model coefficients file - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] # JPL planetary and lunar ephemerides file - - satellite_data: - sp3_files: [ IGS2R03FIN_20191950000_01D_05M_ORB.SP3, - IGS2R03FIN_20191960000_01D_05M_ORB.SP3, - IGS2R03FIN_20191970000_01D_05M_ORB.SP3, - IGS2R03FIN_20191980000_01D_05M_ORB.SP3, - IGS2R03FIN_20191990000_01D_05M_ORB.SP3, - IGS2R03FIN_20192000000_01D_05M_ORB.SP3, - IGS2R03FIN_20192010000_01D_05M_ORB.SP3 ] - sid_files: [ slr/meta/sp3c-satlist.txt ] - com_files: [ slr/com/com_lageos.txt ] - # crd_files: [ slr/obs/galileo/galileo101_201907.npt ] - - tides: - ocean_tide_loading_blq_files: [ slr/meta/OLOAD_SLR.BLQ ] # required if ocean loading is applied - atmos_tide_loading_blq_files: [ slr/meta/ALOAD_SLR.BLQ ] - ocean_pole_tide_loading_files: [ tables/opoleloadcoefcmcor.txt ] - ocean_tide_potential_files: [ tables/fes2014b_Cnm-Snm.dat ] - - pseudo_observations: - sp3_inputs: [ IGS2R03FIN_20191950000_01D_05M_ORB.SP3, - IGS2R03FIN_20191960000_01D_05M_ORB.SP3, - IGS2R03FIN_20191970000_01D_05M_ORB.SP3, - IGS2R03FIN_20191980000_01D_05M_ORB.SP3, - IGS2R03FIN_20191990000_01D_05M_ORB.SP3, - IGS2R03FIN_20192000000_01D_05M_ORB.SP3, - IGS2R03FIN_20192010000_01D_05M_ORB.SP3 ] - eci_pseudoobs: false - -outputs: - - metadata: - config_description: slr_orb_fit_gal_3d_est_eop - - outputs_root: outputs// - colourise_terminal: false - - trace: - output_receivers: true - output_network: true - level: 2 - receiver_filename: --.TRACE - network_filename: --.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - - log: - output: true - directory: ./ - filename: log_.json - - output_rotation: - period: 1 - period_units: day - - sinex: - output: false - - erp: - output: false - - orbit_ics: - output: true - directory: ./orbit_ics/ - filename: __orbits.yaml - - sp3: - output: true - output_interval: 1 - output_inertial: false - output_velocities: true - orbit_sources: [KALMAN] - clock_sources: [PRECISE] - - slr_obs: - output: true - directory: ./slr_obs/ - filename: .slr_obs - -mongo: - - enable: primary - primary_database: - output_config: primary - output_measurements: primary - output_states: primary - output_test_stats: primary - delete_history: primary - primary_uri: mongodb://127.0.0.1:27017 - primary_suffix: "" - -satellite_options: - - global: - pseudo_sigma: 1 - - orbit_propagation: - mass: 1000 - area: 15 - srp_cr: 1.75 - power: 20 - planetary_perturbations: [sun, moon, jupiter] - solar_radiation_pressure: boxwing - antenna_thrust: true - albedo: cannonball - empirical: true - empirical_dyb_eclipse: [true, false, false] - pseudo_pulses: - enable: true - - models: - pos: - enable: true - sources: [KALMAN, PRECISE, BROADCAST] - attitude: - enable: true - sources: [MODEL, PRECISE, NOMINAL] - -receiver_options: - - global: - elevation_mask: 10 # degrees - error_model: elevation_dependent - laser_sigma: 0.10 - - models: - eop: - enable: true - -processing_options: - - epoch_control: - start_epoch: 2019-07-17 00:00:00 - end_epoch: 2019-07-19 23:55:00 - epoch_interval: 300 # seconds - require_obs: true - assign_closest_epoch: true - - process_modes: - ppp: true - # slr: true # Process SLR observations - # preprocessor: true - # spp: false - - gnss_general: - # require_apriori_positions: true - # require_site_eccentricity: true - # require_reflector_com: true - - sys_options: - gal: - process: true - - ppp_filter: - inverter: ldlt # LLT LDLT INV - - outlier_screening: - prefit: - max_iterations: 10 # Maximum number of measurements to exclude using prefit checks before attempting to filter - - postfit: - max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter - - # rts: - # enable: true - - orbit_propagation: - integrator_time_step: 900 - central_force: true - egm_field: true - egm_degree: 15 - indirect_J2: true - general_relativity: true - solid_earth_tide: true - ocean_tide: true - atm_tide: true - pole_tide_ocean: true - pole_tide_solid: true - - model_error_handling: - meas_deweighting: - deweight_factor: 1000 - -estimation_parameters: - - global_models: - eop: - estimated: [true] - sigma: [10] - - eop_rates: - estimated: [true] - sigma: [10] - - receivers: - global: - pos: - estimated: [false] - sigma: [1.0] - - slr_range_bias: - estimated: [false] - sigma: [0.01] - - slr_time_bias: - estimated: [false] - sigma: [0.00001] - - satellites: - global: - orbit: - estimated: [true] - sigma: [1] # posX/Y/Z, velX/Y/Z (final element repeated as necessary) - - emp_d_0: { estimated: [true], sigma: [1e3] } - emp_y_0: { estimated: [true], sigma: [1e3] } - emp_b_0: { estimated: [true], sigma: [1e3] } - - emp_d_1: { estimated: [true], sigma: [1e3] } - # emp_y_1: { estimated: [false], sigma: [1e3] } - emp_b_1: { estimated: [true], sigma: [1e3] } - - emp_d_2: { estimated: [true], sigma: [1e3] } - # emp_y_2: { estimated: [false], sigma: [1e3] } - # emp_b_2: { estimated: [false], sigma: [1e3] } - - # emp_d_3: { estimated: [false], sigma: [1e3] } - # emp_y_3: { estimated: [false], sigma: [1e3] } - # emp_b_3: { estimated: [false], sigma: [1e3] } - - emp_d_4: { estimated: [true], sigma: [1e3] } - # emp_y_4: { estimated: [false], sigma: [1e3] } - # emp_b_4: { estimated: [false], sigma: [1e3] } diff --git a/debugConfigs/slr_pod_with_pobs_lag.yaml b/debugConfigs/slr_pod_with_pobs_lag.yaml deleted file mode 100644 index d9647e3f6..000000000 --- a/debugConfigs/slr_pod_with_pobs_lag.yaml +++ /dev/null @@ -1,226 +0,0 @@ -inputs: - - inputs_root: products/ - - snx_files: [ slr/meta/ecc_une.snx, # SLR station eccentricities - slr/ILRS_Data_Handling_File_2024.02.13.snx, # SLR station biases - slr/meta/ITRF2014-ILRS-TRF-SSC.SNX ] # SLR station positions + drifts - erp_files: [ tables/EOP_14_C04_IAU2000A_one_file_1962-now.txt ] - egm_files: [ tables/goco05s.gfc ] # Earth gravity model coefficients file - planetary_ephemeris_files: [ tables/DE436.1950.2050 ] # JPL planetary and lunar ephemerides file - - satellite_data: - sp3_files: [ slr/orbits/lageos1/ilrsa.orb.lageos1.190720.v71.sp3 ] - sid_files: [ slr/meta/sp3c-satlist.txt ] - com_files: [ slr/com/com_lageos.txt ] - crd_files: [ slr/obs/lageos1/lageos1_201907.npt ] - - tides: - ocean_tide_loading_blq_files: [ slr/meta/OLOAD_SLR.BLQ ] # required if ocean loading is applied - atmos_tide_loading_blq_files: [ slr/meta/ALOAD_SLR.BLQ ] - ocean_pole_tide_loading_files: [ tables/opoleloadcoefcmcor.txt ] - ocean_tide_potential_files: [ tables/fes2014b_Cnm-Snm.dat ] - - pseudo_observations: - sp3_inputs: [ slr/orbits/lageos1/ilrsa.orb.lageos1.190720.v71.sp3 ] - eci_pseudoobs: false - -outputs: - - metadata: - config_description: slr_pod_with_pobs_lag_no_eop_60s - - outputs_root: outputs// - colourise_terminal: false - - trace: - output_receivers: true - output_network: true - level: 2 - receiver_filename: --.TRACE - network_filename: --.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - - log: - output: true - directory: ./ - filename: log_.json - - output_rotation: - period: 1 - period_units: day - - sinex: - output: false - - erp: - output: false - - orbit_ics: - output: true - directory: ./orbit_ics/ - filename: __orbits.yaml - - sp3: - output: true - output_interval: 1 - output_inertial: false - output_velocities: true - orbit_sources: [KALMAN] - clock_sources: [PRECISE] - - slr_obs: - output: true - directory: ./slr_obs/ - filename: .slr_obs - -mongo: - - enable: primary - primary_database: - output_config: primary - output_measurements: primary - output_states: primary - output_test_stats: primary - delete_history: primary - primary_uri: mongodb://127.0.0.1:27017 - primary_suffix: "" - -satellite_options: - - global: - pseudo_sigma: 1 - - orbit_propagation: - mass: 400 - area: 0.28 - srp_cr: 1.75 - planetary_perturbations: [sun, moon, jupiter] - solar_radiation_pressure: cannonball - antenna_thrust: false - albedo: cannonball - empirical: true - empirical_rtn_eclipse: [false, false, false] - - models: - pos: - enable: true - sources: [KALMAN, PRECISE, BROADCAST] - -receiver_options: - - global: - elevation_mask: 10 # degrees - error_model: elevation_dependent - laser_sigma: 0.10 - - models: - eop: - enable: true - -processing_options: - - epoch_control: - start_epoch: 2019-07-14 00:00:18 - end_epoch: 2019-07-20 23:58:18 - epoch_interval: 60 # seconds - require_obs: true - assign_closest_epoch: true - - process_modes: - ppp: true - slr: true # Process SLR observations - preprocessor: true - spp: false - - gnss_general: - require_apriori_positions: true - require_site_eccentricity: true - require_reflector_com: true - - sys_options: - leo: - process: true # includes Lageos1 - - ppp_filter: - inverter: ldlt # LLT LDLT INV - - outlier_screening: - prefit: - max_iterations: 10 # Maximum number of measurements to exclude using prefit checks before attempting to filter - - postfit: - max_iterations: 10 # Maximum number of measurements to exclude using postfit checks while iterating filter - - # rts: - # enable: true - - orbit_propagation: - integrator_time_step: 60 - central_force: true - egm_field: true - egm_degree: 60 - indirect_J2: true - general_relativity: true - solid_earth_tide: true - ocean_tide: true - atm_tide: true - pole_tide_ocean: true - pole_tide_solid: true - - model_error_handling: - meas_deweighting: - deweight_factor: 1000 - -estimation_parameters: - - global_models: - eop: - estimated: [false] - sigma: [10] - - eop_rates: - estimated: [false] - sigma: [10] - - receivers: - global: - pos: - estimated: [false] - sigma: [1.0] - - slr_range_bias: - estimated: [false] - sigma: [0.01] - - slr_time_bias: - estimated: [false] - sigma: [0.00001] - - satellites: - global: - orbit: - estimated: [true] - sigma: [1] # posX/Y/Z, velX/Y/Z (final element repeated as necessary) - - # emp_r_0: { estimated: [false], sigma: [1e3] } - emp_t_0: { estimated: [true], sigma: [1e3] } - # emp_n_0: { estimated: [false], sigma: [1e3] } - - # emp_r_1: { estimated: [false], sigma: [1e3] } - emp_t_1: { estimated: [true], sigma: [1e3] } - emp_n_1: { estimated: [true], sigma: [1e3] } - - # emp_r_2: { estimated: [false], sigma: [1e3] } - # emp_t_2: { estimated: [false], sigma: [1e3] } - # emp_n_2: { estimated: [false], sigma: [1e3] } - - # emp_r_3: { estimated: [false], sigma: [1e3] } - # emp_t_3: { estimated: [false], sigma: [1e3] } - # emp_n_3: { estimated: [false], sigma: [1e3] } - - # emp_r_4: { estimated: [false], sigma: [1e3] } - # emp_t_4: { estimated: [false], sigma: [1e3] } - # emp_n_4: { estimated: [false], sigma: [1e3] } diff --git a/debugConfigs/sp3_ecef2eci.yaml b/debugConfigs/sp3_ecef2eci.yaml deleted file mode 100644 index 462f32be5..000000000 --- a/debugConfigs/sp3_ecef2eci.yaml +++ /dev/null @@ -1,51 +0,0 @@ -inputs: - - inputs_root: products/ - - erp_files: [ "podTest/2019195_07D/COD0MGXFIN*.ERP" ] - - satellite_data: - satellite_data_root: podTest/2019195_07D/ - sp3_files: [ "COD0MGXFIN*.SP3" ] - - -outputs: - - outputs_root: products/podTest/2019195_07D/SP3i/ - - metadata: - config_description: COD0MGXFIN - time_system: G # (string) Time system - e.g. "G", "UTC" - - sp3: - output: true - filename: __01D_05M_ORB.SP3 - orbit_sources: [ PRECISE ] - clock_sources: [ PRECISE ] - output_inertial: true - output_interval: 300 - - -receiver_options: # Options to configure individual stations or global configs - - global: - models: - eop: - enable: true - - -processing_options: - - epoch_control: - start_epoch: 2019-07-14 00:00:00 - end_epoch: 2019-07-20 23:55:00 - epoch_interval: 300 # seconds - require_obs: false - - gnss_general: - sys_options: - gps: { process: true } - gal: { process: true } - glo: { process: true } - bds: { process: true } - # qzs: { process: true } diff --git a/debugConfigs/spire_pod.yaml b/debugConfigs/spire_pod.yaml deleted file mode 100644 index 820f5c00f..000000000 --- a/debugConfigs/spire_pod.yaml +++ /dev/null @@ -1,279 +0,0 @@ -inputs: - - root_directory: products/ - - atx_files: [ igs20.atx ] # Antenna models for receivers and satellites in ANTEX format - snx_files: - - ../otherProducts/LEO_Cube.snx - - tables/igs_satellite_metadata_2203_plus.snx - otl_blq_files: [ OLOAD_GO.BLQ ] # ocean loading is applied - atl_blq_files: [ ALOAD_GO.BLQ ] - opole_files: [ tables/opoleloadcoefcmcor.txt ] - #erp_files: [ ../otherProducts/COD0OPSFIN_20230010000_01D_01D_ERP.ERP] - erp_files: [ tables/finals.data.iau2000.txt] - - egm_files: [ tables/EGM2008.gfc ] # Earth gravity model coefficients file - jpl_files: [ tables/DE436.1950.2050 ] # JPL planetary and lunar ephemerides file - tide_files: [ tables/fes2014b_Cnm-Snm.dat] - - satellite_data: - inputs_root: ../otherProducts/ - nav_files: [ ../otherProducts/brdc0010.23n ] # broadcast navigation file - sp3_files: [ ../otherProducts/COD0OPSFIN_20230010000_01D_05M_ORB.SP3 ] # satellite orbit files in SP3 format - clk_files: [ ../otherProducts/COD0OPSFIN_20230010000_01D_05S_CLK.CLK] # satellite clock files in RNX CLK format - #bsx_files: [ ../spire/products/IAR230020_V01.BIA ] # daily signal biases files - bsx_files: [ ../otherProducts/IAR230010_V01.BIA] # daily signal biases files - obx_files: - - "../otherProducts/conjugated/leoAtt_2023.001.099.00.OBX" - - "../otherProducts/conjugated/leoAtt_2023.001.099.01.OBX" - - "../otherProducts/conjugated/leoAtt_2023.001.099.02.OBX" - - "../otherProducts/conjugated/leoAtt_2023.001.099.03.OBX" - - "../otherProducts/conjugated/leoAtt_2023.001.099.06.OBX" - #- "../spire/leoAtt_nrt_2023_001_OBX_conj/leoAtt_2023.001.099.07.OBX" - #- "../spire/leoAtt_nrt_2023_001_OBX_conj/leoAtt_2023.001.099.21.OBX" - #- "../spire/leoAtt_nrt_2023_001_OBX_conj/leoAtt_2023.001.099.22.OBX" - - gnss_observations: - inputs_root: ../otherData/ - rnx_inputs: - - L99: - #- "podCrx_2023.001.099.00.00.rnx" - - "podCrx_2023.001.099.01.00.rnx" - #- "podCrx_2023.001.099.02.00.rnx" - #- "podCrx_2023.001.099.03.00.rnx" - #- "podCrx_2023.001.099.06.00.rnx" - #- "podCrx_2023.001.099.07.00.rnx" - #- "podCrx_2023.001.099.21.00.rnx" - #- "podCrx_2023.001.099.22.00.rnx" - -outputs: - - root_directory: outputs// - - trace: - output_stations: true - output_network: true - level: 5 - directory: ./ - station_filename: _.TRACE - network_filename: _.SUM - output_residuals: true - output_config: true - - metadata: - config_description: spirePOD - analysis_agency: GAA - analysis_center: Geoscience Australia - analysis_program: AUSACS - rinex_comment: AUSNETWORK1 - -mongo: # Mongo is a database used to store results and intermediate values for later analysis and inter-process communication - enable: true # (bool) Enable and connect to mongo database - database: "" # (string) - delete_history: true # (bool) Drop the collection in the database at the beginning of the run to only show fresh data - output_components: true # (bool) Output components of measurements - output_measurements: true # (bool) Output measurements and their residuals - output_states: true # (bool) Output states - output_logs: false # (bool) Output console trace and warnings to mongo with timestamps and other metadata - output_test_stats: false # (bool) Output test statistics - output_trace: false # (bool) Output trace - suffix: "" # (string) Suffix to append to database elements to make distinctions between runs for comparison - uri: "mongodb://localhost:27017" # (string) Location and port of the mongo database to connect to - -#debug: - #unit_tests: - #stop_on_done: true - #output_pass: true - -satellite_options: - - global: - antenna_boresight: [ 0, 0, +1] - antenna_azimuth: [ 0, +1, 0] - -station_options: - - global: - rnx_code_conversions: - gps: - C1: L1C - C2: L2L - rnx_phase_conversions: - gps: - L1: L1C - L2: L2L - - antenna_boresight: [0, 0, -1] - antenna_azimuth: [+1, 0, 0] - sat_id: "L99" - - L99: - receiver_type: "LEMUR" # (string) - antenna_type: "LEMUR (POD) NONE" # (string) - #eccentricity: [0.0024, -0.0047, -0.1631] # [floats] - eccentricity: [ -0.0047, 0.0024, -0.1631] # [floats] - #apriori_position: [] # [floats] - -processing_options: - - epoch_control: - start_epoch: 2023-01-01 00:18:42 - end_epoch: 2023-01-01 23:59:59 - epoch_interval: 1 #seconds - wait_next_epoch: 3600 - - - process_modes: - preprocessor: true - ppp: true - - gnss_general: - elevation_mask: 1 - error_model: elevation_dependent - raim: true - max_gdop: 100 - rec_reference_system: gps - - sys_options: - gps: - process: true - ambiguity_resolution: false - reject_eclipse: true - #zero_receiver_dcb: true - code_priorities: [ L1C, L2L ] - - orbit_propagation: - central_force: true - planetary_perturbation: true - indirect_J2: true - egm_field: true - solid_earth_tide: true - ocean_tide: true - general_relativity: true - pole_tide_ocean: true - pole_tide_solid: true - solar_radiation_pressure: true - integrator_time_step: 0.5 - sat_mass: 6.5 - sat_area: 0.5 - srp_cr: 1.25 - sat_power: 20 # (float) Transmission power use if not specified in the SINEX metadata file - degree_max: 30 - - #itrf_pseudoobs: true - - model_error_checking: - ambiguities: - reinit_on_all_slips: true - - cycle_slips: # Cycle slips may be detected by the preprocessor and measurements rejected or ambiguities reinitialised - exclude_on: - gf: true # (bool) Exclude measurements that fail GF slip test in preprocessor - lli: true # (bool) Exclude measurements that fail LLI slip test in preprocessor - mw: true # (bool) Exclude measurements that fail MW slip test in preprocessor - scdia: true # (bool) Exclude measurements that fail SCDIA test in preprocessor - - reset_on: - gf: true # (bool) Reset ambiguities if GF test is detecting a slip - lli: true # (bool) Reset ambiguities if LLI test is detecting a slip - mw: true # (bool) Reset ambiguities if MW test is detecting a slip - scdia: true # (bool) Reset ambiguities if SCDIA test is detecting a slip - - slip_threshold: 0.05 # (float) Value used to determine when a slip has occurred - mw_proc_noise: 0 # (float) Process noise applied to filtered Melbourne-Wubenna measurements to detect cycle slips - - orbit_errors: # Orbital states that are not consistent with measurements may be reinitialised to allow for dynamic maneuvers - enable: false - orbit_pos_proc_noise: 0 # (float) Sigma to apply to orbital position states as reinitialisation - orbit_vel_proc_noise: 10 # (float) Sigma to apply to orbital velocity states as reinitialisation - orbit_vel_proc_noise_trail: 1 # (float) Initial sigma for exponentially decaying noise to apply for subsequent epochs as soft reinitialisation - orbit_vel_proc_noise_trail_tau: 0.1 # (float) Time constant for exponentially decauing noise - #orbit_errors: # Orbital states that are not consistent with measurements may be reinitialised to allow for dynamic maneuvers - # orbit_pos_proc_noise: 10 # (float) Sigma to apply to orbital position states as reinitialisation - # orbit_vel_proc_noise: 5 # (float) Sigma to apply to orbital velocity states as reinitialisation - # orbit_vel_proc_noise_trail: 1 # (float) Initial sigma for exponentially decaying noise to apply for subsequent epochs as soft reinitialisation - # orbit_vel_proc_noise_trail_tau: 0.05 # (float) Time constant for exponentially decauing noise - - gnss_models: - troposphere: - enable: false - tides: - enable: false - - ionospheric_component: - enable: true - #corr_mode: iono_free_linear_combo # estimate, iono_free_linear_combo - iono_sigma_limit: 10000000 - common_ionosphere: true # Code and Phase measurment share the same ionosphere - use_if_combo: true - - #ambiguity_resolution: - - #elevation_mask: 15 - - #wide_lane: - #mode: iter_rnd # AR mode for WL: off, round, iter_rnd, bootst, lambda, lambda_alt, lambda_al2, lambda_bie - #success_rate_threshold: 0.999 - #solution_ratio_threshold: 3 - #process_noise_sat: 0.00001 - #process_noise_rec: 0.0001 - - #narrow_lane: - #mode: lambda_bie # AR mode for WL: off, round, iter_rnd, bootst, lambda, lambda_alt, lambda_al2, lambda_bie - #success_rate_threshold: 0 - #solution_ratio_threshold: 30 - - #lambda_set_size: 200 - #max_hold_epochs: 0 - #max_rounding_iterations: 5 - - filter_options: - outlier_screening: - max_filter_iterations: 10 - chi_square_mode: NONE # (enum) Chi-square test mode - innovation, measurement, state {none,innovation,measurement,state} - chi_square_test: false # (bool) Enable Chi-square test - max_prefit_removals: 0 # (int) Maximum number of measurements to exclude using prefit checks before attempting to filter - #sigma_check: false # (bool) Enable prefit and postfit sigma check - state_sigma_threshold: 5 # (float) sigma threshold for states - meas_sigma_threshold: 5 # (float) sigma threshold for measurements - omega_test: false # (bool) Enable w-test - - - inverter: LDLT #LLT LDLT INV - -estimation_parameters: - - stations: - #error_model: uniform #uniform elevation_dependent - code_sigmas: [0.5] - phase_sigmas: [0.005] - spp_sigma_scaling: 4 - - orbit: - estimated: [true] - sigma: [50, 50, 50, 5000, 5000, 5000] - proc_noise: [0, 0, 0, 0.01] - - #pos: - #estimated: [true] - #sigma: [100] - - #pos_rate: - #estimated: [true] - #sigma: [5000] - #proc_noise: [10] - - clk: - estimated: [true] - sigma: [1000] - proc_noise: [10] - - amb: - estimated: [true] - sigma: [1000] - proc_noise: [0] - - ion_stec: - estimated: [true] - sigma: [400] - proc_noise: [30] diff --git a/debugConfigs/tide_debug.yaml b/debugConfigs/tide_debug.yaml deleted file mode 100644 index 718224a8e..000000000 --- a/debugConfigs/tide_debug.yaml +++ /dev/null @@ -1,228 +0,0 @@ -# Post-processing PPP example GPS+GAL - -inputs: - root_directory: products/ - snx_files: [ tables/igs_satellite_metadata_2203_plus.snx, - IGS1R03SNX_20191950000_07D_07D_CRD.SNX ] - atx_files: [ M20.ATX ] - otl_blq_files: [ OLOAD_GO.test.BLQ ] - atl_blq_files: [ ALOAD_GO.test.BLQ ] - opole_files: [ tables/opoleloadcoefcmcor.txt ] - egm_files: [ tables/EGM2008.gfc ] - jpl_files: [ tables/DE436.1950.2050 ] - tide_files: [ tables/fes2014b_Cnm-Snm.dat ] - igrf_files: [ tables/igrf13coeffs.txt ] - erp_files: [ tables/finals.data.iau2000.txt ] - - satellite_data: - # nav_files: [ brdm1990.19p ] - clk_files: [ IGS2R03FIN_20191990000_01D_30S_CLK.CLK ] - bsx_files: [ IGS2R03FIN_20191990000_01D_01D_OSB.BIA ] - sp3_files: [ IGS2R03FIN_20191990000_01D_05M_ORB.SP3 ] - - troposphere: - gpt2grid_files: gpt_25.grd - - gnss_observations: - inputs_root: ../data/ - rnx_inputs: - - ALIC00AUS_R_20191990000_01D_30S_MO.rnx - - REYK00ISL_R_20191990000_01D_30S_MO.rnx - # - COCO00AUS_R_20191990000_01D_30S_MO.rnx - # - LHAZ00CHN_R_20191990000_01D_30S_MO.rnx - # - KZN200RUS_S_20191990000_01D_30S_MO.rnx - -outputs: - metadata: - config_description: "tideDebug" - - root_directory: outputs// - - trace: - level: 3 - output_stations: true - output_network: true - station_filename: _.TRACE - network_filename: _.TRACE - output_residuals: true - output_residual_chain: true - output_config: true - - ppp_sol: - output: true - filename: _.POS - - gpx: - output: true - filename: _.GPX - -mongo: - enable: true - uri: mongodb://127.0.0.1:27017 - database: - output_rtcm_messages: true - output_components: true - output_states: true - output_measurements: true - output_test_stats: true - delete_history: true - suffix: "" - -satellite_options: - global: - phase_bias: - enable: true - - E05: - exclude: true - E06: - exclude: true - E10: - exclude: true - E16: - exclude: true - E17: - exclude: true - E23: - exclude: true - E28: - exclude: true - E29: - exclude: true - E32: - exclude: true - E34: - exclude: true - E35: - exclude: true - -station_options: - global: - phase_bias: - enable: true - - ALIC: - receiver_type: "LEICA GR25" - antenna_type: "LEIAR25.R3 NONE" - eccentricity: [ 0.0000, 0.0000, 0.0015 ] - # apriori_position: [ -4052051.7670, 4212836.2150, -2545106.0270 ] - REYK: - receiver_type: "LEICA GR50" - antenna_type: "LEIAR25.R4 LEIT" - eccentricity: [ 0.0000, 0.0000, 0.0635 ] - # apriori_position: [ -4052051.7670, 4212836.2150, -2545106.0270 ] - # COCO: - # receiver_type: "SEPT POLARXS" - # antenna_type: "AOAD/M_T NONE" - # eccentricity: [ 0.0000, 0.0000, 0.0040 ] - # # apriori_position: [ -741949.8528, 6190961.6634, -1337768.7328 ] - # LHAZ: - # receiver_type: "LEICA GR25" - # antenna_type: "LEIAR25.R4 LEIT" - # eccentricity: [ 0.0000, 0.0000, 0.1330 ] - # # apriori_position: [ -106943.5000, 5549296.1400, 3139212.6000 ] - # KZN2: - # receiver_type: "TRIMBLE NETR9" - # antenna_type: "TRM59800.00 SCIS" - # eccentricity: [ 0.0000, 0.0000, 0.0750 ] - # # apriori_position: [ 2352345.7000, 2717466.1000, 5251458.5000 ] - -processing_options: - epoch_control: - max_epochs: 2880 - epoch_interval: 30 - - process_modes: - ppp: true - - gnss_general: - error_model: ELEVATION_DEPENDENT - code_measurements: - sigmas: [ 0.3 ] - phase_measurements: - sigmas: [ 0.003 ] - elevation_mask: 10 - rec_reference_system: GPS - sys_options: - gps: - process: true - reject_eclipse: false - zero_receiver_dcb: true - code_priorities: [ L1C, L2W ] - ambiguity_resolution: true - gal: - process: true - reject_eclipse: false - zero_receiver_dcb: true - code_priorities: [ L1C, L5Q, L1X, L5X ] - ambiguity_resolution: true - - gnss_models: - eop: - enable: true - troposphere: - model: GPT2 - ionospheric_component2: - enable: true - ionospheric_component3: - enable: true - - filter_options: - outlier_screening: - max_filter_iterations: 5 - max_prefit_removals: 3 - station_chunking: - enable: true - rts: - enable: true - - model_error_checking: - deweighting: - deweight_factor: 1000 - ambiguities: - outage_reset_limit: 5 - phase_reject_limit: 2 - reinit_on_all_slips: true - - ambiguity_resolution: - elevation_mask: 15 - lambda_set_size: 200 - mode: LAMBDA_BIE - success_rate_threshold: 0.99 - solution_ratio_threshold: 30 - -estimation_parameters: - stations: - global: - pos: - estimated: [ true ] - sigma: [ 100 ] - proc_noise: [ 100 ] - clk: - estimated: [ true ] - sigma: [ 1000 ] - proc_noise: [ 100 ] - amb: - estimated: [ true ] - sigma: [ 1000 ] - proc_noise: [ 0 ] - trop: - estimated: [ true ] - sigma: [ 0.3 ] - proc_noise: [ 0.0001 ] - trop_grads: - estimated: [ true ] - sigma: [ 0.03 ] - proc_noise: [ 1.0E-6 ] - ion_stec: - estimated: [ true ] - sigma: [ 200 ] - proc_noise: [ 10 ] - code_bias: - estimated: [ true ] - sigma: [ 20 ] - proc_noise: [ 0 ] - phase_bias: - estimated: [ true ] - sigma: [ 10 ] - proc_noise: [ 0 ] diff --git a/docker/Dockerfile b/docker/Dockerfile index 25261573f..d26e7865e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -45,6 +45,7 @@ RUN \ mkdir -p src/build \ && cd src/build \ && cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_DOC=TRUE .. \ + && make -j$BUILD_THREADS unit_tests \ && make -j$BUILD_THREADS $TARGET \ && cd - \ && rm -rf src/build @@ -117,4 +118,4 @@ COPY --from=gnssanalysis/ginan-env:latest /opt/mongocxx/lib/ /usr/local/lib/ # Render yaml of PEA's default parameter values, for reference. Note: -Y 3 is used in pipeline. # This doubles as a check that pea loads successfully. -RUN pea -Y 3 > /ginan/pea-defaults.yaml \ No newline at end of file +RUN pea -Y 3 > /ginan/pea-defaults.yaml diff --git a/exampleConfigs/loading/blq.yaml b/exampleConfigs/loading/blq.yaml new file mode 100644 index 000000000..b1cd00eaf --- /dev/null +++ b/exampleConfigs/loading/blq.yaml @@ -0,0 +1,13 @@ +greenfunction: inputData/loading/greens/F_A3.txt +tide: + - inputData/loading/fes2014b/m2.nc + - inputData/loading/fes2014b/s2.nc + - inputData/loading/fes2014b/n2.nc + - inputData/loading/fes2014b/k2.nc + - inputData/loading/fes2014b/k1.nc + - inputData/loading/fes2014b/o1.nc + - inputData/loading/fes2014b/p1.nc + - inputData/loading/fes2014b/q1.nc + - inputData/loading/fes2014b/mf.nc + - inputData/loading/fes2014b/mm.nc + - inputData/loading/fes2014b/ssa.nc diff --git a/exampleConfigs/ppp_example.yaml b/exampleConfigs/ppp_example.yaml index f800a0d7b..81b1f03eb 100644 --- a/exampleConfigs/ppp_example.yaml +++ b/exampleConfigs/ppp_example.yaml @@ -42,8 +42,8 @@ inputs: # - "*.rnx" - ALIC00AUS_R_20191990000_01D_30S_MO.rnx # - ALIC2.rnx - # - DARW00AUS_R_20191990000_01D_30S_MO.rnx - # - HOB200AUS_R_20191990000_01D_30S_MO.rnx + - DARW00AUS_R_20191990000_01D_30S_MO.rnx + - HOB200AUS_R_20191990000_01D_30S_MO.rnx # - "M*.rnx" outputs: @@ -59,7 +59,7 @@ outputs: output: true filename: .POS trace: - level: 2 + level: 4 output_receivers: true output_network: true receiver_filename: __.TRACE diff --git a/exampleConfigs/record_streams.yaml b/exampleConfigs/record_streams.yaml index 7a66c3035..4d61da884 100644 --- a/exampleConfigs/record_streams.yaml +++ b/exampleConfigs/record_streams.yaml @@ -88,6 +88,7 @@ satellite_options: # Required if write out SP3 files with SSRA streams receiver_options: # Receiver and antenna information to write to Rinex file headers (can use valid Sinex files instead) ALIC: + meta_priority: [CONFIG, RTCM, SINEX, RINEX] # Highest priority first within META receiver_type: "SEPT POLARX5" antenna_type: "TWIVC6050 NONE" apriori_position: [-4052052.7352, 4212835.9833, -2545104.5853] diff --git a/exampleConfigs/rt_ppp_example.yaml b/exampleConfigs/rt_ppp_example.yaml index 29b81cab3..9f5286aa6 100644 --- a/exampleConfigs/rt_ppp_example.yaml +++ b/exampleConfigs/rt_ppp_example.yaml @@ -84,12 +84,15 @@ receiver_options: global: elevation_mask: 15 # (degrees) error_model: ELEVATION_DEPENDENT # {uniform,elevation_dependent} + meta_priority: [RTCM, SINEX] # Highest priority first within META code_sigma: 0.3 # Standard deviation of code measurements (m) phase_sigma: 0.003 # Standard deviation of phase measurmeents (m) clock_codes: [AUTO, AUTO] zero_dcb_codes: [AUTO, AUTO] rec_reference_system: GPS models: + pos: + sources: [KALMAN, META, SPP, REMOTE] phase_bias: enable: false troposphere: # Tropospheric modelling accounts for delays due to refraction of light in water vapour @@ -105,15 +108,17 @@ receiver_options: ionospheric_components: use_2nd_order: true use_3rd_order: true - - ALIC: - receiver_type: "SEPT POLARX5" - antenna_type: "TWIVC6050 NONE" - apriori_position: [-4052052.7352, 4212835.9833, -2545104.5853] - models: eccentricity: enable: true - offset: [0.0000, 0.0000, 0.0250] + + # ALIC: + # receiver_type: "SEPT POLARX5" + # antenna_type: "TWIVC6050 NONE" + # apriori_position: [-4052052.7352, 4212835.9833, -2545104.5853] + # models: + # eccentricity: + # enable: true + # offset: [0.0000, 0.0000, 0.0250] MAW1: receiver_type: "SEPT POLARX5" @@ -317,7 +322,7 @@ estimation_parameters: process_noise: [0.001] mongo: # Mongo is a database used to store results and intermediate values for later analysis and inter-process communication - enable: primary # Enable and connect to mongo database {none,primary,secondary,both} + enable: none # Enable and connect to mongo database {none,primary,secondary,both} primary_uri: mongodb://localhost:27017 # Location and port of the mongo database to connect to primary_database: primary_suffix: "" # Suffix to append to database elements to make distinctions between runs for comparison diff --git a/exampleConfigs/slr_pod_with_pseudoobs_gal.yaml b/exampleConfigs/slr_pod_with_pseudoobs_gal.yaml index 5ec3a79ea..8c94ba267 100644 --- a/exampleConfigs/slr_pod_with_pseudoobs_gal.yaml +++ b/exampleConfigs/slr_pod_with_pseudoobs_gal.yaml @@ -169,8 +169,6 @@ processing_options: process: true ppp_filter: - inverter: ldlt # LLT LDLT INV - outlier_screening: prefit: max_iterations: 10 # Maximum number of measurements to exclude using prefit checks before attempting to filter diff --git a/exampleConfigs/slr_pod_with_pseudoobs_lag.yaml b/exampleConfigs/slr_pod_with_pseudoobs_lag.yaml index 72dd502d6..e921794d1 100644 --- a/exampleConfigs/slr_pod_with_pseudoobs_lag.yaml +++ b/exampleConfigs/slr_pod_with_pseudoobs_lag.yaml @@ -141,8 +141,6 @@ processing_options: process: true # includes Lageos1 ppp_filter: - inverter: ldlt # LLT LDLT INV - outlier_screening: prefit: max_iterations: 10 # Maximum number of measurements to exclude using prefit checks before attempting to filter diff --git a/scripts/GinanUI/README.md b/scripts/GinanUI/README.md index 83897d1dd..bea643d8b 100644 --- a/scripts/GinanUI/README.md +++ b/scripts/GinanUI/README.md @@ -4,6 +4,8 @@ An intelligent and user-friendly interface for using the Geoscience Australia GN [User manual available here](./docs/USER_MANUAL.md) +[Application architecture document available here](./docs/APPLICATION_ARCHITECTURE.md) + ## Installation Please read the user manual above for installation instructions. \ No newline at end of file diff --git a/scripts/GinanUI/app/controllers/__init__.py b/scripts/GinanUI/app/controllers/__init__.py index e69de29bb..f24dea6df 100644 --- a/scripts/GinanUI/app/controllers/__init__.py +++ b/scripts/GinanUI/app/controllers/__init__.py @@ -0,0 +1,29 @@ +# app/controllers/__init__.py +""" +Controller layer for the Ginan-UI application. + +Controllers coordinate between the UI (views) and the backend (models / utils). +Each controller is responsible for a specific area of the UI: + + InputController - Parent controller: top-level buttons (Observations, + Output, Show Config, Process, Stop All, CDDIS + Credentials, User Manual, Reset Config), shared state, + and ExtractedInputs dataclass. + + GeneralConfigController - General config tab: mode, constellations multi-select, + PPP provider / project / series, receiver / antenna types, + time window, data interval, antenna offset, apriori + position. Also owns background workflows for CDDIS + archive scanning and SINEX file validation. + + ConstellationConfigController - Constellations config tab: observation code list widgets + with drag-drop reordering and checkboxes, BIA code + priority fetching and validation styling, + placeholder / status labels. + + OutputConfigController - Output config tab: POS, GPX, TRACE, SNX file output + checkboxes. + + VisualisationController - Visualisation panel: embedded HTML plot display, + external browser opening, plot selector combo box. +""" diff --git a/scripts/GinanUI/app/controllers/constellation_config_controller.py b/scripts/GinanUI/app/controllers/constellation_config_controller.py new file mode 100644 index 000000000..0e6f72dd0 --- /dev/null +++ b/scripts/GinanUI/app/controllers/constellation_config_controller.py @@ -0,0 +1,688 @@ +""" +Controller for the Constellations configuration tab. + +Manages the following UI widgets and background workflows: + - Per-constellation observation code QListWidgets (GPS, GAL, GLO, BDS, QZS) + - Per-constellation labels + - Placeholder / explanation / BIA warning / BIA loading status labels + - BIA code priority fetching (background worker) and code validation styling +""" + +from __future__ import annotations +from typing import List +from PySide6.QtCore import QObject, Qt, QThread +from PySide6.QtGui import QColor, QBrush +from PySide6.QtWidgets import ( + QLabel, + QListWidget, + QListWidgetItem, + QAbstractItemView, + QSizePolicy, + QWidget, + QVBoxLayout, +) +from scripts.GinanUI.app.utils.logger import Logger +from scripts.GinanUI.app.utils.workers import BiasProductWorker +from scripts.GinanUI.app.utils.toast import show_toast + + +class ConstellationConfigController(QObject): + """ + Manages the Constellations configuration tab: observation code list widgets, + BIA code priority validation, and placeholder/status labels. + + Arguments: + ui: The main window UI instance. + input_ctrl: The parent InputController instance (for accessing shared state). + """ + + def __init__(self, ui, input_ctrl): + """ + Initialise constellation tab bindings and state. + + Arguments: + ui: The main window UI instance. + input_ctrl: The parent InputController that owns shared state. + """ + super().__init__(parent=input_ctrl) + self.ui = ui + self.ctrl = input_ctrl # parent InputController + + # BIA worker tracking + self._bia_loading = False + self._bia_worker = None + self._bia_thread = None + self._bia_current_provider = None + self._bia_current_series = None + self._bia_current_project = None + + # Setup placeholder and status labels + self._setup_placeholder() + self._hide_all_widgets() + + # Connect tab change signal to trigger BIA fetch when switching to Constellations tab + self.ui.configTabWidget.currentChanged.connect(self.on_config_tab_changed) + + #region UI Tooltips + + def setup_tooltips(self): + """ + Set up tooltips for all constellation list widgets. + """ + tooltip_mapping = { + 'gpsListWidget': "GPS observation codes", + 'galListWidget': "Galileo observation codes", + 'gloListWidget': "GLONASS observation codes", + 'bdsListWidget': "BeiDou observation codes", + 'qzsListWidget': "QZSS observation codes", + } + for widget_name, label in tooltip_mapping.items(): + if hasattr(self.ui, widget_name): + getattr(self.ui, widget_name).setToolTip( + f"{label}\n" + "✓ Check / uncheck to enable / disable codes\n" + "↕ Drag and drop to set priority order (top = highest priority)" + ) + + #endregion + + #region Status Labels + + def _setup_placeholder(self): + """ + Create a placeholder label for the Constellations tab that shows when + no constellations are selected or no RINEX file is loaded. + """ + # Create the placeholder label + self._constellation_placeholder = QLabel( + "No constellations available!\n\n" + "Load a RINEX observation file and select constellations\n" + "in the General tab to configure observation codes" + ) + self._constellation_placeholder.setAlignment(Qt.AlignCenter) + self._constellation_placeholder.setWordWrap(True) + self._constellation_placeholder.setMinimumWidth(250) + self._constellation_placeholder.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self._constellation_placeholder.setStyleSheet( + "color: #bfbfbf; font-size: 13pt; margin: 15px;" + ) + + # Add to the constellations tab layout + if hasattr(self.ui, 'constellationsGridLayout'): + self.ui.constellationsGridLayout.addWidget( + self._constellation_placeholder, 0, 0, 10, 1, Qt.AlignCenter + ) + + # Initially visible + self._constellation_placeholder.setVisible(True) + + # Create explanation label for the Constellations tab + self._constellation_explanation_label = QLabel( + "Select observation codes and set priorities for each active constellation below.
" + "These observation codes are extracted from the loaded RINEX file.
" + "Red strikethrough = missing from .BIA file" + ) + self._constellation_explanation_label.setTextFormat(Qt.RichText) + self._constellation_explanation_label.setWordWrap(True) + self._constellation_explanation_label.setStyleSheet( + "color: #bfbfbf; font-size: 11pt; font-style: italic; margin-bottom: 6x; line-height: 1.4;" + ) + self._constellation_explanation_label.setVisible(False) + + # Create BIA warning label (shown when BIA fetch fails) + self._bia_warning_label = QLabel( + "⚠️ Failed to fetch BIA file for selected PPP products - unable to validate codes" + ) + self._bia_warning_label.setWordWrap(True) + self._bia_warning_label.setStyleSheet( + "QLabel { background-color: #8B4513; color: white; padding: 6px 12px; " + "border-radius: 4px; font: 10pt 'Segoe UI'; }" + ) + self._bia_warning_label.setAlignment(Qt.AlignCenter) + self._bia_warning_label.setVisible(False) + + # Create BIA loading label + self._bia_loading_label = QLabel("⏳ Loading code priorities from .BIA file...") + self._bia_loading_label.setWordWrap(True) + self._bia_loading_label.setStyleSheet( + "QLabel { background-color: #2c5d7c; color: white; padding: 8px 16px; " + "border-radius: 4px; font: 12pt 'Segoe UI'; }" + ) + self._bia_loading_label.setAlignment(Qt.AlignCenter) + self._bia_loading_label.setVisible(False) + + # Create a container widget with vertical layout for the status labels + self._constellation_status_container = QWidget() + status_layout = QVBoxLayout(self._constellation_status_container) + status_layout.setContentsMargins(0, 0, 0, 8) + status_layout.setSpacing(4) + status_layout.addWidget(self._constellation_explanation_label) + status_layout.addWidget(self._bia_warning_label) + status_layout.addWidget(self._bia_loading_label) + + # Add the status container to row 0 of the constellations grid layout + if hasattr(self.ui, 'constellationsGridLayout'): + self.ui.constellationsGridLayout.addWidget(self._constellation_status_container, 0, 0) + + def _hide_all_widgets(self): + """ + Hide all constellation labels and list widgets on startup. + They will be shown when a RINEX file is loaded and constellations are selected. + """ + widget_names = [ + 'gpsLabel', 'gpsListWidget', + 'galLabel', 'galListWidget', + 'gloLabel', 'gloListWidget', + 'bdsLabel', 'bdsListWidget', + 'qzsLabel', 'qzsListWidget', + ] + for widget_name in widget_names: + if hasattr(self.ui, widget_name): + getattr(self.ui, widget_name).setVisible(False) + + def _update_placeholder(self, show_placeholder: bool): + """ + Show or hide the constellation placeholder message. + + Arguments: + show_placeholder (bool): True to show placeholder, False to hide it. + """ + if hasattr(self, '_constellation_placeholder'): + self._constellation_placeholder.setVisible(show_placeholder) + # Show explanation label when placeholder is hidden (i.e., constellations are visible) + if hasattr(self, '_constellation_explanation_label'): + self._constellation_explanation_label.setVisible(not show_placeholder) + + #endregion + + #region Populate Observation Codes from RINEX + + def populate_observation_codes(self, result: dict): + """ + Populate the observation code list widgets with available codes from RINEX. + + Arguments: + result (dict): Dictionary containing observation code lists for each constellation. + """ + list_widget_mapping = { + 'GPS': ('obs_types_gps', 'enabled_gps', 'gpsListWidget'), + 'GAL': ('obs_types_gal', 'enabled_gal', 'galListWidget'), + 'GLO': ('obs_types_glo', 'enabled_glo', 'gloListWidget'), + 'BDS': ('obs_types_bds', 'enabled_bds', 'bdsListWidget'), + 'QZS': ('obs_types_qzs', 'enabled_qzs', 'qzsListWidget') + } + + populated_constellations = [] + + for const_name, (result_key, enabled_key, widget_name) in list_widget_mapping.items(): + if not hasattr(self.ui, widget_name): + continue + + list_widget = getattr(self.ui, widget_name) + codes = result.get(result_key, []) + enabled_codes = result.get(enabled_key, set()) + + if codes and len(codes) > 0: + self._setup_list_widget(list_widget, codes, enabled_codes) + populated_constellations.append(const_name) + else: + list_widget.clear() + list_widget.setEnabled(False) + + if populated_constellations: + Logger.workflow(f"✅ Populated observation codes for {', '.join(populated_constellations)}") + else: + Logger.workflow("⚠️ No observation codes found in RINEX") + + def _setup_list_widget(self, list_widget: QListWidget, codes: List[str], enabled_codes: set): + """ + Set up a list widget with drag-drop reordering and checkboxes for observation codes. + + Arguments: + list_widget (QListWidget): The list widget to set up. + codes (List[str]): List of observation codes to populate (in priority order). + enabled_codes (set): Set of codes that should be checked by default. + """ + list_widget.setEnabled(True) + list_widget.clear() + + # Enable drag and drop for reordering + list_widget.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + list_widget.setDefaultDropAction(Qt.DropAction.MoveAction) + list_widget.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + + # Add items with checkboxes + for code in codes: + item = QListWidgetItem(code) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled) + + # Check if this code is in the enabled set (from template priorities) + if code in enabled_codes: + item.setCheckState(Qt.CheckState.Checked) # Priority codes: checked + else: + item.setCheckState(Qt.CheckState.Unchecked) # Extra codes: unchecked + + list_widget.addItem(item) + + def extract_observation_codes(self) -> dict: + """ + Extract selected observation codes from all constellation list widgets in priority order. + + Returns: + dict: Dictionary mapping constellation names to lists of selected codes in order. + """ + obs_codes = {} + + list_widget_mapping = { + 'gps': 'gpsListWidget', + 'gal': 'galListWidget', + 'glo': 'gloListWidget', + 'bds': 'bdsListWidget', + 'qzs': 'qzsListWidget' + } + + for const_name, widget_name in list_widget_mapping.items(): + if not hasattr(self.ui, widget_name): + obs_codes[const_name] = [] + continue + + list_widget = getattr(self.ui, widget_name) + + # Extract checked items in their current order (priority order) + selected = [] + for i in range(list_widget.count()): + item = list_widget.item(i) + if item.checkState() == Qt.CheckState.Checked: + selected.append(item.text()) + + obs_codes[const_name] = selected + + return obs_codes + + #endregion + + #region Visibility of List Widgets + + def sync_list_widgets_to_selection(self): + """ + Show / hide constellation list widgets and labels based on the "General" tab's + constellation multi-select. Called when constellation selection changes. + Shows a placeholder message when no constellations are selected. + """ + selected_constellations = self.ctrl.general_tab.get_selected_constellation_set() + + widget_mapping = { + 'GPS': ('gpsLabel', 'gpsListWidget'), + 'GAL': ('galLabel', 'galListWidget'), + 'GLO': ('gloLabel', 'gloListWidget'), + 'BDS': ('bdsLabel', 'bdsListWidget'), + 'QZS': ('qzsLabel', 'qzsListWidget'), + } + + for const_name, (label_name, list_widget_name) in widget_mapping.items(): + is_enabled = const_name in selected_constellations + + if hasattr(self.ui, label_name): + getattr(self.ui, label_name).setVisible(is_enabled) + if hasattr(self.ui, list_widget_name): + getattr(self.ui, list_widget_name).setVisible(is_enabled) + + self._update_placeholder(len(selected_constellations) == 0) + + #endregion + + #region BIA Code Priority Fetching and Validation + + def on_config_tab_changed(self, index: int): + """ + UI handler: triggered when the config tab widget changes tabs. + When switching to the Constellations tab (index 1), fetch .BIA code priorities + for the current PPP selection if not already cached. + + Arguments: + index (int): The index of the newly selected tab. + """ + if index != 1: + return + + provider = self.ui.pppProviderCombo.currentText() + series = self.ui.pppSeriesCombo.currentText() + project = self.ui.pppProjectCombo.currentText() + + # Guard: Skip if any combo is empty or has placeholder values + if not provider or not series or not project: + return + if provider in ("", "None", "Select one") or series in ("", "None", "Select one") or project in ("", "None", "Select one"): + return + + # Guard: Skip if products_df is empty (happens during RINEX file change) + if self.ctrl.products_df.empty: + return + + # Check if we already have cached BIA data for this combination + if self._is_bia_cached(provider, series, project): + self._validate_codes_against_bia() + return + + # Check if we are already loading the same combination + if self._bia_loading: + if (self._bia_current_provider != provider or + self._bia_current_series != series or + self._bia_current_project != project): + Logger.console(f"🔄 BIA fetch interrupted - switching to {provider}/{series}/{project}") + else: + return + + # Start BIA fetch (will stop any existing worker first) + self._fetch_bia_code_priorities(provider, series, project) + + def _is_bia_cached(self, provider: str, series: str, project: str) -> bool: + """ + Check if BIA code priorities are cached for the given combination. + + Arguments: + provider (str): Analysis centre code. + series (str): Solution type code. + project (str): Project code. + + Returns: + bool: True if cached, False otherwise. + """ + try: + return (provider in self.ctrl.bia_code_priorities and + series in self.ctrl.bia_code_priorities[provider] and + project in self.ctrl.bia_code_priorities[provider][series]) + except (KeyError, TypeError): + return False + + def _fetch_bia_code_priorities(self, provider: str, series: str, project: str): + """ + Start background worker to fetch and parse BIA file for code priorities. + + Arguments: + provider (str): Analysis centre code. + series (str): Solution type code. + project (str): Project code. + """ + # Safety guard: don't start worker with invalid parameters + if not provider or not series or not project: + Logger.console(f"⚠️ BIA fetch skipped: invalid parameters provider='{provider}' series='{series}' project='{project}'") + return + if provider in ("", "None", "Select one") or series in ("", "None", "Select one") or project in ("", "None", "Select one"): + Logger.console(f"⚠️ BIA fetch skipped: placeholder values in parameters") + return + if self.ctrl.products_df.empty: + Logger.console(f"⚠️ BIA fetch skipped: products_df is empty") + return + + # Stop any existing BIAProductWorker before starting a new one + self.stop_bia_worker() + + self._bia_loading = True + self._show_bia_loading_indicator(True) + + # Create worker and thread + self._bia_thread = QThread() + self._bia_worker = BiasProductWorker(self.ctrl.products_df, provider, series, project) + self._bia_worker.moveToThread(self._bia_thread) + + # Connect signals + self._bia_thread.started.connect(self._bia_worker.run) + self._bia_worker.finished.connect(self._on_bia_finished) + self._bia_worker.error.connect(self._on_bia_error) + self._bia_worker.progress.connect(self._on_bia_progress) + self._bia_worker.finished.connect(self._bia_thread.quit) + self._bia_worker.error.connect(self._bia_thread.quit) + self._bia_thread.finished.connect(self._on_bia_thread_finished) + + # Store current selection for when results come back + self._bia_current_provider = provider + self._bia_current_series = series + self._bia_current_project = project + + self._bia_thread.start() + + def stop_bia_worker(self): + """ + Stop any running BIA worker and clean up thread resources. + """ + if self._bia_worker is not None: + self._bia_worker.stop() + try: + self._bia_worker.finished.disconnect() + self._bia_worker.error.disconnect() + self._bia_worker.progress.disconnect() + except (RuntimeError, TypeError): + pass + + if self._bia_thread is not None: + try: + self._bia_thread.started.disconnect() + self._bia_thread.finished.disconnect() + except (RuntimeError, TypeError): + pass + + if self._bia_thread.isRunning(): + self._bia_thread.quit() + if not self._bia_thread.wait(2000): + Logger.console("⚠️ BIA thread did not stop gracefully, forcing termination") + self._bia_thread.terminate() + self._bia_thread.wait(1000) + + self._bia_worker = None + self._bia_thread = None + self._bia_loading = False + + def _on_bia_progress(self, description: str, percent: int): + """ + UI handler: update progress during BIA fetch. + + Arguments: + description (str): Progress description. + percent (int): Progress percentage (-1 for indeterminate). + """ + if hasattr(self, '_bia_loading_label') and self._bia_loading_label: + self._bia_loading_label.setText(f"⏳ {description}") + + def _on_bia_finished(self, code_priorities: dict): + """ + UI handler: BIA fetch completed successfully. + + Arguments: + code_priorities (dict): Dictionary mapping constellation names to sets of code priorities + e.g., {'GPS': {'L1C', 'L2W'}, 'GAL': {'L1C', 'L5Q'}, ...} + """ + self._bia_loading = False + self._show_bia_loading_indicator(False) + self._show_bia_warning(False) + + # Cache the results + provider = self._bia_current_provider + series = self._bia_current_series + project = self._bia_current_project + + if provider not in self.ctrl.bia_code_priorities: + self.ctrl.bia_code_priorities[provider] = {} + if series not in self.ctrl.bia_code_priorities[provider]: + self.ctrl.bia_code_priorities[provider][series] = {} + self.ctrl.bia_code_priorities[provider][series][project] = code_priorities + + Logger.workflow(f"✅ BIA code priorities cached for {provider}/{series}/{project}") + self._validate_codes_against_bia() + + def _on_bia_error(self, error_msg: str): + """ + UI handler: BIA fetch failed. + + Arguments: + error_msg (str): Error message describing the failure. + """ + self._bia_loading = False + self._show_bia_loading_indicator(False) + + Logger.console(f"⚠️ BIA fetch error: {error_msg}") + + # Don't show warnings for cancelled fetches (user-initiated) + if "cancelled" in error_msg.lower(): + return + + self._mark_all_codes_invalid() + self._show_bia_warning(True) + Logger.workflow(f"⚠️ Failed to fetch BIA file for selected PPP products - unable to validate codes") + show_toast(self.ctrl.parent, f"⚠️ Could not fetch BIA data: {error_msg}", duration=3000) + + def _on_bia_thread_finished(self): + """ + Slot called when the BIA thread has fully finished. + Safe to clean up references here. + """ + self._bia_worker = None + self._bia_thread = None + + def _show_bia_loading_indicator(self, show: bool): + """ + Show or hide a loading indicator on the Constellations tab. + + Arguments: + show (bool): True to show, False to hide. + """ + if not hasattr(self, '_bia_loading_label') or self._bia_loading_label is None: + return + if show: + self._bia_loading_label.setText("⏳ Loading code priorities from .BIA file...") + self._bia_loading_label.setVisible(show) + + def _show_bia_warning(self, show: bool): + """ + Show or hide the BIA warning label on the Constellations tab. + + Arguments: + show (bool): True to show warning, False to hide it. + """ + if hasattr(self, '_bia_warning_label'): + self._bia_warning_label.setVisible(show) + + #endregion + + #region Code Frequency Validation Styling + + def _validate_codes_against_bia(self): + """ + Validate the codes in each constellation list widget against the cached BIA codes. + Codes that are NOT in the .BIA file are marked with strikethrough and a different colour. + """ + provider = self.ui.pppProviderCombo.currentText() + series = self.ui.pppSeriesCombo.currentText() + project = self.ui.pppProjectCombo.currentText() + + bia_codes = None + try: + bia_codes = self.ctrl.bia_code_priorities.get(provider, {}).get(series, {}).get(project, None) + except (KeyError, TypeError, AttributeError): + pass + + if not bia_codes: + self.reset_list_styling() + return + + widget_mapping = { + 'gpsListWidget': 'GPS', + 'galListWidget': 'GAL', + 'gloListWidget': 'GLO', + 'bdsListWidget': 'BDS', + 'qzsListWidget': 'QZS', + } + + # Colours for codes + valid_color = QColor('white') + invalid_color = QColor('#FF6B6B') + + for widget_name, constellation in widget_mapping.items(): + if not hasattr(self.ui, widget_name): + continue + + list_widget = getattr(self.ui, widget_name) + constellation_bia_codes = bia_codes.get(constellation, set()) + + for i in range(list_widget.count()): + item = list_widget.item(i) + if item is None: + continue + + code = item.text().strip() + font = item.font() + + if code in constellation_bia_codes: + font.setStrikeOut(False) + item.setFont(font) + item.setForeground(QBrush(valid_color)) + else: + font.setStrikeOut(True) + item.setFont(font) + item.setForeground(QBrush(invalid_color)) + + Logger.workflow(f"✅ Validated constellation codes against BIA for {provider}/{series}/{project}") + + def reset_list_styling(self): + """ + Reset all constellation list widget items to normal styling (no strikethrough, white colour). + Called when BIA data is not available. + """ + widget_names = ['gpsListWidget', 'galListWidget', 'gloListWidget', 'bdsListWidget', 'qzsListWidget'] + normal_color = QColor('white') + + for widget_name in widget_names: + if not hasattr(self.ui, widget_name): + continue + list_widget = getattr(self.ui, widget_name) + for i in range(list_widget.count()): + item = list_widget.item(i) + if item is None: + continue + font = item.font() + font.setStrikeOut(False) + item.setFont(font) + item.setForeground(QBrush(normal_color)) + + self._show_bia_warning(False) + + def _mark_all_codes_invalid(self): + """ + Mark all constellation list widget items as invalid (red strikethrough). + Called when BIA file fetch fails. + """ + widget_names = ['gpsListWidget', 'galListWidget', 'gloListWidget', 'bdsListWidget', 'qzsListWidget'] + invalid_color = QColor('#ff6b6b') + + for widget_name in widget_names: + if not hasattr(self.ui, widget_name): + continue + list_widget = getattr(self.ui, widget_name) + for i in range(list_widget.count()): + item = list_widget.item(i) + if item is None: + continue + font = item.font() + font.setStrikeOut(True) + item.setFont(font) + item.setForeground(QBrush(invalid_color)) + + #endregion + + #region Reset to Defaults + + def reset_to_defaults(self): + """ + Reset all Constellations tab fields to their default/initial states. + """ + list_widgets = ['gpsListWidget', 'galListWidget', 'gloListWidget', 'bdsListWidget', 'qzsListWidget'] + for widget_name in list_widgets: + if hasattr(self.ui, widget_name): + list_widget = getattr(self.ui, widget_name) + list_widget.clear() + list_widget.setEnabled(False) + + self._hide_all_widgets() + self._update_placeholder(True) + + #endregion \ No newline at end of file diff --git a/scripts/GinanUI/app/controllers/general_config_controller.py b/scripts/GinanUI/app/controllers/general_config_controller.py new file mode 100644 index 000000000..2662b9472 --- /dev/null +++ b/scripts/GinanUI/app/controllers/general_config_controller.py @@ -0,0 +1,1369 @@ +""" +Controller for the General configuration tab. + +Manages the following UI widgets and background workflows: + - Mode combo (Static / Kinematic / Dynamic) + - Constellations multi-select combo + - PPP Provider / Project / Series combos + - Receiver Type and Antenna Type (free-text combos) + - Antenna Offset button / dialog + - Apriori Position button / dialog + - Time Window button / dialog + - Data Interval button / dialog + - CDDIS archive scanning for valid PPP analysis centres (DownloadWorker) + - SINEX validation against RINEX-extracted metadata (SinexValidationWorker) + - Constellation info retrieval from SP3 headers +""" + +from __future__ import annotations +from datetime import datetime +from pathlib import Path +from typing import Callable, List +import pandas as pd +from PySide6.QtCore import QDateTime, QObject, QThread, Qt +from PySide6.QtGui import QStandardItem, QStandardItemModel +from PySide6.QtWidgets import ( + QComboBox, + QDateTimeEdit, + QDialog, + QDoubleSpinBox, + QFormLayout, + QHBoxLayout, + QInputDialog, + QLineEdit, + QMessageBox, + QPushButton, + QSizePolicy, +) +from scripts.GinanUI.app.models.dl_products import ( + get_valid_analysis_centers, + get_valid_series_for_provider, +) +from scripts.GinanUI.app.models.execution import INPUT_PRODUCTS_PATH +from scripts.GinanUI.app.utils.logger import Logger +from scripts.GinanUI.app.utils.toast import show_toast +from scripts.GinanUI.app.utils.workers import DownloadWorker, SinexValidationWorker + +class GeneralConfigController(QObject): + """ + Manages the General configuration tab widgets and the background workflows + (CDDIS scanning, SINEX validation) that are triggered from this tab. + + Arguments: + ui: The main window UI instance. + input_ctrl: The parent InputController instance (for accessing shared state). + """ + + def __init__(self, ui, input_ctrl): + """ + Initialise config panel bindings and background worker state. + + Arguments: + ui: The main window UI instance. + input_ctrl: The parent InputController that owns shared state. + """ + super().__init__(parent=input_ctrl) + self.ui = ui + self.ctrl = input_ctrl # parent InputController + + # Mode combo + self._bind_combo(self.ui.modeCombo, lambda: ["Static", "Kinematic", "Dynamic"]) + + # PPP provider, project and series + self.ui.pppProviderCombo.currentTextChanged.connect(self._on_ppp_provider_changed) + self.ui.pppProjectCombo.currentTextChanged.connect(self._on_ppp_project_changed) + self.ui.pppSeriesCombo.currentTextChanged.connect(self._on_ppp_series_changed) + + # Constellations multi-select + self._bind_multiselect_combo( + self.ui.constellationsCombo, + lambda: ["GPS", "GAL", "GLO", "BDS", "QZS"], + self.ui.constellationsValue, + placeholder="Select one or more", + ) + + # Receiver/Antenna types: free-text input + self._enable_free_text_for_receiver_and_antenna() + + # Antenna offset + self.ui.antennaOffsetButton.clicked.connect(self._open_antenna_offset_dialog) + self.ui.antennaOffsetButton.setCursor(Qt.CursorShape.PointingHandCursor) + self.ui.antennaOffsetValue.setText("0.0, 0.0, 0.0") + + # Apriori position + self.ui.aprioriPositionButton.clicked.connect(self._open_apriori_position_dialog) + self.ui.aprioriPositionButton.setCursor(Qt.CursorShape.PointingHandCursor) + + # Time window and data interval + self.ui.timeWindowButton.clicked.connect(self._open_time_window_dialog) + self.ui.timeWindowButton.setCursor(Qt.CursorShape.PointingHandCursor) + self.ui.dataIntervalButton.clicked.connect(self._open_data_interval_dialog) + self.ui.dataIntervalButton.setCursor(Qt.CursorShape.PointingHandCursor) + + # CDDIS analysis centre scan worker tracking + self._worker = None + self._metadata_thread = None + self._pending_threads = [] + + # SINEX validation worker tracking + self._sinex_worker = None + self._sinex_thread = None + self._sinex_path = None + + #region UI Tooltips + + def setup_tooltips(self): + """ + Set up tooltips for all General config tab widgets. + """ + self.ui.modeCombo.setToolTip( + "Processing mode:\n" + "• Static: For stationary receivers\n" + "• Kinematic: For moving receivers\n" + "• Dynamic: For high-dynamic applications" + ) + self.ui.constellationsCombo.setToolTip( + "Select which GNSS constellations to use:\n" + "GPS, Galileo (GAL), GLONASS (GLO), BeiDou (BDS), QZSS (QZS)\n" + "More constellations generally improve accuracy" + ) + self.ui.pppProviderCombo.setToolTip( + "Analysis centre that provides PPP products\n" + "Options populated based on your observation time window" + ) + self.ui.pppProjectCombo.setToolTip( + "PPP product project type.\n" + "Different projects types offer varying GNSS constellation PPP products." + ) + self.ui.pppSeriesCombo.setToolTip( + "PPP product series:\n" + "• ULT: Ultra-rapid (lower latency)\n" + "• RAP: Rapid \n" + "• FIN: Final (highest accuracy)" + ) + self.ui.receiverTypeCombo.setToolTip( + "Receiver model extracted from RINEX header\n" + "Click to manually edit if needed" + ) + self.ui.antennaTypeCombo.setToolTip( + "Antenna model extracted from RINEX header\n" + "Must match entries in the ANTEX (.atx) calibration file\n" + "Click to manually edit if needed" + ) + self.ui.timeWindowButton.setToolTip( + "Observation time window extracted from RINEX file\n" + "Click to adjust start and end times for processing" + ) + self.ui.dataIntervalButton.setToolTip( + "Data sampling interval in seconds\n" + "Click to change the processing interval" + ) + self.ui.antennaOffsetButton.setToolTip( + "Antenna reference point offset in metres (East, North, Up)\n" + "Typically extracted from RINEX header\n" + "Click to modify if needed" + ) + self.ui.aprioriPositionButton.setToolTip( + "Approximate receiver position in ECEF coordinates (X, Y, Z) in metres\n" + "Typically extracted from RINEX header\n" + "Click to modify if needed" + ) + self.ui.receiverTypeValue.setToolTip("Receiver type from RINEX header") + self.ui.antennaTypeValue.setToolTip("Antenna type from RINEX header") + self.ui.constellationsValue.setToolTip("Available constellations in RINEX data") + self.ui.timeWindowValue.setToolTip("Observation time span") + self.ui.dataIntervalValue.setToolTip("Data sampling interval") + self.ui.antennaOffsetValue.setToolTip("Antenna offset: East, North, Up (metres)") + + #endregion + + #region UI Population from RINEX Extraction + + def populate_from_rinex(self, result: dict): + """ + Populate the General config tab fields with extracted RINEX metadata. + + Arguments: + result (dict): Dictionary from RinexExtractor.extract_rinex_data(). + """ + self.ui.constellationsValue.setText(result["constellations"]) + self.ui.timeWindowValue.setText(f"{result['start_epoch']} to {result['end_epoch']}") + self.ui.timeWindowButton.setText(f"{result['start_epoch']} to {result['end_epoch']}") + self.ui.dataIntervalButton.setText(f"{result['epoch_interval']} s") + self.ctrl.rinex_epoch_interval = float(result['epoch_interval']) + self.ui.receiverTypeValue.setText(result["receiver_type"]) + self.ui.antennaTypeValue.setText(result["antenna_type"]) + self.ui.antennaOffsetValue.setText(", ".join(map(str, result["antenna_offset"]))) + self.ui.antennaOffsetButton.setText(", ".join(map(str, result["antenna_offset"]))) + + # Populate apriori position if available + apriori = result.get("apriori_position") + if apriori and any(v != 0.0 for v in apriori): + self.ui.aprioriPositionButton.setText(", ".join(map(str, apriori))) + else: + self.ui.aprioriPositionButton.setText("0.0, 0.0, 0.0") + + # Receiver and antenna type combos + self.ui.receiverTypeCombo.clear() + self.ui.receiverTypeCombo.addItem(result["receiver_type"]) + self.ui.receiverTypeCombo.setCurrentIndex(0) + self.ui.receiverTypeCombo.lineEdit().setText(result["receiver_type"]) + + self.ui.antennaTypeCombo.clear() + self.ui.antennaTypeCombo.addItem(result["antenna_type"]) + self.ui.antennaTypeCombo.setCurrentIndex(0) + self.ui.antennaTypeCombo.lineEdit().setText(result["antenna_type"]) + + # Constellation multi-select + self._update_constellations_multiselect(result["constellations"]) + + #endregion + + #region Constellations Multi-Select + + def _update_constellations_multiselect(self, constellation_str: str): + """ + Populate and mirror a multi-select constellation combo with checkboxes. + + Arguments: + constellation_str (str): Comma-separated constellations (e.g., "GPS, GAL, GLO"). + """ + constellations = [c.strip() for c in constellation_str.split(",") if c.strip()] + combo = self.ui.constellationsCombo + + # Remove previous bindings + if hasattr(combo, '_old_showPopup'): + delattr(combo, '_old_showPopup') + + combo.clear() + combo.setEditable(True) + combo.lineEdit().setReadOnly(True) + combo.setInsertPolicy(QComboBox.NoInsert) + + # Build the item model + model = QStandardItemModel(combo) + for txt in constellations: + item = QStandardItem(txt) + item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) + item.setCheckState(Qt.Checked) + model.appendRow(item) + + def on_item_changed(_item): + selected = [ + model.item(i).text() + for i in range(model.rowCount()) + if model.item(i).checkState() == Qt.Checked + ] + label = ", ".join(selected) if selected else "Select one or more" + combo.lineEdit().setText(label) + self.ui.constellationsValue.setText(label) + self.ctrl.constellations_tab.sync_list_widgets_to_selection() + + model.itemChanged.connect(on_item_changed) + combo.setModel(model) + combo.setCurrentIndex(-1) + + # Custom showPopup function to keep things reset + def show_popup_constellation(): + if combo.model() != model: + combo.setModel(model) + combo.setCurrentIndex(-1) + QComboBox.showPopup(combo) + + combo.showPopup = show_popup_constellation + + # Store for access and event consistency + combo._constellation_model = model + combo._constellation_on_item_changed = on_item_changed + + # Set initial label text + combo.lineEdit().setText(", ".join(constellations)) + self.ui.constellationsValue.setText(", ".join(constellations)) + + # Initial sync of list widgets + self.ctrl.constellations_tab.sync_list_widgets_to_selection() + + def get_selected_constellations_text(self) -> str: + """ + Return comma-separated text of currently selected constellations from the General tab combo. + + Returns: + str: e.g. "GPS, GAL, GLO" or fallback from the label. + """ + combo = self.ui.constellationsCombo + if hasattr(combo, '_constellation_model') and combo._constellation_model: + model = combo._constellation_model + selected = [model.item(i).text() for i in range(model.rowCount()) if model.item(i).checkState() == Qt.Checked] + return ", ".join(selected) + # Fallback to the label text if no custom model exists + return self.ui.constellationsValue.text() + + def get_selected_constellation_set(self) -> set: + """ + Return a set of currently selected constellation names (upper-cased). + + Returns: + set[str]: e.g. {'GPS', 'GAL', 'GLO'} + """ + selected = set() + combo = self.ui.constellationsCombo + if hasattr(combo, '_constellation_model') and combo._constellation_model: + model = combo._constellation_model + for i in range(model.rowCount()): + if model.item(i).checkState() == Qt.Checked: + selected.add(model.item(i).text().upper()) + return selected + + def update_constellations_for_ppp_selection(self): + """ + Update the constellations combobox to enable / disable items based on the + currently selected PPP provider/series/project combination. + Constellations supported by the selected combination are enabled and checked, + unsupported constellations are disabled and unchecked. + """ + combo = self.ui.constellationsCombo + if not hasattr(combo, '_constellation_model') or combo._constellation_model is None: + return + + model = combo._constellation_model + + # Get current PPP selection + provider = self.ui.pppProviderCombo.currentText() + series = self.ui.pppSeriesCombo.currentText() + project = self.ui.pppProjectCombo.currentText() + + # Get available constellations for this combination + available_constellations = set() + if hasattr(self.ctrl, 'provider_constellations') and self.ctrl.provider_constellations: + try: + available_constellations = self.ctrl.provider_constellations.get(provider, {}).get(series, {}).get(project, set()) + except (KeyError, AttributeError): + available_constellations = set() + + # If no constellation info available, enable all (fallback behaviour) + if not available_constellations: + for i in range(model.rowCount()): + item = model.item(i) + item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) + return + + # Block signals to prevent triggering on_item_changed multiple times + model.blockSignals(True) + + # Update each constellation item + for i in range(model.rowCount()): + item = model.item(i) + constellation_name = item.text().upper() + + if constellation_name in available_constellations: + # Enable and check this constellation + #item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) # Un-comment to also disable checkability + item.setCheckState(Qt.Checked) + else: + # Disable and uncheck this constellation + #item.setFlags(Qt.ItemIsUserCheckable) # Un-comment to also disable checkability + item.setCheckState(Qt.Unchecked) + + model.blockSignals(False) + + # Update the label text to show only enabled/checked constellations + selected = [ + model.item(i).text() + for i in range(model.rowCount()) + if model.item(i).checkState() == Qt.Checked + ] + label = ", ".join(selected) if selected else "Select one or more" + combo.lineEdit().setText(label) + self.ui.constellationsValue.setText(label) + + # Sync the constellation list widgets + self.ctrl.constellations_tab.sync_list_widgets_to_selection() + + #endregion + + # region Time Window Dialog + + def _open_time_window_dialog(self): + """ + UI handler: open dialog to adjust observation start/end times. + """ + dlg = QDialog(self.ui.timeWindowButton) + dlg.setWindowTitle("Time Window") + + current_text = self.ui.timeWindowButton.text() + try: + s_text, e_text = current_text.split(" to ") + s_dt = QDateTime.fromString(s_text, "yyyy-MM-dd_HH:mm:ss") + e_dt = QDateTime.fromString(e_text, "yyyy-MM-dd_HH:mm:ss") + if not s_dt.isValid(): + s_dt = QDateTime.fromString(s_text, "yyyy-MM-dd HH:mm:ss") + if not e_dt.isValid(): + e_dt = QDateTime.fromString(e_text, "yyyy-MM-dd HH:mm:ss") + except Exception: + s_dt = e_dt = QDateTime.currentDateTime() + + form = QFormLayout(dlg) + start_edit = QDateTimeEdit(s_dt, dlg) + end_edit = QDateTimeEdit(e_dt, dlg) + start_edit.setCalendarPopup(True) + end_edit.setCalendarPopup(True) + start_edit.setDisplayFormat("yyyy-MM-dd_HH:mm:ss") + end_edit.setDisplayFormat("yyyy-MM-dd_HH:mm:ss") + start_edit.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + end_edit.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + form.addRow("Start:", start_edit) + form.addRow("End:", end_edit) + + btn_row = QHBoxLayout() + ok_btn = QPushButton("OK", dlg) + cancel_btn = QPushButton("Cancel", dlg) + btn_row.addWidget(ok_btn) + btn_row.addWidget(cancel_btn) + form.addRow(btn_row) + + ok_btn.clicked.connect(lambda: self._set_time_window(start_edit, end_edit, dlg)) + cancel_btn.clicked.connect(dlg.reject) + dlg.setMinimumWidth(300) + dlg.setFixedHeight(dlg.sizeHint().height()) + dlg.exec() + + def _set_time_window(self, start_edit, end_edit, dlg: QDialog): + """ + UI handler: validate and set selected time window into UI. + + Arguments: + start_edit (QDateTimeEdit): Start time widget. + end_edit (QDateTimeEdit): End time widget. + dlg (QDialog): Dialog to accept/close. + """ + if end_edit.dateTime() < start_edit.dateTime(): + QMessageBox.warning(dlg, "Time error", + "End time cannot be earlier than start time.\nPlease select again.") + return + + s = start_edit.dateTime().toString("yyyy-MM-dd_HH:mm:ss") + e = end_edit.dateTime().toString("yyyy-MM-dd_HH:mm:ss") + self.ui.timeWindowButton.setText(f"{s} to {e}") + self.ui.timeWindowValue.setText(f"{s} to {e}") + dlg.accept() + + # endregion + + # region Data Interval Dialog + + def _open_data_interval_dialog(self): + """ + UI handler: open dialog to adjust data interval (seconds). + """ + dlg = QDialog(self.ui.dataIntervalButton) + dlg.setWindowTitle("Data Interval") + + current_text = self.ui.dataIntervalButton.text().replace(" s", "").strip() + try: + current_val = float(current_text) + except ValueError: + current_val = 1.0 + + form = QFormLayout(dlg) + interval_spin = QDoubleSpinBox(dlg) + interval_spin.setRange(0.01, 999999.99) + interval_spin.setDecimals(2) + interval_spin.setValue(current_val) + interval_spin.setSuffix(" s") + interval_spin.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + form.addRow("Interval:", interval_spin) + + btn_row = QHBoxLayout() + ok_btn = QPushButton("OK", dlg) + cancel_btn = QPushButton("Cancel", dlg) + btn_row.addWidget(ok_btn) + btn_row.addWidget(cancel_btn) + form.addRow(btn_row) + + ok_btn.clicked.connect(lambda: self._set_data_interval(interval_spin, dlg)) + cancel_btn.clicked.connect(dlg.reject) + dlg.setMinimumWidth(300) + dlg.setFixedHeight(dlg.sizeHint().height()) + dlg.exec() + + def _set_data_interval(self, interval_spin, dlg: QDialog): + """ + UI handler: apply data interval value back to UI. + + Arguments: + interval_spin (QDoubleSpinBox): Interval spin box. + dlg (QDialog): Dialog to accept/close. + """ + val = interval_spin.value() + text = f"{int(val)} s" if val == int(val) else f"{val:.2f} s" + self.ui.dataIntervalButton.setText(text) + self.ui.dataIntervalValue.setText(text) + dlg.accept() + + # endregion + + # region Receiver / Antenna Type Dialog + + def _enable_free_text_for_receiver_and_antenna(self): + """ + Allow users to enter custom receiver/antenna types via popup, mirroring to UI. + """ + self.ui.receiverTypeCombo.setEditable(True) + self.ui.receiverTypeCombo.lineEdit().setReadOnly(True) + self.ui.antennaTypeCombo.setEditable(True) + self.ui.antennaTypeCombo.lineEdit().setReadOnly(True) + + # Receiver type free text + def _ask_receiver_type(): + current_text = self.ui.receiverTypeCombo.currentText().strip() + text, ok = QInputDialog.getText( + self.ui.receiverTypeCombo, + "Receiver Type", + "Enter receiver type:", + text=current_text + ) + if ok and text: + self.ui.receiverTypeCombo.clear() + self.ui.receiverTypeCombo.addItem(text) + self.ui.receiverTypeCombo.lineEdit().setText(text) + self.ui.receiverTypeValue.setText(text) + + self.ui.receiverTypeCombo.showPopup = _ask_receiver_type + + # Antenna type free text + def _ask_antenna_type(): + current_text = self.ui.antennaTypeCombo.currentText().strip() + text, ok = QInputDialog.getText( + self.ui.antennaTypeCombo, + "Antenna Type", + "Enter antenna type:", + text=current_text + ) + if ok and text: + self.ui.antennaTypeCombo.clear() + self.ui.antennaTypeCombo.addItem(text) + self.ui.antennaTypeCombo.lineEdit().setText(text) + self.ui.antennaTypeValue.setText(text) + + self.ui.antennaTypeCombo.showPopup = _ask_antenna_type + + # endregion + + # region Antenna Type Verification + + def verify_antenna_type(self, result: dict): + """ + UI handler: verify that the RINEX antenna_type exists in the selected ANTEX (.atx) file. + + Arguments: + result (dict): RINEX extraction result containing 'antenna_type'. + """ + atx_path = self._get_best_atx_path() + + with open(atx_path, "r") as file: + for line in file: + label = line[60:].strip() + + # Read and find antenna_type tag + if label == "TYPE / SERIAL NO" and line[20:24].strip() == "": + valid_antenna_type = line[0:20] + + if len(valid_antenna_type.strip()) < 16 or not valid_antenna_type[16:].strip(): + # Just the antenna part is included, need to add radome (cover) + antenna_part = valid_antenna_type[:15].strip() + valid_antenna_type = f"{antenna_part:<15} NONE" + + # Do same normalisation for result["antenna_type"] + result_antenna = result["antenna_type"] + + if len(result_antenna.strip()) < 16 or ( + len(result_antenna) > 16 and not result_antenna[16:].strip()): + antenna_part = result_antenna[:15].strip() + result_antenna = f"{antenna_part:<15} NONE" + + # Compare strings + if result_antenna.strip() == valid_antenna_type.strip(): + Logger.workflow("✅ Antenna type verified from .atx file") + return + + # Not found! Return warning to user + QMessageBox.warning( + None, + "Provided Antenna Type Invalid", + f'Provided antenna type in .rnx file: "{result["antenna_type"]}"\n' + f'not found in .atx file: "{atx_path}"' + ) + Logger.workflow(f"⚠️ Antenna type failed to verify from .atx file: {atx_path}") + return + + def _get_best_atx_path(self): + """ + Select the best available ANTEX (.atx) file with a priority order. + + Returns: + Path: Path to the best available .atx file. + + Raises: + FileNotFoundError: If no .atx file is found. + """ + atx_files = list(INPUT_PRODUCTS_PATH.glob("*.atx")) + if len(atx_files) == 0: + raise FileNotFoundError("No .atx file found") + elif len(atx_files) > 1: + priority_order = ['igs20.atx', 'igs14.atx', 'igs13.atx', 'igs08.atx', 'igs05.atx'] + atx_path = None + for best_atx in priority_order: + matching_files = [f for f in atx_files if f.name == best_atx] + if matching_files: + atx_path = matching_files[0] + Logger.workflow(f"📁 Selected .atx file: {atx_path.name} based on priority") + break + + if atx_path is None: + atx_path = atx_files[0] + Logger.workflow(f"📁 Selected .atx file: {atx_path.name} based on fallback") + else: + atx_path = atx_files[0] + return atx_path + + # endregion + + # region Antenna Offset Dialog + + def _open_antenna_offset_dialog(self): + """ + UI handler: open antenna offset dialog (E, N, U) with text input fields. + """ + dlg = QDialog(self.ui.antennaOffsetButton) + dlg.setWindowTitle("Antenna Offset") + + try: + e0, n0, u0 = [x.strip() for x in self.ui.antennaOffsetValue.text().split(",")] + except Exception: + e0 = n0 = u0 = "0.0" + + form = QFormLayout(dlg) + edit_e = QLineEdit(str(e0), dlg) + edit_n = QLineEdit(str(n0), dlg) + edit_u = QLineEdit(str(u0), dlg) + form.addRow("E:", edit_e) + form.addRow("N:", edit_n) + form.addRow("U:", edit_u) + + btn_row = QHBoxLayout() + ok_btn = QPushButton("OK", dlg) + cancel_btn = QPushButton("Cancel", dlg) + btn_row.addWidget(ok_btn) + btn_row.addWidget(cancel_btn) + form.addRow(btn_row) + + ok_btn.clicked.connect(lambda: self._set_antenna_offset(edit_e, edit_n, edit_u, dlg)) + cancel_btn.clicked.connect(dlg.reject) + dlg.setMinimumWidth(300) + dlg.setFixedHeight(dlg.sizeHint().height()) + dlg.exec() + + def _set_antenna_offset(self, edit_e, edit_n, edit_u, dlg: QDialog): + """ + UI handler: apply antenna offset values back to UI. + + Arguments: + edit_e (QLineEdit): East (E) input field. + edit_n (QLineEdit): North (N) input field. + edit_u (QLineEdit): Up (U) input field. + dlg (QDialog): Dialog to accept/close. + """ + try: + e = float(edit_e.text().strip()) + n = float(edit_n.text().strip()) + u = float(edit_u.text().strip()) + except ValueError: + QMessageBox.warning(dlg, "Invalid input", "Please enter valid numeric values.") + return + + text = f"{e}, {n}, {u}" + self.ui.antennaOffsetButton.setText(text) + self.ui.antennaOffsetValue.setText(text) + dlg.accept() + + # endregion + + # region Apriori Position Dialog + + def _open_apriori_position_dialog(self): + """ + UI handler: open apriori position dialog (X, Y, Z) with text input fields. + """ + dlg = QDialog(self.ui.aprioriPositionButton) + dlg.setWindowTitle("Apriori Position (ECEF)") + + try: + x0, y0, z0 = [x.strip() for x in self.ui.aprioriPositionButton.text().split(",")] + except Exception: + x0 = y0 = z0 = "0.0" + + form = QFormLayout(dlg) + edit_x = QLineEdit(str(x0), dlg) + edit_y = QLineEdit(str(y0), dlg) + edit_z = QLineEdit(str(z0), dlg) + form.addRow("X:", edit_x) + form.addRow("Y:", edit_y) + form.addRow("Z:", edit_z) + + btn_row = QHBoxLayout() + ok_btn = QPushButton("OK", dlg) + cancel_btn = QPushButton("Cancel", dlg) + btn_row.addWidget(ok_btn) + btn_row.addWidget(cancel_btn) + form.addRow(btn_row) + + ok_btn.clicked.connect(lambda: self._set_apriori_position(edit_x, edit_y, edit_z, dlg)) + cancel_btn.clicked.connect(dlg.reject) + dlg.setMinimumWidth(300) + dlg.setFixedHeight(dlg.sizeHint().height()) + dlg.exec() + + def _set_apriori_position(self, edit_x, edit_y, edit_z, dlg: QDialog): + """ + UI handler: apply apriori position values back to UI. + + Arguments: + edit_x (QLineEdit): X coordinate input field. + edit_y (QLineEdit): Y coordinate input field. + edit_z (QLineEdit): Z coordinate input field. + dlg (QDialog): Dialog to accept/close. + """ + try: + x = float(edit_x.text().strip()) + y = float(edit_y.text().strip()) + z = float(edit_z.text().strip()) + except ValueError: + QMessageBox.warning(dlg, "Invalid input", "Please enter valid numeric values.") + return + + text = f"{x}, {y}, {z}" + self.ui.aprioriPositionButton.setText(text) + dlg.accept() + + # endregion + + # region SINEX Validation + + def start_sinex_validation(self, target_date: datetime, marker_name: str, receiver_type: str, + antenna_type: str, antenna_offset: list, apriori_position: list = None): + """ + Start SINEX validation in a background thread. + + Arguments: + target_date (datetime): Date for which to download the SINEX file. + marker_name (str): 4-character marker name from RINEX. + receiver_type (str): Receiver type from RINEX. + antenna_type (str): Antenna type from RINEX. + antenna_offset (list): Antenna offset [E, N, U] from RINEX. + apriori_position (list): Optional apriori position [X, Y, Z] from RINEX. + """ + if not marker_name or len(marker_name) < 4: + Logger.workflow("⚠️ Invalid marker name - SINEX validation skipped") + return + + # Stop any existing SINEX worker + self._stop_sinex_worker() + + Logger.workflow(f"📋 Starting SINEX validation for marker '{marker_name[:4]}'...") + + # Create worker and thread + self._sinex_worker = SinexValidationWorker( + target_date=target_date, + marker_name=marker_name[:4], # Use first 4 characters + receiver_type=receiver_type, + antenna_type=antenna_type, + antenna_offset=antenna_offset, + apriori_position=apriori_position, + ) + self._sinex_thread = QThread() + self._sinex_worker.moveToThread(self._sinex_thread) + + # Connect signals + self._sinex_worker.finished.connect(self._on_sinex_validation_finished) + self._sinex_worker.error.connect(self._on_sinex_validation_error) + self._sinex_worker.progress.connect(self._on_sinex_validation_progress) + + self._sinex_thread.started.connect(self._sinex_worker.run) + self._sinex_worker.finished.connect(self._sinex_thread.quit) + self._sinex_worker.error.connect(self._sinex_thread.quit) + self._sinex_thread.finished.connect(self._on_sinex_thread_finished) + + self._sinex_thread.start() + + def _stop_sinex_worker(self): + """ + Stop any running SINEX validation worker and clean up thread resources. + """ + if self._sinex_worker is not None: + self._sinex_worker.stop() + try: + self._sinex_worker.finished.disconnect() + self._sinex_worker.error.disconnect() + self._sinex_worker.progress.disconnect() + except (RuntimeError, TypeError): + pass + + if self._sinex_thread is not None: + try: + self._sinex_thread.started.disconnect() + self._sinex_thread.finished.disconnect() + except (RuntimeError, TypeError): + pass + + if self._sinex_thread.isRunning(): + self._sinex_thread.quit() + if not self._sinex_thread.wait(2000): + Logger.console("⚠️ SINEX thread did not stop gracefully, forcing termination") + self._sinex_thread.terminate() + self._sinex_thread.wait(1000) + + self._sinex_worker = None + self._sinex_thread = None + + def _on_sinex_validation_progress(self, description: str, percent: int): + """ + UI handler: update progress bar during SINEX download. + + Arguments: + description (str): Progress description (filename). + percent (int): Progress percentage (0-100). + """ + if hasattr(self.ui, 'progressBar'): + self.ui.progressBar.setValue(percent) + self.ui.progressBar.setFormat(f"📥 {description}: {percent}%") + + def _on_sinex_validation_finished(self, sinex_path, validation_results: dict): + """ + UI handler: SINEX validation completed. + + Arguments: + sinex_path (Path): Path to the downloaded SINEX file. + validation_results (dict): Validation results dictionary. + """ + self._sinex_path = sinex_path + + # Store the SINEX filename for later use in apply_ui_config() + if sinex_path is not None: + self.ctrl._sinex_filename = sinex_path.name + + # Reset progress bar + if hasattr(self.ui, 'progressBar'): + self.ui.progressBar.setValue(0) + self.ui.progressBar.setFormat("") + + # Check validation results and show appropriate toast + if 'error' in validation_results: + show_toast(self.ctrl.parent, f"⚠️ SINEX validation error: {validation_results['error']}", duration=5000) + return + + if not validation_results.get('marker_found', False): + show_toast(self.ctrl.parent, f"ℹ️ Marker not found in SINEX file", duration=3000) + return + + # Apply SINEX apriori_position to UI if available (SINEX is more accurate than RINEX) + apriori_result = validation_results.get('apriori_position', {}) + sinex_position = apriori_result.get('sinex_value') + if sinex_position is not None and len(sinex_position) == 3: + position_str = ", ".join(str(v) for v in sinex_position) + self.ui.aprioriPositionButton.setText(position_str) + + # Check if all validations passed + all_valid = True + has_validations = False + for field in ['receiver_type', 'antenna_type', 'antenna_offset', 'apriori_position']: + field_result = validation_results.get(field, {}) + if field_result.get('valid') is True: + has_validations = True + elif field_result.get('valid') is False: + all_valid = False + has_validations = True + + if has_validations: + if all_valid: + show_toast(self.ctrl.parent, "✅ SINEX validation passed", duration=3000) + else: + show_toast(self.ctrl.parent, "⚠️ SINEX validation warnings - check workflow", duration=5000) + + def _on_sinex_validation_error(self, error_msg: str): + """ + UI handler: SINEX validation failed. + + Arguments: + error_msg (str): Error message describing the failure. + """ + # Don't show a toast for cancelled operations + if "cancelled" in error_msg.lower(): + return + + # A skipped validation (e.g. the SINEX file does not exist for this date) + # is expected behaviour, not a failure - inform the user and move on. + if "skipped" in error_msg.lower(): + show_toast(self.ctrl.parent, "ℹ️ SINEX unavailable - validation skipped", duration=3000) + return + + Logger.workflow(f"⚠️ SINEX validation error: {error_msg}") + show_toast(self.ctrl.parent, f"⚠️ SINEX validation failed: {error_msg}", duration=5000) + + def _on_sinex_thread_finished(self): + """ + Slot called when the SINEX thread has fully finished. + Safe to clean up references here. + """ + self._sinex_worker = None + self._sinex_thread = None + + # endregion + + #region PPP Provider / Series / Project Combos + + def _on_ppp_provider_changed(self, provider_name: str): + """ + UI handler: when PPP provider changes, refresh project and series options. + Only shows series that have all required files (SP3, BIA, CLK). + """ + if not provider_name or provider_name.strip() == "": + return + try: + # Get valid series for this provider (only those with all required files) + valid_series = get_valid_series_for_provider(self.ctrl.products_df, provider_name) + + if not valid_series: + raise ValueError(f"No valid series (with all required files) for provider: {provider_name}") + + # Get DataFrame of valid (project, series) pairs - filter for valid series only + df = self.ctrl.products_df.loc[ + (self.ctrl.products_df["analysis_center"] == provider_name) & + (self.ctrl.products_df["solution_type"].isin(valid_series)), + ["project", "solution_type"]] + + if df.empty: + raise ValueError(f"No valid project–series combinations for provider: {provider_name}") + + # Store for future filtering if needed + self.ctrl._valid_project_series_df = df + self.ctrl._valid_series_for_provider = valid_series # Cache valid series + + project_options = sorted(df['project'].unique()) + series_options = sorted(df['solution_type'].unique()) + + # Block signals before clearing and populating to prevent any duplicates in dropdown + self.ui.pppProjectCombo.blockSignals(True) + self.ui.pppSeriesCombo.blockSignals(True) + + self.ui.pppProjectCombo.clear() + self.ui.pppSeriesCombo.clear() + + self.ui.pppProjectCombo.addItems(project_options) + self.ui.pppSeriesCombo.addItems(series_options) + + self.ui.pppProjectCombo.setCurrentIndex(0) + self.ui.pppSeriesCombo.setCurrentIndex(0) + + # Unblock signals now that the population is complete + self.ui.pppProjectCombo.blockSignals(False) + self.ui.pppSeriesCombo.blockSignals(False) + + # Update constellations combobox based on new PPP selection + self.update_constellations_for_ppp_selection() + + # If we're on the Constellations tab, trigger BIA fetch for new selection + if self.ui.configTabWidget.currentIndex() == 1: + self.ctrl.constellations_tab.on_config_tab_changed(1) + + except Exception as e: + self.ui.pppSeriesCombo.clear() + self.ui.pppSeriesCombo.addItem("None") + self.ui.pppProjectCombo.clear() + self.ui.pppProjectCombo.addItem("None") + + def _on_ppp_series_changed(self, selected_series: str): + """ + UI handler: when PPP series changes, filter valid projects. + + Arguments: + selected_series (str): Series code, e.g., 'ULT', 'RAP', 'FIN'. + """ + if not hasattr(self.ctrl, "_valid_project_series_df"): + return + + df = self.ctrl._valid_project_series_df + filtered_df = df[df["solution_type"] == selected_series] + valid_projects = sorted(filtered_df["project"].unique()) + + self.ui.pppProjectCombo.blockSignals(True) + self.ui.pppProjectCombo.clear() + self.ui.pppProjectCombo.addItems(valid_projects) + self.ui.pppProjectCombo.setCurrentIndex(0) + self.ui.pppProjectCombo.blockSignals(False) + + # Update constellations combobox based on new PPP selection + self.update_constellations_for_ppp_selection() + + # If we are on the Constellations tab, trigger BIA fetch for new selection + if self.ui.configTabWidget.currentIndex() == 1: + self.ctrl.constellations_tab.on_config_tab_changed(1) + + def _on_ppp_project_changed(self, selected_project: str): + """ + UI handler: when PPP project changes, filter valid series. + Only displays series that have all required files (SP3, BIA, CLK). + """ + if not hasattr(self.ctrl, "_valid_project_series_df"): + return + + df = self.ctrl._valid_project_series_df + filtered_df = df[df["project"] == selected_project] + valid_series = sorted(filtered_df["solution_type"].unique()) + + # Ensure only series with all required files are displayed + if hasattr(self.ctrl, "_valid_series_for_provider"): + valid_series = [s for s in valid_series if s in self.ctrl._valid_series_for_provider] + + self.ui.pppSeriesCombo.blockSignals(True) + self.ui.pppSeriesCombo.clear() + self.ui.pppSeriesCombo.addItems(valid_series) + self.ui.pppSeriesCombo.setCurrentIndex(0) + self.ui.pppSeriesCombo.blockSignals(False) + + # Update constellations combobox based on new PPP selection + self.update_constellations_for_ppp_selection() + + Logger.workflow(f"✅ Filtered PPP series for project '{selected_project}': {valid_series}") + + # If we are on the Constellations tab, trigger BIA fetch for new selection + if self.ui.configTabWidget.currentIndex() == 1: + self.ctrl.constellations_tab.on_config_tab_changed(1) + + #endregion + + #region CDDIS Analysis Centre Scanning + + def start_analysis_centre_scan(self, start_epoch: datetime, end_epoch: datetime): + """ + Start a background worker to scan the CDDIS archive for valid PPP analysis centres. + + Arguments: + start_epoch (datetime): Start of the observation time window. + end_epoch (datetime): End of the observation time window. + """ + # Clean up any existing analysis centre threads before starting a new one + self._cleanup_analysis_thread() + + self._worker = DownloadWorker(start_epoch=start_epoch, end_epoch=end_epoch, analysis_centers=True) + self._metadata_thread = QThread() + self._worker.moveToThread(self._metadata_thread) + + self._worker.finished.connect(self.on_cddis_ready) + self._worker.finished.connect(self._restore_cursor) + self._worker.cancelled.connect(self._on_cddis_cancelled) + self._worker.cancelled.connect(self._restore_cursor) + self._worker.constellation_info.connect(self._on_constellation_info_received) + + # Connect both finished and cancelled to thread quit + self._worker.finished.connect(self._metadata_thread.quit) + self._worker.cancelled.connect(self._metadata_thread.quit) + self._metadata_thread.finished.connect(self._on_analysis_thread_finished) + self._metadata_thread.started.connect(self._worker.run) + self._metadata_thread.start() + + def on_cddis_ready(self, data: pd.DataFrame, log_messages: bool = True): + """ + UI handler: receive PPP products DataFrame from worker and populate provider/project/series combos. + + Arguments: + data (pd.DataFrame): Products dataframe from CDDIS scan. + log_messages (bool): Whether to log success messages (False when clearing). + """ + self.ctrl.products_df = data + + if data.empty: + self.ctrl.valid_analysis_centers = [] + self.ui.pppProviderCombo.clear() + self.ui.pppProviderCombo.addItem("None") + self.ui.pppSeriesCombo.clear() + self.ui.pppSeriesCombo.addItem("None") + return + + self.ctrl.valid_analysis_centers = list(get_valid_analysis_centers(self.ctrl.products_df)) + + if len(self.ctrl.valid_analysis_centers) == 0: + self.ui.pppProviderCombo.clear() + self.ui.pppProviderCombo.addItem("None") + self.ui.pppSeriesCombo.clear() + self.ui.pppSeriesCombo.addItem("None") + return + + self.ui.pppProviderCombo.blockSignals(True) + self.ui.pppProviderCombo.clear() + self.ui.pppProviderCombo.addItems(self.ctrl.valid_analysis_centers) + self.ui.pppProviderCombo.setCurrentIndex(0) + + # Update PPP series based on default PPP provider + self.ui.pppProviderCombo.blockSignals(False) + self.ctrl.try_enable_process_button() + self._on_ppp_provider_changed(self.ctrl.valid_analysis_centers[0]) + if log_messages: + Logger.workflow( + f"✅ CDDIS archive scan complete. Found PPP product providers: {', '.join(self.ctrl.valid_analysis_centers)}") + show_toast(self.ctrl.parent, f"✅ Found {len(self.ctrl.valid_analysis_centers)} PPP provider(s)", duration=3000) + + def _on_cddis_cancelled(self): + """ + UI handler: handle cancellation of CDDIS worker. + """ + Logger.workflow("📦 PPP provider scan was cancelled") + + def _on_cddis_error(self, msg): + """ + UI handler: report CDDIS worker error to the UI. + + Arguments: + msg (str): Error message from the worker. + """ + Logger.workflow(f"Error loading CDDIS data: {msg}") + self.ui.pppProviderCombo.clear() + self.ui.pppProviderCombo.addItem("None") + self.ctrl.parent.setCursor(Qt.CursorShape.ArrowCursor) + show_toast(self.ctrl.parent, "⚠️ Failed to scan CDDIS archive", duration=4000) + + def _restore_cursor(self): + """ + Restore the cursor to normal arrow after background operation completes. + """ + self.ctrl.parent.setCursor(Qt.CursorShape.ArrowCursor) + + def _on_constellation_info_received(self, provider_constellations: dict): + """ + UI handler: receive and store constellation information for each PPP provider/series/project. + This is emitted by the DownloadWorker after fetching the SP3 headers. + + Arguments: + provider_constellations (dict): Nested dictionary mapping "provider -> series -> project -> constellations" + e.g., { + 'COD': { + 'FIN': {'OPS': {'GPS', 'GLO', 'GAL'}, 'MGX': {'GPS', 'GLO', 'GAL', 'BDS', 'QZS'}}, + 'RAP': {'OPS': {'GPS', 'GLO', 'GAL'}} + }, ... + } + """ + # Store for later use when filtering constellations UI based on selected provider/series/project + self.ctrl.provider_constellations = provider_constellations + + Logger.console("📡 Provider constellation information received") + + # Update constellations combobox based on current PPP selection + self.update_constellations_for_ppp_selection() + + # If already on Constellations tab, trigger BIA fetch + if self.ui.configTabWidget.currentIndex() == 1: + self.ctrl.constellations_tab.on_config_tab_changed(1) + + def _cleanup_analysis_thread(self): + """ + Request any running analysis centre threads to cancel. + Moves the thread to _pending_threads list so it isn't destroyed while running. + """ + if self._worker is not None: + self._worker.stop() + + if self._metadata_thread is not None: + if self._metadata_thread.isRunning(): + # Disconnect old signals to prevent callbacks to stale state + try: + self._worker.finished.disconnect() + self._worker.cancelled.disconnect() + except (TypeError, RuntimeError): + pass # Already disconnected or object deleted + try: + self._worker.constellation_info.disconnect() + self._worker.progress.disconnect() + except (TypeError, RuntimeError): + pass # Already disconnected or object deleted + + # Keep reference alive until thread actually finishes + old_thread = self._metadata_thread + + def cleanup_old_thread(): + if old_thread in self._pending_threads: + self._pending_threads.remove(old_thread) + + old_thread.finished.connect(cleanup_old_thread) + self._pending_threads.append(old_thread) + + # Clear current references so new thread can be created + self._worker = None + self._metadata_thread = None + + def _on_analysis_thread_finished(self): + """ + Slot called when the analysis thread has fully finished. + Safe to clean up references here. + """ + if self._metadata_thread is not None: + if not self._metadata_thread.isRunning(): + self._worker = None + self._metadata_thread = None + + # Also clean any finished pending threads + self._pending_threads = [t for t in self._pending_threads if t.isRunning()] + + #endregion + + #region Thread Management + + def stop_all_workers(self): + """ + Best-effort stop for all background workers managed by this controller. + """ + try: + if self._worker is not None: + self._worker.stop() + except Exception: + pass + + try: + self._stop_sinex_worker() + except Exception: + pass + + #endregion + + #region Reset to Defaults + + def reset_to_defaults(self): + """ + Reset all General config tab fields to their default/placeholder states. + """ + # Mode combo + self.ui.modeCombo.clear() + self.ui.modeCombo.addItem("Select one") + self.ui.modeCombo.setCurrentIndex(0) + + # Constellations combo + self.ui.constellationsCombo.clear() + self.ui.constellationsCombo.setEditable(True) + self.ui.constellationsCombo.lineEdit().clear() + self.ui.constellationsCombo.lineEdit().setPlaceholderText("Select one or more") + self.ui.constellationsValue.setText("Constellations") + if hasattr(self.ui.constellationsCombo, '_constellation_model'): + delattr(self.ui.constellationsCombo, '_constellation_model') + if hasattr(self.ui.constellationsCombo, '_constellation_on_item_changed'): + delattr(self.ui.constellationsCombo, '_constellation_on_item_changed') + + # Time window + self.ui.timeWindowButton.setText("Start / End") + self.ui.timeWindowValue.setText("Time Window") + + # Data interval + self.ui.dataIntervalButton.setText("Interval (Seconds)") + self.ui.dataIntervalValue.setText("Data interval") + + # Receiver type + self.ui.receiverTypeCombo.clear() + self.ui.receiverTypeCombo.addItem("Import text") + self.ui.receiverTypeCombo.setCurrentIndex(0) + if self.ui.receiverTypeCombo.lineEdit(): + self.ui.receiverTypeCombo.lineEdit().setText("Import text") + self.ui.receiverTypeValue.setText("Receiver Type") + + # Antenna type + self.ui.antennaTypeCombo.clear() + self.ui.antennaTypeCombo.addItem("Import text") + self.ui.antennaTypeCombo.setCurrentIndex(0) + if self.ui.antennaTypeCombo.lineEdit(): + self.ui.antennaTypeCombo.lineEdit().setText("Import text") + self.ui.antennaTypeValue.setText("") + + # Antenna offset + self.ui.antennaOffsetButton.setText("0.0, 0.0, 0.0") + self.ui.antennaOffsetValue.setText("0.0, 0.0, 0.0") + + # Apriori position + self.ui.aprioriPositionButton.setText("0.0, 0.0, 0.0") + + # PPP combos + self.ui.pppProviderCombo.clear() + self.ui.pppProviderCombo.addItem("Select one") + self.ui.pppProviderCombo.setCurrentIndex(0) + self.ui.pppSeriesCombo.clear() + self.ui.pppSeriesCombo.addItem("Select one") + self.ui.pppSeriesCombo.setCurrentIndex(0) + self.ui.pppProjectCombo.clear() + self.ui.pppProjectCombo.addItem("Select one") + self.ui.pppProjectCombo.setCurrentIndex(0) + + #endregion + + #region Combo Plumbing Helpers + + def _bind_combo(self, combo: QComboBox, items_func: Callable[[], List[str]]): + """ + Bind a single-choice combo to dynamically populate items on open and keep the UI clean. + + Arguments: + combo (QComboBox): Target combo box to bind. + items_func (Callable[[], list[str]]): Function returning the items list. + """ + combo._old_showPopup = combo.showPopup + + def new_showPopup(): + combo.clear() + combo.setEditable(True) + combo.lineEdit().setAlignment(Qt.AlignCenter) + for item in items_func(): + combo.addItem(item) + combo.setEditable(False) + combo._old_showPopup() + + combo.showPopup = new_showPopup + + def _bind_multiselect_combo(self, combo: QComboBox, items_func: Callable[[], List[str]], mirror_label, placeholder: str): + """ + Bind a multi-select combo using checkable items and mirror checked labels as comma-separated text. + + Arguments: + combo (QComboBox): Target combo box. + items_func (Callable[[], list[str]]): Function returning the items list. + mirror_label (QLabel): Label where checked values are mirrored. + placeholder (str): Placeholder text when no item is checked. + """ + combo.setEditable(True) + combo.lineEdit().setReadOnly(True) + combo.lineEdit().setPlaceholderText(placeholder) + combo.setInsertPolicy(QComboBox.NoInsert) + + combo._old_showPopup = combo.showPopup + + def show_popup(): + model = QStandardItemModel(combo) + for txt in items_func(): + it = QStandardItem(txt) + it.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) + it.setData(Qt.Unchecked, Qt.CheckStateRole) + model.appendRow(it) + + def on_item_changed(_item: QStandardItem): + selected = [ + model.item(r).text() + for r in range(model.rowCount()) + if model.item(r).checkState() == Qt.Checked + ] + text = ", ".join(selected) if selected else placeholder + combo.lineEdit().setText(text) + mirror_label.setText(text) + + model.itemChanged.connect(on_item_changed) + combo.setModel(model) + combo._old_showPopup() + + combo.showPopup = show_popup + combo.clear() + combo.lineEdit().clear() + combo.lineEdit().setPlaceholderText(placeholder) + + #endregion \ No newline at end of file diff --git a/scripts/GinanUI/app/controllers/input_controller.py b/scripts/GinanUI/app/controllers/input_controller.py index 62f3e6118..c19ce6ed8 100644 --- a/scripts/GinanUI/app/controllers/input_controller.py +++ b/scripts/GinanUI/app/controllers/input_controller.py @@ -1,10 +1,20 @@ -# app/controllers/input_controller.py """ -UI input flow controller for the Ginan-UI. +Top-level UI input controller for the Ginan-UI. + +This is the parent controller that owns the top-level action buttons +(Observations, Output, Show Config, Process, Stop All, CDDIS Credentials, +User Manual, Reset Config) and coordinates three tab-specific sub-controllers: + + - GeneralConfigController - General config tab + - ConstellationConfigController - Constellations config tab + - OutputConfigController - Output config tab + - YAMLConfigController - YAML config tab + +It also holds shared state (rnx_file, output_dir, products_df, execution) +and the ExtractedInputs dataclass used by the Execution model. """ -from __future__ import annotations -import math +from __future__ import annotations import os import re import subprocess @@ -12,56 +22,44 @@ from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Callable, List -from decimal import Decimal, InvalidOperation - +from typing import List import pandas as pd - -from scripts.GinanUI.app.utils.logger import Logger - -from scripts.GinanUI.app.models.dl_products import ( - get_valid_analysis_centers, - get_valid_series_for_provider, - get_valid_providers_with_series, - str_to_datetime -) -from PySide6.QtCore import QObject, Signal, Qt, QDateTime, QThread -from PySide6.QtGui import QStandardItemModel, QStandardItem, QColor, QBrush +from PySide6.QtCore import QObject, Qt, Signal from PySide6.QtWidgets import ( - QFileDialog, QDialog, - QFormLayout, - QDoubleSpinBox, - QSpinBox, - QHBoxLayout, - QVBoxLayout, - QDateTimeEdit, - QInputDialog, - QMessageBox, - QComboBox, + QFileDialog, + QLabel, QLineEdit, + QMessageBox, QPushButton, - QLabel, - QListWidget, - QListWidgetItem, - QAbstractItemView, - QSizePolicy, - QWidget + QVBoxLayout, ) - -from scripts.GinanUI.app.models.execution import Execution, GENERATED_YAML, INPUT_PRODUCTS_PATH -from scripts.GinanUI.app.utils.common_dirs import USER_MANUAL_PATH +from scripts.GinanUI.app.models.archive_manager import archive_old_outputs, archive_products_if_rinex_changed +from scripts.GinanUI.app.models.execution import GENERATED_YAML, INPUT_PRODUCTS_PATH, Execution from scripts.GinanUI.app.models.rinex_extractor import RinexExtractor +from scripts.GinanUI.app.controllers.constellation_config_controller import ConstellationConfigController +from scripts.GinanUI.app.controllers.general_config_controller import GeneralConfigController +from scripts.GinanUI.app.controllers.output_config_controller import OutputConfigController +from scripts.GinanUI.app.controllers.yaml_config_controller import YAMLConfigController from scripts.GinanUI.app.utils.cddis_credentials import save_earthdata_credentials -from scripts.GinanUI.app.models.archive_manager import (archive_products_if_rinex_changed) -from scripts.GinanUI.app.models.archive_manager import archive_old_outputs -from scripts.GinanUI.app.utils.workers import DownloadWorker, BiasProductWorker, SinexValidationWorker +from scripts.GinanUI.app.utils.common_dirs import USER_MANUAL_PATH +from scripts.GinanUI.app.utils.logger import Logger from scripts.GinanUI.app.utils.toast import show_toast - class InputController(QObject): """ - UI controller class InputController. + Parent UI controller that coordinates file selection, configuration, + and processing workflows across the Ginan-UI input panel. + + Delegates detailed responsibilities to three tab-specific sub-controllers: + - self.general_tab (GeneralConfigController) + - self.constellations_tab (ConstellationConfigController) + - self.output_tab (OutputConfigController) + - self.yaml_tab (YAMLConfigController) + + Signals: + ready(str, str): Emitted when both RINEX path and output directory are set. + pea_ready(): Emitted when PEA processing should start. """ ready = Signal(str, str) # rnx_path, output_path @@ -69,11 +67,11 @@ class InputController(QObject): def __init__(self, ui, parent_window, execution: Execution): """ - UI handler: init. + Initialise the top-level input controller and its sub-controllers. Arguments: - ui (Any): Main window UI instance (generated from Qt .ui). - parent_window (Any): Parent widget/window to anchor dialogs. + ui: Main window UI instance (generated from Qt .ui). + parent_window: Parent widget/window to anchor dialogs. execution (Execution): Backend execution bridge used to read/apply UI config. """ super().__init__() @@ -81,102 +79,59 @@ def __init__(self, ui, parent_window, execution: Execution): self.parent = parent_window self.execution = execution + # Shared state self.rnx_file: Path = None self.output_dir: Path = None self.products_df: pd.DataFrame = pd.DataFrame() # CDDIS replaces with a populated dataframe - - # Config file path self.config_path = GENERATED_YAML - ### Wire: file selection buttons ### - self.ui.observationsButton.clicked.connect(self.load_rnx_file) - self.ui.outputButton.clicked.connect(self.load_output_dir) - - # Initial states - self.ui.outputButton.setEnabled(False) - self.ui.showConfigButton.setEnabled(False) - self.ui.processButton.setEnabled(False) - self.ui.stopAllButton.setEnabled(False) - - ### Bind: configuration drop-downs / UIs ### + # Time window (set during on_run_pea, used by MainWindow for downloads) + self.start_time: datetime = None + self.end_time: datetime = None - self._bind_combo(self.ui.modeCombo, self._get_mode_items) + # Track the last loaded RINEX path (for change detection / archiving) + self.last_rinex_path: Path = None - # PPP provider, project and series - self.ui.pppProviderCombo.currentTextChanged.connect(self._on_ppp_provider_changed) - self.ui.pppProjectCombo.currentTextChanged.connect(self._on_ppp_project_changed) - self.ui.pppSeriesCombo.currentTextChanged.connect(self._on_ppp_series_changed) - - # Constellations - self._bind_multiselect_combo( - self.ui.constellationsCombo, - self._get_constellations_items, - self.ui.constellationsValue, - placeholder="Select one or more", - ) - - # Receiver/Antenna types: free-text input - self._enable_free_text_for_receiver_and_antenna() + # BIA code priorities cache: provider -> series -> project -> {'GPS': set(), ...} + self.bia_code_priorities = {} - # Antenna offset - self.ui.antennaOffsetButton.clicked.connect(self._open_antenna_offset_dialog) - self.ui.antennaOffsetButton.setCursor(Qt.CursorShape.PointingHandCursor) - self.ui.antennaOffsetValue.setText("0.0, 0.0, 0.0") + # SINEX validation result + self._sinex_filename = None # Stored until apply_ui_config() is called - # Apriori position - self.ui.aprioriPositionButton.clicked.connect(self._open_apriori_position_dialog) - self.ui.aprioriPositionButton.setCursor(Qt.CursorShape.PointingHandCursor) + # Valid analysis centres from CDDIS scan + self.valid_analysis_centers = [] - # Time window and data interval - self.ui.timeWindowButton.clicked.connect(self._open_time_window_dialog) - self.ui.timeWindowButton.setCursor(Qt.CursorShape.PointingHandCursor) - self.ui.dataIntervalButton.clicked.connect(self._open_data_interval_dialog) - self.ui.dataIntervalButton.setCursor(Qt.CursorShape.PointingHandCursor) + # Instantiate sub-controllers (one per config tab) + self.general_tab = GeneralConfigController(ui, self) + self.constellations_tab = ConstellationConfigController(ui, self) + self.output_tab = OutputConfigController(ui, self) + self.yaml_tab = YAMLConfigController(ui, self) - # Run buttons - self.ui.showConfigButton.clicked.connect(self.on_show_config) - self.ui.showConfigButton.setCursor(Qt.CursorShape.PointingHandCursor) + # Top-level button wiring + self.ui.observationsButton.clicked.connect(self.load_rnx_file) + self.ui.outputButton.clicked.connect(self.load_output_dir) self.ui.processButton.clicked.connect(self.on_run_pea) - - # CDDIS credentials dialog self.ui.cddisCredentialsButton.clicked.connect(self._open_cddis_credentials_dialog) - - # Reset config button - self.ui.resetConfigButton.clicked.connect(self._on_reset_config_clicked) - - # User manual button self.ui.userManualButton.clicked.connect(self._open_user_manual) - self.setup_tooltips() - - # Initialise "Constellations" placeholder - self._setup_constellation_placeholder() - self._hide_all_constellation_widgets() - - # Track threads that are pending cleanup (threads that are cancelled but not yet finished) - self._pending_threads = [] + # Initial button states + self.ui.outputButton.setEnabled(False) + self.ui.processButton.setEnabled(False) + self.ui.stopAllButton.setEnabled(False) - # BIA code priorities cache: provider -> series -> project -> {'GPS': set(), ...} - self.bia_code_priorities = {} - self._bia_loading = False - self._bia_worker = None - self._bia_thread = None - - # SINEX validation worker tracking - self._sinex_worker = None - self._sinex_thread = None - self._sinex_path = None - self._sinex_filename = None # Stored until apply_ui_config() is called + self._setup_top_level_tooltips() + self.general_tab.setup_tooltips() + self.constellations_tab.setup_tooltips() + self.output_tab.setup_tooltips() + self.yaml_tab.setup_tooltips() - # Connect tab change signal to trigger BIA fetch when switching to Constellations tab - self.ui.configTabWidget.currentChanged.connect(self._on_config_tab_changed) + #region UI Tooltips - def setup_tooltips(self): + def _setup_top_level_tooltips(self): """ - UI handler: setup tooltips and visual style for key controls. + Set up tooltips and visual style for top-level action buttons. + Sub-controller tooltips are set up in their own setup_tooltips() methods. """ - - # Consistent tooltip style for all elements tooltip_style = """ QToolTip { background-color: #2c5d7c; @@ -188,213 +143,43 @@ def setup_tooltips(self): } """ - # Apply to parent window self.parent.setStyleSheet(self.parent.styleSheet() + tooltip_style) - # Add tooltip styling to buttons without changing their appearance - # Just append the tooltip style to their existing styles + for btn in [self.ui.observationsButton, self.ui.outputButton, + self.ui.processButton, self.ui.stopAllButton, + self.ui.cddisCredentialsButton]: + btn.setStyleSheet(btn.styleSheet() + tooltip_style) - # Get current styles and append tooltip styling - obs_style = self.ui.observationsButton.styleSheet() + tooltip_style - out_style = self.ui.outputButton.styleSheet() + tooltip_style - proc_style = self.ui.processButton.styleSheet() + tooltip_style - stop_style = self.ui.stopAllButton.styleSheet() + tooltip_style - cddis_style = self.ui.cddisCredentialsButton.styleSheet() + tooltip_style - - self.ui.observationsButton.setStyleSheet(obs_style) - self.ui.outputButton.setStyleSheet(out_style) - self.ui.processButton.setStyleSheet(proc_style) - self.ui.stopAllButton.setStyleSheet(stop_style) - self.ui.cddisCredentialsButton.setStyleSheet(cddis_style) - - # File selection buttons self.ui.observationsButton.setToolTip( "Select a RINEX observation file (.rnx or .rnx.gz).\n" "This will automatically extract metadata and populate the UI fields." ) - self.ui.outputButton.setToolTip( "Choose the directory where processing results will be saved.\n" - "Existing .POS or .GPX output in this directory will be saved in the archived subdirectory." + "Existing .POS, .GPX, .TRACE, and .SNX output in this directory will be saved in the archived subdirectory." ) - self.ui.processButton.setToolTip( "Start the Ginan (PEA) PPP processing using the configured parameters.\n" "Ensure all required fields are filled before processing." ) - self.ui.stopAllButton.setToolTip( "Stop the Ginan (PEA) PPP processing.\n" "Will terminate all download threads and unlock the UI again." ) - - # Configuration buttons - self.ui.showConfigButton.setToolTip( - "Generate and open the YAML configuration file.\n" - "You can review and modify advanced settings before processing.\n" - "Note: UI defined parameters will ALWAYS override manual config edits." - ) - - self.ui.resetConfigButton.setToolTip( - "Delete and regenerate the YAML configuration file and start from a clean slate.\n" - "Note: Will delete all modifications to the existing file!" - ) - self.ui.userManualButton.setToolTip( "Open the Ginan-UI User Manual\n" "Located in docs/USER_MANUAL.md" ) - self.ui.cddisCredentialsButton.setToolTip( "Set your NASA Earthdata credentials for downloading PPP products\n" "Required for accessing the CDDIS archive data" ) - # Input fields and combos - self.ui.modeCombo.setToolTip( - "Processing mode:\n" - "• Static: For stationary receivers\n" - "• Kinematic: For moving receivers\n" - "• Dynamic: For high-dynamic applications" - ) - - self.ui.constellationsCombo.setToolTip( - "Select which GNSS constellations to use:\n" - "GPS, Galileo (GAL), GLONASS (GLO), BeiDou (BDS), QZSS (QZS)\n" - "More constellations generally improve accuracy" - ) - - self.ui.pppProviderCombo.setToolTip( - "Analysis centre that provides PPP products\n" - "Options populated based on your observation time window" - ) - - self.ui.pppProjectCombo.setToolTip( - "PPP product project type.\n" - "Different projects types offer varying GNSS constellation PPP products." - ) - - self.ui.pppSeriesCombo.setToolTip( - "PPP product series:\n" - "• ULT: Ultra-rapid (lower latency)\n" - "• RAP: Rapid \n" - "• FIN: Final (highest accuracy)" - ) - - # Receiver/Antenna fields - self.ui.receiverTypeCombo.setToolTip( - "Receiver model extracted from RINEX header\n" - "Click to manually edit if needed" - ) - - self.ui.antennaTypeCombo.setToolTip( - "Antenna model extracted from RINEX header\n" - "Must match entries in the ANTEX (.atx) calibration file\n" - "Click to manually edit if needed" - ) - - # Time and offset buttons - self.ui.timeWindowButton.setToolTip( - "Observation time window extracted from RINEX file\n" - "Click to adjust start and end times for processing" - ) - - self.ui.dataIntervalButton.setToolTip( - "Data sampling interval in seconds\n" - "Click to change the processing interval" - ) - - self.ui.antennaOffsetButton.setToolTip( - "Antenna reference point offset in metres (East, North, Up)\n" - "Typically extracted from RINEX header\n" - "Click to modify if needed" - ) - - self.ui.aprioriPositionButton.setToolTip( - "Approximate receiver position in ECEF coordinates (X, Y, Z) in metres\n" - "Typically extracted from RINEX header\n" - "Click to modify if needed" - ) - - # Value display labels - self.ui.receiverTypeValue.setToolTip("Receiver type from RINEX header") - self.ui.antennaTypeValue.setToolTip("Antenna type from RINEX header") - self.ui.constellationsValue.setToolTip("Available constellations in RINEX data") - self.ui.timeWindowValue.setToolTip("Observation time span") - self.ui.dataIntervalValue.setToolTip("Data sampling interval") - self.ui.antennaOffsetValue.setToolTip("Antenna offset: East, North, Up (metres)") - - # Observation code list widget tooltips - if hasattr(self.ui, 'gpsListWidget'): - self.ui.gpsListWidget.setToolTip( - "GPS observation codes\n" - "✓ Check / uncheck to enable / disable codes\n" - "↕ Drag and drop to set priority order (top = highest priority)" - ) - if hasattr(self.ui, 'galListWidget'): - self.ui.galListWidget.setToolTip( - "Galileo observation codes\n" - "✓ Check / uncheck to enable / disable codes\n" - "↕ Drag and drop to set priority order (top = highest priority)" - ) - if hasattr(self.ui, 'gloListWidget'): - self.ui.gloListWidget.setToolTip( - "GLONASS observation codes\n" - "✓ Check / uncheck to enable / disable codes\n" - "↕ Drag and drop to set priority order (top = highest priority)" - ) - if hasattr(self.ui, 'bdsListWidget'): - self.ui.bdsListWidget.setToolTip( - "BeiDou observation codes\n" - "✓ Check / uncheck to enable / disable codes\n" - "↕ Drag and drop to set priority order (top = highest priority)" - ) - if hasattr(self.ui, 'qzsListWidget'): - self.ui.qzsListWidget.setToolTip( - "QZSS observation codes\n" - "✓ Check / uncheck to enable / disable codes\n" - "↕ Drag and drop to set priority order (top = highest priority)" - ) - - self.ui.posCheckbox.setToolTip( - "Enable / disable Ginan (PEA) PPP Processing outputting a Positioning Solution (.POS) file" - ) - - self.ui.gpxCheckbox.setToolTip( - "Enable / disable Ginan (PEA) PPP Processing outputting a GPS Exchange Format (.GPX) file" - ) - - self.ui.traceCheckbox.setToolTip( - "Enable / disable Ginan (PEA) PPP Processing outputting a trace log (.TRACE) file" - ) - - def _hide_all_constellation_widgets(self): - """ - Hide all constellation labels and list widgets on startup. - They will be shown when a RINEX file is loaded and constellations are selected. - """ - widget_names = [ - 'gpsLabel', 'gpsListWidget', - 'galLabel', 'galListWidget', - 'gloLabel', 'gloListWidget', - 'bdsLabel', 'bdsListWidget', - 'qzsLabel', 'qzsListWidget', - ] - - for widget_name in widget_names: - if hasattr(self.ui, widget_name): - widget = getattr(self.ui, widget_name) - widget.setVisible(False) + #endregion - def _open_cddis_credentials_dialog(self): - """ - UI handler: open the CDDIS credentials dialog for Earthdata login. - """ - dialog = CredentialsDialog(self.parent) - dialog.exec() + #region File Selection - # region File Selection + Metadata Extraction + PPP product selection - def load_rnx_file(self) -> ExtractedInputs | None: + def load_rnx_file(self): """ UI handler: choose a RINEX file, extract metadata, update UI, and start PPP products query. """ @@ -405,25 +190,25 @@ def load_rnx_file(self) -> ExtractedInputs | None: current_rinex_path = Path(path).resolve() archive_products_if_rinex_changed( current_rinex=current_rinex_path, - last_rinex=getattr(self, "last_rinex_path", None), + last_rinex=self.last_rinex_path, products_dir=INPUT_PRODUCTS_PATH ) + # Disable until new providers found - if current_rinex_path != getattr(self, "last_rinex_path", None): + if current_rinex_path != self.last_rinex_path: self.ui.processButton.setEnabled(False) self.ui.stopAllButton.setEnabled(False) - self._on_cddis_ready(pd.DataFrame(), False) # Clears providers until worker completes + self.general_tab.on_cddis_ready(pd.DataFrame(), False) # Clears providers until worker completes # Stop any running BIA worker before clearing cache - self._stop_bia_worker() - # Clear BIA code priorities cache when RINEX file changes + self.constellations_tab.stop_bia_worker() self.bia_code_priorities = {} - self._reset_constellation_list_styling() + self.constellations_tab.reset_list_styling() self.last_rinex_path = current_rinex_path self.rnx_file = str(current_rinex_path) - Logger.terminal(f"📄 RINEX file selected: {self.rnx_file}") + Logger.workflow(f"📄 RINEX file selected: {self.rnx_file}") try: extractor = RinexExtractor(self.rnx_file) @@ -431,12 +216,12 @@ def load_rnx_file(self) -> ExtractedInputs | None: # Verify antenna_type against .atx file if not self.parent.atx_required_for_rnx_extraction: - Logger.terminal( + Logger.workflow( "⚠️ ANTEX (.atx) file not installed yet. Antenna type verification will be skipped.") else: - self.verify_antenna_type(result) + self.general_tab.verify_antenna_type(result) - Logger.terminal("🔍 Scanning CDDIS archive for PPP products. Please wait...") + Logger.workflow("🔍 Scanning CDDIS archive for PPP products. Please wait...") # Show toast notification show_toast(self.parent, "🔍 Scanning CDDIS archive for PPP products...", duration=15000) @@ -444,72 +229,25 @@ def load_rnx_file(self) -> ExtractedInputs | None: # Show waiting cursor during CDDIS scan self.parent.setCursor(Qt.CursorShape.WaitCursor) - # Retrieve valid analysis centers + # Start CDDIS scan in background + from scripts.GinanUI.app.models.dl_products import str_to_datetime start_epoch = str_to_datetime(result['start_epoch']) end_epoch = str_to_datetime(result['end_epoch']) + self.general_tab.start_analysis_centre_scan(start_epoch, end_epoch) - # Clean up any existing analysis centre threads before starting a new one - self._cleanup_analysis_thread() - - self.worker = DownloadWorker(start_epoch=start_epoch, end_epoch=end_epoch, analysis_centers=True) - self.metadata_thread = QThread() - self.worker.moveToThread(self.metadata_thread) - - self.worker.finished.connect(self._on_cddis_ready) - self.worker.finished.connect(self._restore_cursor) - self.worker.cancelled.connect(self._on_cddis_cancelled) - self.worker.cancelled.connect(self._restore_cursor) - self.worker.constellation_info.connect(self._on_constellation_info_received) - - # Connect both finished and cancelled to thread quit - self.worker.finished.connect(self.metadata_thread.quit) - self.worker.cancelled.connect(self.metadata_thread.quit) - self.metadata_thread.finished.connect(self._on_analysis_thread_finished) - self.metadata_thread.started.connect(self.worker.run) - self.metadata_thread.start() - - # Populate extracted metadata immediately - self.ui.constellationsValue.setText(result["constellations"]) - self.ui.timeWindowValue.setText(f"{result['start_epoch']} to {result['end_epoch']}") - self.ui.timeWindowButton.setText(f"{result['start_epoch']} to {result['end_epoch']}") - self.ui.dataIntervalButton.setText(f"{result['epoch_interval']} s") - self.rinex_epoch_interval = float(result['epoch_interval']) - self.ui.receiverTypeValue.setText(result["receiver_type"]) - self.ui.antennaTypeValue.setText(result["antenna_type"]) - self.ui.antennaOffsetValue.setText(", ".join(map(str, result["antenna_offset"]))) - self.ui.antennaOffsetButton.setText(", ".join(map(str, result["antenna_offset"]))) - - # Populate apriori position if available - apriori = result.get("apriori_position") - if apriori and any(v != 0.0 for v in apriori): - self.ui.aprioriPositionButton.setText(", ".join(map(str, apriori))) - else: - self.ui.aprioriPositionButton.setText("0.0, 0.0, 0.0") - - self.ui.receiverTypeCombo.clear() - self.ui.receiverTypeCombo.addItem(result["receiver_type"]) - self.ui.receiverTypeCombo.setCurrentIndex(0) - self.ui.receiverTypeCombo.lineEdit().setText(result["receiver_type"]) + # Populate extracted metadata into the config panel immediately + self.general_tab.populate_from_rinex(result) - self.ui.antennaTypeCombo.clear() - self.ui.antennaTypeCombo.addItem(result["antenna_type"]) - self.ui.antennaTypeCombo.setCurrentIndex(0) - self.ui.antennaTypeCombo.lineEdit().setText(result["antenna_type"]) - - self._update_constellations_multiselect(result["constellations"]) - - # Populate observation code combos if available - self._populate_observation_code_combos(result) + # Populate observation code list widgets + self.constellations_tab.populate_observation_codes(result) self.ui.outputButton.setEnabled(True) self.ui.showConfigButton.setEnabled(True) - Logger.terminal("⚒️ RINEX file metadata extracted and applied to UI fields") - self.ui.outputButton.setEnabled(True) - self.ui.showConfigButton.setEnabled(True) + Logger.workflow("⚒️ RINEX file metadata extracted and applied to UI fields") # Start SINEX validation in background - self._start_sinex_validation( + self.general_tab.start_sinex_validation( target_date=start_epoch, marker_name=result.get("marker_name", ""), receiver_type=result.get("receiver_type", ""), @@ -518,8 +256,11 @@ def load_rnx_file(self) -> ExtractedInputs | None: apriori_position=result.get("apriori_position"), ) + # Store marker number (DOMES number) for loading BLQ generation + self._marker_number = result.get("marker_number") + except Exception as e: - Logger.terminal(f"Error extracting RNX metadata: {e}") + Logger.workflow(f"Error extracting RNX metadata: {e}") return None # Always update MainWindow's state @@ -530,1693 +271,195 @@ def load_rnx_file(self) -> ExtractedInputs | None: return result - def _populate_observation_code_combos(self, result: dict): + def load_output_dir(self): """ - Populate the observation code list widgets with available codes from RINEX. - - Arguments: - result (dict): Dictionary containing observation code lists for each constellation + UI handler: choose the output directory and (if RNX is set) emit ready. """ - # Map constellation names to list widgets and result keys - list_widget_mapping = { - 'GPS': ('obs_types_gps', 'enabled_gps', 'gpsListWidget'), - 'GAL': ('obs_types_gal', 'enabled_gal', 'galListWidget'), - 'GLO': ('obs_types_glo', 'enabled_glo', 'gloListWidget'), - 'BDS': ('obs_types_bds', 'enabled_bds', 'bdsListWidget'), - 'QZS': ('obs_types_qzs', 'enabled_qzs', 'qzsListWidget') - } - - populated_constellations = [] - - for const_name, (result_key, enabled_key, widget_name) in list_widget_mapping.items(): - if not hasattr(self.ui, widget_name): - continue - - list_widget = getattr(self.ui, widget_name) - codes = result.get(result_key, []) - enabled_codes = result.get(enabled_key, set()) - - if codes and len(codes) > 0: - self._setup_observation_code_list_widget(list_widget, codes, enabled_codes) - populated_constellations.append(const_name) - else: - # Clear and disable list widget if no codes available - list_widget.clear() - list_widget.setEnabled(False) - - # Log summary message - if populated_constellations: - Logger.terminal(f"✅ Populated observation codes for {', '.join(populated_constellations)}") - else: - Logger.terminal("⚠️ No observation codes found in RINEX") + path = self._select_output_dir(self.parent) + if not path: + return - def _setup_observation_code_list_widget(self, list_widget: QListWidget, codes: List[str], enabled_codes: set): - """ - Set up a list widget with drag-drop reordering and checkboxes for observation codes. + # Ensure output_dir is a Path object + self.output_dir = Path(path).resolve() + Logger.workflow(f"📂 Output directory selected: {self.output_dir}") - Arguments: - list_widget (QListWidget): The list widget to set up - codes (List[str]): List of observation codes to populate (in priority order) - enabled_codes (set): Set of codes that should be checked by default - """ - list_widget.setEnabled(True) - list_widget.clear() - - # Enable drag and drop for reordering - list_widget.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) - list_widget.setDefaultDropAction(Qt.DropAction.MoveAction) - list_widget.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) - - # Add items with checkboxes - for code in codes: - item = QListWidgetItem(code) - item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled) - - # Check if this code is in the enabled set (from template priorities) - if code in enabled_codes: - item.setCheckState(Qt.CheckState.Checked) # Priority codes: checked - else: - item.setCheckState(Qt.CheckState.Unchecked) # Extra codes: unchecked + # Archive existing/old outputs + visual_dir = self.output_dir / "visual" + archive_old_outputs(self.output_dir, visual_dir) - list_widget.addItem(item) + # Enable process button + # MainWindow owns when to enable processButton. This controller exposes a helper if needed. + self.try_enable_process_button() - def verify_antenna_type(self, result: List[str]): - """ - UI handler: verify that the RINEX antenna_type exists in the selected ANTEX (.atx) file. - """ - # Verify antenna_type is present within the .atx file - # Return warning if not - atx_path = self.get_best_atx_path() - - with open(atx_path, "r") as file: - for line in file: - label = line[60:].strip() - - # Read and find antenna_type tag - if label == "TYPE / SERIAL NO" and line[20:24].strip() == "": - valid_antenna_type = line[0:20] - - if len(valid_antenna_type.strip()) < 16 or not valid_antenna_type[16:].strip(): - # Just the antenna part is included, need to add radome (cover) - antenna_part = valid_antenna_type[:15].strip() - valid_antenna_type = f"{antenna_part:<15} NONE" - - # Do same normalisation for result["antenna_type"] - result_antenna = result["antenna_type"] - - if len(result_antenna.strip()) < 16 or ( - len(result_antenna) > 16 and not result_antenna[16:].strip()): - antenna_part = result_antenna[:15].strip() - result_antenna = f"{antenna_part:<15} NONE" - - # Compare strings - if result_antenna.strip() == valid_antenna_type.strip(): - Logger.terminal("✅ Antenna type verified from .atx file") - return - - # Not found! Return warning to user - QMessageBox.warning( - None, - "Provided Antenna Type Invalid", - f'Provided antenna type in .rnx file: "{result["antenna_type"]}"\n' - f'not found in .atx file: "{atx_path}"' - ) - Logger.terminal(f"⚠️ Antenna type failed to verify from .atx file: {atx_path}") - return + # Always update MainWindow's state + self.parent.output_dir = self.output_dir - def get_best_atx_path(self): - """ - Select the best available ANTEX (.atx) file with a priority order. - """ - # Find all .atx files present and prioritise the newest ones - # Return filepath string to best .atx file - atx_files = list(INPUT_PRODUCTS_PATH.glob("*.atx")) - if len(atx_files) == 0: - raise FileNotFoundError("No .atx file found") - elif len(atx_files) > 1: - # Priority order: igs20 > igs14 > igs13 > igs08 > igs05 > any other .atx file - priority_order = ['igs20.atx', 'igs14.atx', 'igs13.atx', 'igs08.atx', 'igs05.atx'] - atx_path = None - for best_atx in priority_order: - matching_files = [f for f in atx_files if f.name == best_atx] - if matching_files: - atx_path = matching_files[0] - Logger.terminal(f"📁 Selected .atx file: {atx_path.name} based on priority") - break - - # If none of the preferred files found, use the first available - if atx_path is None: - atx_path = atx_files[0] - Logger.terminal(f"📁 Selected .atx file: {atx_path.name} based on fallback") - else: - atx_path = atx_files[0] - return atx_path + if self.rnx_file: + self.ready.emit(str(self.rnx_file), str(self.output_dir)) - def _update_constellations_multiselect(self, constellation_str: str): - """ - Populate and mirror a multi-select constellation combo with checkboxes. + #endregion - Arguments: - constellation_str (str): Comma-separated constellations (e.g., "GPS, GAL, GLO"). + #region PEA Processing + def on_run_pea(self): """ - from PySide6.QtGui import QStandardItemModel, QStandardItem - - constellations = [c.strip() for c in constellation_str.split(",") if c.strip()] - combo = self.ui.constellationsCombo - - # Remove previous bindings - if hasattr(combo, '_old_showPopup'): - delattr(combo, '_old_showPopup') - - combo.clear() - combo.setEditable(True) - combo.lineEdit().setReadOnly(True) - combo.setInsertPolicy(QComboBox.NoInsert) - - # Build the item model - model = QStandardItemModel(combo) - for txt in constellations: - item = QStandardItem(txt) - item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) - item.setCheckState(Qt.Checked) - model.appendRow(item) - - def on_item_changed(_item): - selected = [ - model.item(i).text() - for i in range(model.rowCount()) - if model.item(i).checkState() == Qt.Checked - ] - label = ", ".join(selected) if selected else "Select one or more" - combo.lineEdit().setText(label) - self.ui.constellationsValue.setText(label) - self._sync_constellation_list_widgets_to_selection() - - model.itemChanged.connect(on_item_changed) - combo.setModel(model) - combo.setCurrentIndex(-1) - - # Custom showPopup function to keep things reset - def show_popup_constellation(): - if combo.model() != model: - combo.setModel(model) - combo.setCurrentIndex(-1) - QComboBox.showPopup(combo) - - combo.showPopup = show_popup_constellation - - # Store for access and event consistency - combo._constellation_model = model - combo._constellation_on_item_changed = on_item_changed - - # Set initial label text - combo.lineEdit().setText(", ".join(constellations)) - self.ui.constellationsValue.setText(", ".join(constellations)) - - # Initial sync of list widgets - self._sync_constellation_list_widgets_to_selection() - - def _sync_constellation_list_widgets_to_selection(self): - """ - Show / hide constellation list widgets and labels based on the "General" tab's - constellation multi-select. Called when constellation selection changes. - Shows a placeholder message when no constellations are selected. - """ - # Get currently selected constellations from the General tab combo - selected_constellations = set() - combo = self.ui.constellationsCombo - if hasattr(combo, '_constellation_model') and combo._constellation_model: - model = combo._constellation_model - for i in range(model.rowCount()): - if model.item(i).checkState() == Qt.Checked: - selected_constellations.add(model.item(i).text().upper()) - - # Map constellation names to their UI widgets - widget_mapping = { - 'GPS': ('gpsLabel', 'gpsListWidget'), - 'GAL': ('galLabel', 'galListWidget'), - 'GLO': ('gloLabel', 'gloListWidget'), - 'BDS': ('bdsLabel', 'bdsListWidget'), - 'QZS': ('qzsLabel', 'qzsListWidget'), - } - - for const_name, (label_name, list_widget_name) in widget_mapping.items(): - is_enabled = const_name in selected_constellations - - # Show / hide label - if hasattr(self.ui, label_name): - label = getattr(self.ui, label_name) - label.setVisible(is_enabled) - - # Show / hide list widget - if hasattr(self.ui, list_widget_name): - list_widget = getattr(self.ui, list_widget_name) - list_widget.setVisible(is_enabled) - - # Show / hide placeholder message - self._update_constellation_placeholder(len(selected_constellations) == 0) - - def _setup_constellation_placeholder(self): - """ - Create a placeholder label for the Constellations tab that shows when - no constellations are selected or no RINEX file is loaded. + UI handler: validate time window and config, apply UI, then emit pea_ready. + If YAML overwrite is disabled, skip applying UI values to the config file + but still store loading BLQ parameters for ensure_loading_blq(). """ - # Create the placeholder label - self._constellation_placeholder = QLabel( - "No constellations available!\n\n" - "Load a RINEX observation file and select constellations\n" - "in the General tab to configure observation codes" - ) - self._constellation_placeholder.setAlignment(Qt.AlignCenter) - self._constellation_placeholder.setWordWrap(True) - self._constellation_placeholder.setMinimumWidth(250) - self._constellation_placeholder.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self._constellation_placeholder.setStyleSheet( - "color: #bfbfbf; font-size: 13pt; margin: 15px;" - ) + raw = self.ui.timeWindowValue.text() - # Add to the constellations tab layout - if hasattr(self.ui, 'constellationsGridLayout'): - self.ui.constellationsGridLayout.addWidget( - self._constellation_placeholder, 0, 0, 10, 1, Qt.AlignCenter + try: + start_str, end_str = raw.split("to") + start_time = datetime.strptime(start_str.strip(), "%Y-%m-%d_%H:%M:%S") + end_time = datetime.strptime(end_str.strip(), "%Y-%m-%d_%H:%M:%S") + except ValueError: + QMessageBox.warning( + None, + "Format error", + "Time window must be in the format:\n" + "YYYY-MM-DD_HH:MM:SS to YYYY-MM-DD_HH:MM:SS" ) - - # Initially visible - self._constellation_placeholder.setVisible(True) - - # Create explanation label for the Constellations tab - self._constellation_explanation_label = QLabel( - "Select observation codes and set priorities for each active constellation below.
" - "These observation codes are extracted from the loaded RINEX file.
" - "Red strikethrough = missing from .BIA file" - ) - self._constellation_explanation_label.setTextFormat(Qt.RichText) - self._constellation_explanation_label.setWordWrap(True) - self._constellation_explanation_label.setStyleSheet( - "color: #bfbfbf; font-size: 11pt; font-style: italic; margin-bottom: 6x; line-height: 1.4;" - ) - self._constellation_explanation_label.setVisible(False) - - # Create BIA warning label (shown when BIA fetch fails) - self._bia_warning_label = QLabel( - "⚠️ Failed to fetch BIA file for selected PPP products - unable to validate codes" - ) - self._bia_warning_label.setWordWrap(True) - self._bia_warning_label.setStyleSheet( - "QLabel { background-color: #8B4513; color: white; padding: 6px 12px; " - "border-radius: 4px; font: 10pt 'Segoe UI'; }" - ) - self._bia_warning_label.setAlignment(Qt.AlignCenter) - self._bia_warning_label.setVisible(False) - - # Create BIA loading label - self._bia_loading_label = QLabel("⏳ Loading code priorities from .BIA file...") - self._bia_loading_label.setWordWrap(True) - self._bia_loading_label.setStyleSheet( - "QLabel { background-color: #2c5d7c; color: white; padding: 8px 16px; " - "border-radius: 4px; font: 12pt 'Segoe UI'; }" - ) - self._bia_loading_label.setAlignment(Qt.AlignCenter) - self._bia_loading_label.setVisible(False) - - # Create a container widget with vertical layout for the status labels - self._constellation_status_container = QWidget() - status_layout = QVBoxLayout(self._constellation_status_container) - status_layout.setContentsMargins(0, 0, 0, 8) - status_layout.setSpacing(4) - status_layout.addWidget(self._constellation_explanation_label) - status_layout.addWidget(self._bia_warning_label) - status_layout.addWidget(self._bia_loading_label) - - # Add the status container to row 0 of the constellations grid layout - # (existing widgets start at row 1, so row 0 is available) - if hasattr(self.ui, 'constellationsGridLayout'): - self.ui.constellationsGridLayout.addWidget(self._constellation_status_container, 0, 0) - - def _update_constellation_placeholder(self, show_placeholder: bool): - """ - Show or hide the constellation placeholder message. - - Arguments: - show_placeholder (bool): True to show placeholder, False to hide it. - """ - if hasattr(self, '_constellation_placeholder'): - self._constellation_placeholder.setVisible(show_placeholder) - # Show explanation label when placeholder is hidden (i.e., constellations are visible) - if hasattr(self, '_constellation_explanation_label'): - self._constellation_explanation_label.setVisible(not show_placeholder) - - def _on_cddis_ready(self, data: pd.DataFrame, log_messages: bool = True): - """ - UI handler: receive PPP products DataFrame from worker and populate provider/project/series combos. - """ - self.products_df = data - - if data.empty: - self.valid_analysis_centers = [] - self.ui.pppProviderCombo.clear() - self.ui.pppProviderCombo.addItem("None") - self.ui.pppSeriesCombo.clear() - self.ui.pppSeriesCombo.addItem("None") return - self.valid_analysis_centers = list(get_valid_analysis_centers(self.products_df)) - - if len(self.valid_analysis_centers) == 0: - self.ui.pppProviderCombo.clear() - self.ui.pppProviderCombo.addItem("None") - self.ui.pppSeriesCombo.clear() - self.ui.pppSeriesCombo.addItem("None") + if start_time > end_time: + QMessageBox.warning(None, "Time error", "Start time cannot be later than end time.") return - self.ui.pppProviderCombo.blockSignals(True) - self.ui.pppProviderCombo.clear() - self.ui.pppProviderCombo.addItems(self.valid_analysis_centers) - self.ui.pppProviderCombo.setCurrentIndex(0) - - # Update PPP series based on default PPP provider - self.ui.pppProviderCombo.blockSignals(False) - self.try_enable_process_button() - self._on_ppp_provider_changed(self.valid_analysis_centers[0]) - if log_messages: - Logger.terminal( - f"✅ CDDIS archive scan complete. Found PPP product providers: {', '.join(self.valid_analysis_centers)}") - # Show success toast - show_toast(self.parent, f"✅ Found {len(self.valid_analysis_centers)} PPP provider(s)", duration=3000) - - def _on_cddis_error(self, msg): - """ - UI handler: report CDDIS worker error to the UI. - """ - Logger.terminal(f"Error loading CDDIS data: {msg}") - self.ui.pppProviderCombo.clear() - self.ui.pppProviderCombo.addItem("None") - # Restore cursor in case of error - self.parent.setCursor(Qt.CursorShape.ArrowCursor) - # Show error toast - show_toast(self.parent, "⚠️ Failed to scan CDDIS archive", duration=4000) - - def _restore_cursor(self): - """ - Restore the cursor to normal arrow after background operation completes. - """ - self.parent.setCursor(Qt.CursorShape.ArrowCursor) - - def _cleanup_analysis_thread(self): - """ - Request any running analysis centre threads to cancel - Moves the thread to _pending_threads list so it isn't destroyed while running. - """ - if hasattr(self, 'worker') and self.worker is not None: - self.worker.stop() - - if hasattr(self, 'metadata_thread') and self.metadata_thread is not None: - if self.metadata_thread.isRunning(): - # Disconnect old signals to prevent callbacks to stale state - try: - self.worker.finished.disconnect() - self.worker.cancelled.disconnect() - except (TypeError, RuntimeError): - pass # Already disconnected or object deleted - - # Keep reference alive until thread actually finishes - old_thread = self.metadata_thread - - def cleanup_old_thread(): - if old_thread in self._pending_threads: - self._pending_threads.remove(old_thread) - - old_thread.finished.connect(cleanup_old_thread) - self._pending_threads.append(old_thread) - - # Clear current references so new thread can be created - self.worker = None - self.metadata_thread = None - - def _on_cddis_cancelled(self): - """ - UI handler: handle cancellation of CDDIS worker. - """ - Logger.terminal("📦 PPP provider scan was cancelled") - - def _on_constellation_info_received(self, provider_constellations: dict): - """ - UI handler: receive and store constellation information for each PPP provider/series/project - This is emitted by the DownloadWorker after fetching the SP3 headers - - Arguments: - provider_constellations (dict): Nested dictionary mapping "provider -> series -> project -> constellations" - e.g., { - 'COD': { - 'FIN': {'OPS': {'GPS', 'GLO', 'GAL'}, 'MGX': {'GPS', 'GLO', 'GAL', 'BDS', 'QZS'}}, - 'RAP': {'OPS': {'GPS', 'GLO', 'GAL'}} - }, ... - } - """ - # Store for later use when filtering constellations UI based on selected provider/series/project - self.provider_constellations = provider_constellations + if not getattr(self, "config_path", None): + QMessageBox.warning( + None, + "No config file", + "Please click Show config and select a YAML file first." + ) + return - # Log the received constellation info - Logger.console("📡 Provider constellation information received") + self.start_time = start_time + self.end_time = end_time - # Update constellations combobox based on current PPP selection - self._update_constellations_for_ppp_selection() + yaml_overwrite = self.yaml_tab.get_yaml_toggles() - # If already on Constellations tab, trigger BIA fetch - if self.ui.configTabWidget.currentIndex() == 1: - self._on_config_tab_changed(1) + # Warn the user when YAML overwrite is disabled so they are aware + # that UI changes will not be applied to the config file + if not yaml_overwrite: + reply = QMessageBox.warning( + self.parent, + "YAML Overwrite Disabled", + "YAML config overwrite is disabled. The config file on disk will be " + "used as-is, and any UI changes (RINEX file, constellations, output " + "settings, etc.) will NOT be applied.\n\n" + "Are you sure you want to continue?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + if reply != QMessageBox.StandardButton.Yes: + return - def _update_constellations_for_ppp_selection(self): - """ - Update the constellations combobox to enable / disable items based on the - currently selected PPP provider/series/project combination. - Constellations supported by the selected combination are enabled and checked, - unsupported constellations are disabled and unchecked. - """ - combo = self.ui.constellationsCombo - if not hasattr(combo, '_constellation_model') or combo._constellation_model is None: - return + try: + self.execution.reload_config() - model = combo._constellation_model - - # Get current PPP selection - provider = self.ui.pppProviderCombo.currentText() - series = self.ui.pppSeriesCombo.currentText() - project = self.ui.pppProjectCombo.currentText() - - # Get available constellations for this combination - available_constellations = set() - if hasattr(self, 'provider_constellations') and self.provider_constellations: - try: - available_constellations = self.provider_constellations.get(provider, {}).get(series, {}).get(project, set()) - except (KeyError, AttributeError): - available_constellations = set() - - # If no constellation info available, enable all (fallback behaviour) - if not available_constellations: - for i in range(model.rowCount()): - item = model.item(i) - item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) + if yaml_overwrite: + inputs = self.extract_ui_values(self.rnx_file) + self.execution.apply_ui_config(inputs) + self.execution.write_cached_changes() + else: + # Still need to store loading BLQ parameters even when not overwriting, + # so that ensure_loading_blq() can generate BLQ files if needed + self.execution.set_loading_params( + marker_name=self.extract_marker_name(self.rnx_file), + marker_number=getattr(self, '_marker_number', None), + apriori_position=self.parse_apriori_position(self.ui.aprioriPositionButton.text()), + ) + self.execution.yaml_overwrite = False + except Exception as e: + Logger.workflow(f"⚠️ Failed to apply config: {e}") return - # Block signals to prevent triggering on_item_changed multiple times - model.blockSignals(True) - - # Update each constellation item - for i in range(model.rowCount()): - item = model.item(i) - constellation_name = item.text().upper() + self.pea_ready.emit() - if constellation_name in available_constellations: - # Enable and check this constellation - #item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) # Un-comment to also disable checkability - item.setCheckState(Qt.Checked) - else: - # Disable and uncheck this constellation - #item.setFlags(Qt.ItemIsUserCheckable) # Un-comment to also disable checkability - item.setCheckState(Qt.Unchecked) - - model.blockSignals(False) - - # Update the label text to show only enabled/checked constellations - selected = [ - model.item(i).text() - for i in range(model.rowCount()) - if model.item(i).checkState() == Qt.Checked - ] - label = ", ".join(selected) if selected else "Select one or more" - combo.lineEdit().setText(label) - self.ui.constellationsValue.setText(label) - - # Sync the constellation list widgets - self._sync_constellation_list_widgets_to_selection() - - def _on_config_tab_changed(self, index: int): + def _reset_ui_to_defaults(self): """ - UI handler: triggered when the config tab widget changes tabs. - When switching to the Constellations tab (index 1), fetch .BIA code priorities - for the current PPP selection if not already cached - - Arguments: - index (int): The index of the newly selected tab + Reset all UI fields to their default/initial states. + This is the "start from scratch" reset that clears all user inputs. """ - # Constellations tab is index 1 - if index != 1: - return - - # Check if we have valid PPP selection - provider = self.ui.pppProviderCombo.currentText() - series = self.ui.pppSeriesCombo.currentText() - project = self.ui.pppProjectCombo.currentText() + self.rnx_file = None + self.output_dir = None + self.products_df = pd.DataFrame() + self.last_rinex_path = None + self.valid_analysis_centers = [] + self.start_time = None + self.end_time = None + for attr in ['_valid_project_series_df', '_valid_series_for_provider']: + if hasattr(self, attr): + delattr(self, attr) - # Guard: Skip if any combo is empty or has placeholder values - if not provider or not series or not project: - return - if provider in ("", "None", "Select one") or series in ("", "None", "Select one") or project in ("", "None", "Select one"): - return + self._marker_number = None - # Guard: Skip if products_df is empty (happens during RINEX file change) - if self.products_df.empty: - return + self.parent.rnx_file = None + self.parent.output_dir = None - # Check if we already have cached BIA data for this combination - if self._is_bia_cached(provider, series, project): - # Already have the data, just validate - self._validate_constellation_codes_against_bia() - return + # Delegate to each tab controller + self.general_tab.reset_to_defaults() + self.constellations_tab.reset_to_defaults() + self.output_tab.reset_to_defaults() + self.yaml_tab.reset_to_defaults() - # Check if we are already loading the same combination - if self._bia_loading: - # Check if it is for a different combination - if so, restart - if (hasattr(self, '_bia_current_provider') and - (self._bia_current_provider != provider or - self._bia_current_series != series or - self._bia_current_project != project)): - # Different combination requested, stop current and start new - Logger.console(f"🔄 BIA fetch interrupted - switching to {provider}/{series}/{project}") - else: - # Same combination, let it continue - return + # Reset button states + self.ui.outputButton.setEnabled(False) + self.ui.showConfigButton.setEnabled(False) + self.ui.processButton.setEnabled(False) + self.ui.stopAllButton.setEnabled(False) + self.ui.observationsButton.setEnabled(True) + self.ui.cddisCredentialsButton.setEnabled(True) - # Start BIA fetch (will stop any existing worker first) - self._fetch_bia_code_priorities(provider, series, project) + # Reset visualisation panel + if hasattr(self.parent, 'visCtrl'): + self.parent.visCtrl.set_html_files([]) + if hasattr(self.ui, 'webEngineView'): + self.ui.webEngineView.setHtml("") - def _is_bia_cached(self, provider: str, series: str, project: str) -> bool: - """ - Check if BIA code priorities are cached for the given combination. - - Arguments: - provider (str) - series (str) - project (str) - - Returns: - bool: True if cached, False otherwise - """ - try: - return (provider in self.bia_code_priorities and - series in self.bia_code_priorities[provider] and - project in self.bia_code_priorities[provider][series]) - except (KeyError, TypeError): - return False - - def _fetch_bia_code_priorities(self, provider: str, series: str, project: str): - """ - Start background worker to fetch and parse BIA file for code priorities. - - Arguments: - provider (str) - series (str) - project (str) - """ - # Safety guard: don't start worker with invalid parameters - if not provider or not series or not project: - Logger.console(f"⚠️ BIA fetch skipped: invalid parameters provider='{provider}' series='{series}' project='{project}'") - return - if provider in ("", "None", "Select one") or series in ("", "None", "Select one") or project in ("", "None", "Select one"): - Logger.console(f"⚠️ BIA fetch skipped: placeholder values in parameters") - return - if self.products_df.empty: - Logger.console(f"⚠️ BIA fetch skipped: products_df is empty") - return - - # Stop any existing BIAProductWorker before starting a new one - self._stop_bia_worker() - - self._bia_loading = True - - # Show loading indicator - self._show_bia_loading_indicator(True) - - # Create worker and thread - self._bia_thread = QThread() - self._bia_worker = BiasProductWorker(self.products_df, provider, series, project) - self._bia_worker.moveToThread(self._bia_thread) - - # Connect signals - self._bia_thread.started.connect(self._bia_worker.run) - self._bia_worker.finished.connect(self._on_bia_finished) - self._bia_worker.error.connect(self._on_bia_error) - self._bia_worker.progress.connect(self._on_bia_progress) - self._bia_worker.finished.connect(self._bia_thread.quit) - self._bia_worker.error.connect(self._bia_thread.quit) - self._bia_thread.finished.connect(self._on_bia_thread_finished) - - # Store current selection for when results come back - self._bia_current_provider = provider - self._bia_current_series = series - self._bia_current_project = project - - # Start the thread - self._bia_thread.start() - - def _stop_bia_worker(self): - """ - Stop any running BIA worker and clean up thread resources. - """ - if self._bia_worker is not None: - # Disconnect signals to prevent callbacks after cleanup - try: - self._bia_worker.finished.disconnect() - self._bia_worker.error.disconnect() - self._bia_worker.progress.disconnect() - except (RuntimeError, TypeError): - # Signals may not be connected or already disconnected - pass - # Signal the worker to stop - self._bia_worker.stop() - - if self._bia_thread is not None: - # Disconnect thread signals - try: - self._bia_thread.started.disconnect() - self._bia_thread.finished.disconnect() - except (RuntimeError, TypeError): - pass - - if self._bia_thread.isRunning(): - # Ask thread to quit and wait briefly - self._bia_thread.quit() - # Wait up to 2 seconds for thread to finish - if not self._bia_thread.wait(2000): - # Force terminate if it doesn't stop gracefully - Logger.console("⚠️ BIA thread did not stop gracefully, forcing termination") - self._bia_thread.terminate() - self._bia_thread.wait(1000) - - # Clean up references - self._bia_worker = None - self._bia_thread = None - self._bia_loading = False - - def _on_bia_progress(self, description: str, percent: int): - """ - UI handler: update progress during BIA fetch. - - Arguments: - description (str): Progress description - percent (int): Progress percentage (-1 for indeterminate) - """ - # Update the loading label if it exists - if hasattr(self, '_bia_loading_label') and self._bia_loading_label: - self._bia_loading_label.setText(f"⏳ {description}") - - def _on_bia_finished(self, code_priorities: dict): - """ - UI handler: BIA fetch completed successfully. - - Arguments: - code_priorities (dict): Dictionary mapping constellation names to sets of code priorities - e.g., {'GPS': {'L1C', 'L2W'}, 'GAL': {'L1C', 'L5Q'}, ...} - """ - self._bia_loading = False - self._show_bia_loading_indicator(False) - - # Hide any previous BIA warning since we now have valid data - self._show_bia_warning(False) - - # Cache the results - provider = self._bia_current_provider - series = self._bia_current_series - project = self._bia_current_project - - if provider not in self.bia_code_priorities: - self.bia_code_priorities[provider] = {} - if series not in self.bia_code_priorities[provider]: - self.bia_code_priorities[provider][series] = {} - self.bia_code_priorities[provider][series][project] = code_priorities - - Logger.terminal(f"✅ BIA code priorities cached for {provider}/{series}/{project}") - - # Validate the constellation codes against BIA - self._validate_constellation_codes_against_bia() - - def _on_bia_error(self, error_msg: str): - """ - UI handler: BIA fetch failed. - - Arguments: - error_msg (str): Error message describing the failure - """ - self._bia_loading = False - self._show_bia_loading_indicator(False) - - Logger.console(f"⚠️ BIA fetch error: {error_msg}") - - # Don't show warnings for cancelled fetches (user-initiated) - if "cancelled" in error_msg.lower(): - return - - # Mark all codes as invalid (red strikethrough) - self._mark_all_codes_invalid() - - # Show BIA warning label - self._show_bia_warning(True) - - # Log to terminal (workflow tab) so user is aware BIA validation is unavailable - Logger.terminal(f"⚠️ Failed to fetch BIA file for selected PPP products - unable to validate codes") - - show_toast(self.parent, f"⚠️ Could not fetch BIA data: {error_msg}", duration=3000) - - def _on_bia_thread_finished(self): - """ - Slot called when the BIA thread has fully finished. - Safe to clean up references here. - """ - self._bia_worker = None - self._bia_thread = None - - def _show_bia_loading_indicator(self, show: bool): - """ - Show or hide a loading indicator on the Constellations tab. - - Arguments: - show (bool): True to show, False to hide - """ - if not hasattr(self, '_bia_loading_label') or self._bia_loading_label is None: - return - - # Reset to initial text when showing (in case it was changed by progress updates) - if show: - self._bia_loading_label.setText("⏳ Loading code priorities from .BIA file...") - - self._bia_loading_label.setVisible(show) - - def _validate_constellation_codes_against_bia(self): - """ - Validate the codes in each constellation list widget against the cached BIA codes. - Codes that are NOT in the .BIA file are marked with strikethrough and a different colour. - """ - # Get current PPP selection - provider = self.ui.pppProviderCombo.currentText() - series = self.ui.pppSeriesCombo.currentText() - project = self.ui.pppProjectCombo.currentText() - - # Get cached BIA codes for this selection - bia_codes = None - try: - bia_codes = self.bia_code_priorities.get(provider, {}).get(series, {}).get(project, None) - except (KeyError, TypeError, AttributeError): - pass - - if not bia_codes: - # No BIA data available, reset all items to normal styling - self._reset_constellation_list_styling() - return - - # Map widget names to constellation keys - widget_mapping = { - 'gpsListWidget': 'GPS', - 'galListWidget': 'GAL', - 'gloListWidget': 'GLO', - 'bdsListWidget': 'BDS', - 'qzsListWidget': 'QZS', - } - - # Colours for codes - valid_color = QColor('white') # White for valid - invalid_color = QColor('#FF6B6B') # Red for invalid - - for widget_name, constellation in widget_mapping.items(): - if not hasattr(self.ui, widget_name): - continue - - list_widget = getattr(self.ui, widget_name) - constellation_bia_codes = bia_codes.get(constellation, set()) - - for i in range(list_widget.count()): - item = list_widget.item(i) - if item is None: - continue - - # Get the code text (e.g., "L1C", "L2W") - code = item.text().strip() - - # Get current font - font = item.font() - - if code in constellation_bia_codes: - # Valid code - normal styling - font.setStrikeOut(False) - item.setFont(font) - item.setForeground(QBrush(valid_color)) - else: - # Invalid code - strikethrough + colour - font.setStrikeOut(True) - item.setFont(font) - item.setForeground(QBrush(invalid_color)) - - Logger.terminal(f"✅ Validated constellation codes against BIA for {provider}/{series}/{project}") - - def _reset_constellation_list_styling(self): - """ - Reset all constellation list widget items to normal styling (no strikethrough, white colour). - Called when BIA data is not available. - """ - widget_names = ['gpsListWidget', 'galListWidget', 'gloListWidget', 'bdsListWidget', 'qzsListWidget'] - normal_color = QColor('white') - - for widget_name in widget_names: - if not hasattr(self.ui, widget_name): - continue - - list_widget = getattr(self.ui, widget_name) - - for i in range(list_widget.count()): - item = list_widget.item(i) - if item is None: - continue - - font = item.font() - font.setStrikeOut(False) - item.setFont(font) - item.setForeground(QBrush(normal_color)) - - # Also hide BIA warning when resetting - self._show_bia_warning(False) - - def _mark_all_codes_invalid(self): - """ - Mark all constellation list widget items as invalid (red strikethrough). - Called when BIA file fetch fails. - """ - widget_names = ['gpsListWidget', 'galListWidget', 'gloListWidget', 'bdsListWidget', 'qzsListWidget'] - invalid_color = QColor('#ff6b6b') - - for widget_name in widget_names: - if not hasattr(self.ui, widget_name): - continue - - list_widget = getattr(self.ui, widget_name) - - for i in range(list_widget.count()): - item = list_widget.item(i) - if item is None: - continue - - font = item.font() - font.setStrikeOut(True) - item.setFont(font) - item.setForeground(QBrush(invalid_color)) - - def _show_bia_warning(self, show: bool): - """ - Show or hide the BIA warning label on the Constellations tab. - - Arguments: - show (bool): True to show warning, False to hide it. - """ - if hasattr(self, '_bia_warning_label'): - self._bia_warning_label.setVisible(show) - - #region SINEX Validation - - def _start_sinex_validation(self, target_date: datetime, marker_name: str, receiver_type: str, antenna_type: str, - antenna_offset: list, apriori_position: list = None): - """ - Start SINEX validation in a background thread. - - Arguments: - target_date (datetime): Date for which to download the SINEX file - marker_name (str): 4-character marker name from RINEX - receiver_type (str): Receiver type from RINEX - antenna_type (str): Antenna type from RINEX - antenna_offset (list): Antenna offset [E, N, U] from RINEX - apriori_position (list): Optional apriori position [X, Y, Z] from RINEX - """ - if not marker_name or len(marker_name) < 4: - Logger.terminal("⚠️ Invalid marker name - SINEX validation skipped") - return - - # Stop any existing SINEX worker - self._stop_sinex_worker() - - Logger.terminal(f"📋 Starting SINEX validation for marker '{marker_name[:4]}'...") - - # Create worker and thread - self._sinex_worker = SinexValidationWorker( - target_date=target_date, - marker_name=marker_name[:4], # Use first 4 characters - receiver_type=receiver_type, - antenna_type=antenna_type, - antenna_offset=antenna_offset, - apriori_position=apriori_position, - ) - self._sinex_thread = QThread() - self._sinex_worker.moveToThread(self._sinex_thread) - - # Connect signals - self._sinex_worker.finished.connect(self._on_sinex_validation_finished) - self._sinex_worker.error.connect(self._on_sinex_validation_error) - self._sinex_worker.progress.connect(self._on_sinex_validation_progress) - - self._sinex_thread.started.connect(self._sinex_worker.run) - self._sinex_worker.finished.connect(self._sinex_thread.quit) - self._sinex_worker.error.connect(self._sinex_thread.quit) - self._sinex_thread.finished.connect(self._on_sinex_thread_finished) - - self._sinex_thread.start() - - def _stop_sinex_worker(self): - """ - Stop any running SINEX validation worker and clean up thread resources. - """ - if self._sinex_worker is not None: - # Disconnect signals to prevent callbacks after cleanup - try: - self._sinex_worker.finished.disconnect() - self._sinex_worker.error.disconnect() - self._sinex_worker.progress.disconnect() - except (RuntimeError, TypeError): - pass - # Signal the worker to stop - self._sinex_worker.stop() - - if self._sinex_thread is not None: - # Disconnect thread signals - try: - self._sinex_thread.started.disconnect() - self._sinex_thread.finished.disconnect() - except (RuntimeError, TypeError): - pass - - if self._sinex_thread.isRunning(): - self._sinex_thread.quit() - if not self._sinex_thread.wait(2000): - Logger.console("⚠️ SINEX thread did not stop gracefully, forcing termination") - self._sinex_thread.terminate() - self._sinex_thread.wait(1000) - - # Clean up references - self._sinex_worker = None - self._sinex_thread = None - - def _on_sinex_validation_progress(self, description: str, percent: int): - """ - UI handler: update progress bar during SINEX download. - - Arguments: - description (str): Progress description (filename) - percent (int): Progress percentage (0-100) - """ - # Update the progress bar in the UI (same as product downloads) - if hasattr(self.ui, 'progressBar'): - self.ui.progressBar.setValue(percent) - self.ui.progressBar.setFormat(f"📥 {description}: {percent}%") - - def _on_sinex_validation_finished(self, sinex_path, validation_results: dict): - """ - UI handler: SINEX validation completed. - - Arguments: - sinex_path (Path): Path to the downloaded SINEX file - validation_results (dict): Validation results dictionary - """ - self._sinex_path = sinex_path - - # Store the SINEX filename for later use in apply_ui_config() - # (config writing only happens when apply_ui_config is called) - if sinex_path is not None: - self._sinex_filename = sinex_path.name - - # Reset progress bar - if hasattr(self.ui, 'progressBar'): - self.ui.progressBar.setValue(0) - self.ui.progressBar.setFormat("") - - # Check validation results and show appropriate toast - if 'error' in validation_results: - show_toast(self.parent, f"⚠️ SINEX validation error: {validation_results['error']}", duration=5000) - return - - if not validation_results.get('marker_found', False): - show_toast(self.parent, f"ℹ️ Marker not found in SINEX file", duration=3000) - return - - # Apply SINEX apriori_position to UI if available (SINEX is more accurate than RINEX) - apriori_result = validation_results.get('apriori_position', {}) - sinex_position = apriori_result.get('sinex_value') - if sinex_position is not None and len(sinex_position) == 3: - # Update the UI with SINEX coordinates - position_str = ", ".join(str(v) for v in sinex_position) - self.ui.aprioriPositionButton.setText(position_str) - - # Check if all validations passed - all_valid = True - has_validations = False - for field in ['receiver_type', 'antenna_type', 'antenna_offset', 'apriori_position']: - field_result = validation_results.get(field, {}) - if field_result.get('valid') is True: - has_validations = True - elif field_result.get('valid') is False: - all_valid = False - has_validations = True - - if has_validations: - if all_valid: - show_toast(self.parent, "✅ SINEX validation passed", duration=3000) - else: - show_toast(self.parent, "⚠️ SINEX validation warnings - check terminal", duration=5000) - - def _on_sinex_validation_error(self, error_msg: str): - """ - UI handler: SINEX validation failed. - - Arguments: - error_msg (str): Error message describing the failure - """ - Logger.terminal(f"⚠️ SINEX validation error: {error_msg}") - - # Don't show toast for cancelled operations - if "cancelled" not in error_msg.lower(): - show_toast(self.parent, f"⚠️ SINEX validation failed: {error_msg}", duration=5000) - - def _on_sinex_thread_finished(self): - """ - Slot called when the SINEX thread has fully finished. - Safe to clean up references here. - """ - self._sinex_worker = None - self._sinex_thread = None - - # endregion - - def _on_analysis_thread_finished(self): - """ - Slot called when the analysis thread has fully finished. - Safe to clean up references here. - """ - # Clean up current thread references if it's no longer running - if hasattr(self, 'metadata_thread') and self.metadata_thread is not None: - if not self.metadata_thread.isRunning(): - self.worker = None - self.metadata_thread = None - - # Also clean any finished pending threads - self._pending_threads = [t for t in self._pending_threads if t.isRunning()] - - def _on_ppp_provider_changed(self, provider_name: str): - """ - UI handler: when PPP provider changes, refresh project and series options. - Only shows series that have all required files (SP3, BIA, CLK). - """ - if not provider_name or provider_name.strip() == "": - return - try: - # Get valid series for this provider (only those with all required files) - valid_series = get_valid_series_for_provider(self.products_df, provider_name) - - if not valid_series: - raise ValueError(f"No valid series (with all required files) for provider: {provider_name}") - - # Get DataFrame of valid (project, series) pairs - filter for valid series only - df = self.products_df.loc[ - (self.products_df["analysis_center"] == provider_name) & - (self.products_df["solution_type"].isin(valid_series)), - ["project", "solution_type"]] - - if df.empty: - raise ValueError(f"No valid project–series combinations for provider: {provider_name}") - - # Store for future filtering if needed - self._valid_project_series_df = df - self._valid_series_for_provider = valid_series # Cache valid series - - project_options = sorted(df['project'].unique()) - series_options = sorted(df['solution_type'].unique()) - - # Block signals before clearing and populating to prevent any duplicates in dropdown - self.ui.pppProjectCombo.blockSignals(True) - self.ui.pppSeriesCombo.blockSignals(True) - - self.ui.pppProjectCombo.clear() - self.ui.pppSeriesCombo.clear() - - self.ui.pppProjectCombo.addItems(project_options) - self.ui.pppSeriesCombo.addItems(series_options) - - self.ui.pppProjectCombo.setCurrentIndex(0) - self.ui.pppSeriesCombo.setCurrentIndex(0) - - # Unblock signals now that the population is complete - self.ui.pppProjectCombo.blockSignals(False) - self.ui.pppSeriesCombo.blockSignals(False) - - # Update constellations combobox based on new PPP selection - self._update_constellations_for_ppp_selection() - - # If we're on the Constellations tab, trigger BIA fetch for new selection - if self.ui.configTabWidget.currentIndex() == 1: - self._on_config_tab_changed(1) - - except Exception as e: - self.ui.pppSeriesCombo.clear() - self.ui.pppSeriesCombo.addItem("None") - self.ui.pppProjectCombo.clear() - self.ui.pppProjectCombo.addItem("None") - - def _on_ppp_series_changed(self, selected_series: str): - """ - UI handler: when PPP series changes, filter valid projects. - - Arguments: - selected_series (str): Series code, e.g., 'ULT', 'RAP', 'FIN'. - """ - if not hasattr(self, "_valid_project_series_df"): - return - - df = self._valid_project_series_df - filtered_df = df[df["solution_type"] == selected_series] - valid_projects = sorted(filtered_df["project"].unique()) - - self.ui.pppProjectCombo.blockSignals(True) - self.ui.pppProjectCombo.clear() - self.ui.pppProjectCombo.addItems(valid_projects) - self.ui.pppProjectCombo.setCurrentIndex(0) - self.ui.pppProjectCombo.blockSignals(False) - - # Update constellations combobox based on new PPP selection - self._update_constellations_for_ppp_selection() - - # If we are on the Constellations tab, trigger BIA fetch for new selection - # This may occur if the user is on this tab while PPP products are being fetched - if self.ui.configTabWidget.currentIndex() == 1: - self._on_config_tab_changed(1) - - def _on_ppp_project_changed(self, selected_project: str): - """ - UI handler: when PPP project changes, filter valid series. - Only displays series that have all required files (SP3, BIA, CLK). - """ - if not hasattr(self, "_valid_project_series_df"): - return - - df = self._valid_project_series_df - filtered_df = df[df["project"] == selected_project] - valid_series = sorted(filtered_df["solution_type"].unique()) - - # Ensure only series with all required files are displayed - if hasattr(self, "_valid_series_for_provider"): - valid_series = [s for s in valid_series if s in self._valid_series_for_provider] - - self.ui.pppSeriesCombo.blockSignals(True) - self.ui.pppSeriesCombo.clear() - self.ui.pppSeriesCombo.addItems(valid_series) - self.ui.pppSeriesCombo.setCurrentIndex(0) - self.ui.pppSeriesCombo.blockSignals(False) - - # Update constellations combobox based on new PPP selection - self._update_constellations_for_ppp_selection() - - Logger.terminal(f"✅ Filtered PPP series for project '{selected_project}': {valid_series}") - - # If we are on the Constellations tab, trigger BIA fetch for new selection - # This may occur if the user is on this tab while PPP products are being fetched - if self.ui.configTabWidget.currentIndex() == 1: - self._on_config_tab_changed(1) - - def load_output_dir(self): - """ - UI handler: choose the output directory and (if RNX is set) emit ready. - """ - """Pick an output directory; if RNX is also set, emit ready.""" - path = self._select_output_dir(self.parent) - if not path: - return - - # Ensure output_dir is a Path object - self.output_dir = Path(path).resolve() - Logger.terminal(f"📂 Output directory selected: {self.output_dir}") - - # Archive existing/old outputs - visual_dir = self.output_dir / "visual" - archive_old_outputs(self.output_dir, visual_dir) - - # Enable process button - # MainWindow owns when to enable processButton. This controller exposes a helper if needed. - self.try_enable_process_button() - - # Always update MainWindow's state - self.parent.output_dir = self.output_dir - - if self.rnx_file: - self.ready.emit(str(self.rnx_file), str(self.output_dir)) - - def try_enable_process_button(self): - """ - UI handler: enable the Process button when RNX, output path, and metadata are ready. - """ - if not self.parent.metadata_downloaded: - return - if not self.output_dir: - return - if not self.rnx_file: - return - if len(self._get_ppp_provider_items()) < 1: - return - self.ui.processButton.setEnabled(True) - - # endregion - - # region Multi-Selectors Assigning (A.K.A. Combo Plumbing) - - def _on_select(self, combo: QComboBox, label, title: str, index: int): - """ - UI handler: mirror a single-select combo choice to a label and reset placeholder. - - Arguments: - combo (QComboBox): Source combo box. - label (QLabel): Target label to mirror text. - title (str): Placeholder title to reset in the combo. - index (int): Selected index. - """ - value = combo.itemText(index) - label.setText(value) - - combo.clear() - combo.addItem(title) - - def _bind_combo(self, combo: QComboBox, items_func: Callable[[], List[str]]): - """ - Bind a single-choice combo to dynamically populate items on open and keep the UI clean. - - Arguments: - combo (QComboBox): Target combo box to bind. - items_func (Callable[[], list[str]]): Function returning the items list. - """ - combo._old_showPopup = combo.showPopup - - def new_showPopup(): - combo.clear() - combo.setEditable(True) - combo.lineEdit().setAlignment(Qt.AlignCenter) - for item in items_func(): - combo.addItem(item) - combo.setEditable(False) - combo._old_showPopup() - - combo.showPopup = new_showPopup - - def _bind_multiselect_combo( - self, - combo: QComboBox, - items_func: Callable[[], List[str]], - mirror_label, - placeholder: str, - ): - """ - Bind a multi-select combo using checkable items and mirror checked labels as comma-separated text. - - Arguments: - combo (QComboBox): Target combo box. - items_func (Callable[[], list[str]]): Function returning the items list. - mirror_label (QLabel): Label where checked values are mirrored. - placeholder (str): Placeholder text when no item is checked. - - """ - combo.setEditable(True) - combo.lineEdit().setReadOnly(True) - combo.lineEdit().setPlaceholderText(placeholder) - combo.setInsertPolicy(QComboBox.NoInsert) - - combo._old_showPopup = combo.showPopup - - def show_popup(): - model = QStandardItemModel(combo) - for txt in items_func(): - it = QStandardItem(txt) - it.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) - it.setData(Qt.Unchecked, Qt.CheckStateRole) - model.appendRow(it) - - def on_item_changed(_item: QStandardItem): - # Collect all checked items - selected = [ - model.item(r).text() - for r in range(model.rowCount()) - if model.item(r).checkState() == Qt.Checked - ] - text = ", ".join(selected) if selected else placeholder - combo.lineEdit().setText(text) - mirror_label.setText(text) - - model.itemChanged.connect(on_item_changed) - combo.setModel(model) - combo._old_showPopup() - - combo.showPopup = show_popup - combo.clear() - combo.lineEdit().clear() - combo.lineEdit().setPlaceholderText(placeholder) - - # ========================================================== - - # Receiver / Antenna free text popups - # ========================================================== - def _enable_free_text_for_receiver_and_antenna(self): - """ - Allow users to enter custom receiver/antenna types via popup, mirroring to UI. - """ - self.ui.receiverTypeCombo.setEditable(True) - self.ui.receiverTypeCombo.lineEdit().setReadOnly(True) - self.ui.antennaTypeCombo.setEditable(True) - self.ui.antennaTypeCombo.lineEdit().setReadOnly(True) - - # Receiver type free text - def _ask_receiver_type(): - current_text = self.ui.receiverTypeCombo.currentText().strip() - text, ok = QInputDialog.getText( - self.ui.receiverTypeCombo, - "Receiver Type", - "Enter receiver type:", - text=current_text # prefill with current - ) - if ok and text: - self.ui.receiverTypeCombo.clear() - self.ui.receiverTypeCombo.addItem(text) - self.ui.receiverTypeCombo.lineEdit().setText(text) - self.ui.receiverTypeValue.setText(text) - - self.ui.receiverTypeCombo.showPopup = _ask_receiver_type - - # Antenna type free text - def _ask_antenna_type(): - current_text = self.ui.antennaTypeCombo.currentText().strip() - text, ok = QInputDialog.getText( - self.ui.antennaTypeCombo, - "Antenna Type", - "Enter antenna type:", - text=current_text # prefill with current - ) - if ok and text: - self.ui.antennaTypeCombo.clear() - self.ui.antennaTypeCombo.addItem(text) - self.ui.antennaTypeCombo.lineEdit().setText(text) - self.ui.antennaTypeValue.setText(text) - - self.ui.antennaTypeCombo.showPopup = _ask_antenna_type - - # ========================================================== - # Antenna offset popup - # ========================================================== - def _open_antenna_offset_dialog(self): - """ - UI handler: open antenna offset dialog (E, N, U) with text input fields. - """ - dlg = QDialog(self.ui.antennaOffsetButton) - dlg.setWindowTitle("Antenna Offset") - - # Parse existing "E, N, U" - try: - e0, n0, u0 = [x.strip() for x in self.ui.antennaOffsetValue.text().split(",")] - except Exception: - e0 = n0 = u0 = "0.0" - - form = QFormLayout(dlg) - - edit_e = QLineEdit(str(e0), dlg) - edit_n = QLineEdit(str(n0), dlg) - edit_u = QLineEdit(str(u0), dlg) - - form.addRow("E:", edit_e) - form.addRow("N:", edit_n) - form.addRow("U:", edit_u) - - btn_row = QHBoxLayout() - ok_btn = QPushButton("OK", dlg) - cancel_btn = QPushButton("Cancel", dlg) - btn_row.addWidget(ok_btn) - btn_row.addWidget(cancel_btn) - form.addRow(btn_row) - - ok_btn.clicked.connect(lambda: self._set_antenna_offset(edit_e, edit_n, edit_u, dlg)) - cancel_btn.clicked.connect(dlg.reject) - - dlg.setMinimumWidth(300) - dlg.setFixedHeight(dlg.sizeHint().height()) - - dlg.exec() - - def _set_antenna_offset(self, edit_e, edit_n, edit_u, dlg: QDialog): - """ - UI handler: apply antenna offset values back to UI. - - Arguments: - edit_e (QLineEdit): East (E) input field. - edit_n (QLineEdit): North (N) input field. - edit_u (QLineEdit): Up (U) input field. - dlg (QDialog): Dialog to accept/close. - """ - try: - e = float(edit_e.text().strip()) - n = float(edit_n.text().strip()) - u = float(edit_u.text().strip()) - except ValueError: - QMessageBox.warning(dlg, "Invalid input", "Please enter valid numeric values.") - return - - text = f"{e}, {n}, {u}" - self.ui.antennaOffsetButton.setText(text) - self.ui.antennaOffsetValue.setText(text) - dlg.accept() - - # ========================================================== - # Apriori position popup - # ========================================================== - def _open_apriori_position_dialog(self): - """ - UI handler: open apriori position dialog (X, Y, Z) with text input fields. - """ - dlg = QDialog(self.ui.aprioriPositionButton) - dlg.setWindowTitle("Apriori Position (ECEF)") - - # Parse existing "X, Y, Z" - try: - x0, y0, z0 = [x.strip() for x in self.ui.aprioriPositionButton.text().split(",")] - except Exception: - x0 = y0 = z0 = "0.0" - - form = QFormLayout(dlg) - - edit_x = QLineEdit(str(x0), dlg) - edit_y = QLineEdit(str(y0), dlg) - edit_z = QLineEdit(str(z0), dlg) - - form.addRow("X:", edit_x) - form.addRow("Y:", edit_y) - form.addRow("Z:", edit_z) - - btn_row = QHBoxLayout() - ok_btn = QPushButton("OK", dlg) - cancel_btn = QPushButton("Cancel", dlg) - btn_row.addWidget(ok_btn) - btn_row.addWidget(cancel_btn) - form.addRow(btn_row) - - ok_btn.clicked.connect(lambda: self._set_apriori_position(edit_x, edit_y, edit_z, dlg)) - cancel_btn.clicked.connect(dlg.reject) - - dlg.setMinimumWidth(300) - dlg.setFixedHeight(dlg.sizeHint().height()) - - dlg.exec() - - def _set_apriori_position(self, edit_x, edit_y, edit_z, dlg: QDialog): - """ - UI handler: apply apriori position values back to UI. - - Arguments: - edit_x (QLineEdit): X coordinate input field. - edit_y (QLineEdit): Y coordinate input field. - edit_z (QLineEdit): Z coordinate input field. - dlg (QDialog): Dialog to accept/close. - """ - try: - x = float(edit_x.text().strip()) - y = float(edit_y.text().strip()) - z = float(edit_z.text().strip()) - except ValueError: - QMessageBox.warning(dlg, "Invalid input", "Please enter valid numeric values.") - return - - text = f"{x}, {y}, {z}" - self.ui.aprioriPositionButton.setText(text) - dlg.accept() - - # ========================================================== - # Time window popup - # ========================================================== - def _open_time_window_dialog(self): - """ - UI handler: open dialog to adjust observation start/end times. - """ - dlg = QDialog(self.ui.timeWindowButton) - dlg.setWindowTitle("Time Window") - - # Parse existing "yyyy-MM-dd_HH:mm:ss to yyyy-MM-dd_HH:mm:ss" - current_text = self.ui.timeWindowButton.text() - try: - s_text, e_text = current_text.split(" to ") - s_dt = QDateTime.fromString(s_text, "yyyy-MM-dd_HH:mm:ss") - e_dt = QDateTime.fromString(e_text, "yyyy-MM-dd_HH:mm:ss") - if not s_dt.isValid(): - s_dt = QDateTime.fromString(s_text, "yyyy-MM-dd HH:mm:ss") - if not e_dt.isValid(): - e_dt = QDateTime.fromString(e_text, "yyyy-MM-dd HH:mm:ss") - except Exception: - s_dt = e_dt = QDateTime.currentDateTime() - - form = QFormLayout(dlg) - - start_edit = QDateTimeEdit(s_dt, dlg) - end_edit = QDateTimeEdit(e_dt, dlg) - - start_edit.setCalendarPopup(True) - end_edit.setCalendarPopup(True) - start_edit.setDisplayFormat("yyyy-MM-dd_HH:mm:ss") - end_edit.setDisplayFormat("yyyy-MM-dd_HH:mm:ss") - start_edit.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - end_edit.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - - form.addRow("Start:", start_edit) - form.addRow("End:", end_edit) - - btn_row = QHBoxLayout() - ok_btn = QPushButton("OK", dlg) - cancel_btn = QPushButton("Cancel", dlg) - btn_row.addWidget(ok_btn) - btn_row.addWidget(cancel_btn) - form.addRow(btn_row) - - ok_btn.clicked.connect(lambda: self._set_time_window(start_edit, end_edit, dlg)) - cancel_btn.clicked.connect(dlg.reject) - - dlg.setMinimumWidth(300) - dlg.setFixedHeight(dlg.sizeHint().height()) - - dlg.exec() - - def _set_time_window(self, start_edit, end_edit, dlg: QDialog): - """ - UI handler: validate and set selected time window into UI. - - Arguments: - start_edit (QDateTimeEdit): Start time widget. - end_edit (QDateTimeEdit): End time widget. - dlg (QDialog): Dialog to accept/close. - """ - if end_edit.dateTime() < start_edit.dateTime(): - QMessageBox.warning(dlg, "Time error", - "End time cannot be earlier than start time.\nPlease select again.") - return - - s = start_edit.dateTime().toString("yyyy-MM-dd_HH:mm:ss") - e = end_edit.dateTime().toString("yyyy-MM-dd_HH:mm:ss") - self.ui.timeWindowButton.setText(f"{s} to {e}") - self.ui.timeWindowValue.setText(f"{s} to {e}") - dlg.accept() - - # ========================================================== - # Data interval popup - # ========================================================== - def _open_data_interval_dialog(self): - """ - UI handler: open dialog to adjust data interval (seconds). - """ - dlg = QDialog(self.ui.dataIntervalButton) - dlg.setWindowTitle("Data Interval") - - # Extract current value from button text ("30 s" → 30, "0.50 s" → 0.5) - current_text = self.ui.dataIntervalButton.text().replace(" s", "").strip() - try: - current_val = float(current_text) - except ValueError: - current_val = 1.0 # fallback if parsing fails - - form = QFormLayout(dlg) - - interval_spin = QDoubleSpinBox(dlg) - interval_spin.setRange(0.01, 999999.99) - interval_spin.setDecimals(2) - interval_spin.setValue(current_val) - interval_spin.setSuffix(" s") - interval_spin.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - - form.addRow("Interval:", interval_spin) - - btn_row = QHBoxLayout() - ok_btn = QPushButton("OK", dlg) - cancel_btn = QPushButton("Cancel", dlg) - btn_row.addWidget(ok_btn) - btn_row.addWidget(cancel_btn) - form.addRow(btn_row) - - ok_btn.clicked.connect(lambda: self._set_data_interval(interval_spin, dlg)) - cancel_btn.clicked.connect(dlg.reject) - - dlg.setMinimumWidth(300) - dlg.setFixedHeight(dlg.sizeHint().height()) - - dlg.exec() + if hasattr(self.ui, 'configTabWidget'): + self.ui.configTabWidget.setCurrentIndex(0) - def _set_data_interval(self, interval_spin, dlg: QDialog): + def try_enable_process_button(self): """ - UI handler: apply data interval value back to UI. - - Arguments: - interval_spin (QDoubleSpinBox): Interval spin box. - dlg (QDialog): Dialog to accept/close. + Enable the Process button when RNX, output path, and metadata are ready. """ - val = interval_spin.value() - text = f"{int(val)} s" if val == int(val) else f"{val:.2f} s" - self.ui.dataIntervalButton.setText(text) - self.ui.dataIntervalValue.setText(text) - dlg.accept() - - # endregion - - # region Config and PEA Processing + if not self.parent.metadata_downloaded: + return + if not self.output_dir: + return + if not self.rnx_file: + return + if len(self.valid_analysis_centers) < 1: + return + self.ui.processButton.setEnabled(True) def extract_ui_values(self, rnx_path): """ - Extract current UI values, parse/normalize them, and return as dataclass. + Extract current UI values, parse/normalise them, and return as dataclass. Arguments: rnx_path (str): Selected RINEX observation file path. Returns: ExtractedInputs: Dataclass containing parsed fields and raw strings. - """ - # Extract user input from the UI and assign it to class variables. mode_raw = self.ui.modeCombo.currentText() if self.ui.modeCombo.currentText() != "Select one" else "Static" - - # Get constellations from the actual dropdown selections, not the label - constellations_raw = "" - combo = self.ui.constellationsCombo - if hasattr(combo, '_constellation_model') and combo._constellation_model: - model = combo._constellation_model - selected = [model.item(i).text() for i in range(model.rowCount()) if - model.item(i).checkState() == Qt.Checked] - constellations_raw = ", ".join(selected) - else: - # Fallback to the label text if no custom model exists - constellations_raw = self.ui.constellationsValue.text() - time_window_raw = self.ui.timeWindowValue.text() # Get from button, not value label - epoch_interval_raw = self.ui.dataIntervalButton.text() # Get from button, not value label + constellations_raw = self.general_tab.get_selected_constellations_text() + time_window_raw = self.ui.timeWindowValue.text() + epoch_interval_raw = self.ui.dataIntervalButton.text() receiver_type = self.ui.receiverTypeValue.text() antenna_type = self.ui.antennaTypeValue.text() - antenna_offset_raw = self.ui.antennaOffsetButton.text() # Get from button, not value label - apriori_position_raw = self.ui.aprioriPositionButton.text() # Get from button, not value label + antenna_offset_raw = self.ui.antennaOffsetButton.text() + apriori_position_raw = self.ui.aprioriPositionButton.text() ppp_provider = self.ui.pppProviderCombo.currentText() if self.ui.pppProviderCombo.currentText() != "Select one" else "" ppp_series = self.ui.pppSeriesCombo.currentText() if self.ui.pppSeriesCombo.currentText() != "Select one" else "" ppp_project = self.ui.pppProjectCombo.currentText() if self.ui.pppProjectCombo.currentText() != "Select one" else "" - # Extract observation codes from combos - obs_codes = self._extract_observation_codes() + obs_codes = self.constellations_tab.extract_observation_codes() + gpx_output, pos_output, trace_output_network, snx_output = self.output_tab.get_output_toggles() - # Parsed values start_epoch, end_epoch = self.parse_time_window(time_window_raw) antenna_offset = self.parse_antenna_offset(antenna_offset_raw) apriori_position = self.parse_apriori_position(apriori_position_raw) @@ -2224,12 +467,6 @@ def extract_ui_values(self, rnx_path): marker_name = self.extract_marker_name(rnx_path) mode = self.determine_mode_value(mode_raw) - # Output toggles - gpx_output = self.ui.gpxCheckbox.isChecked() if hasattr(self.ui, "gpxCheckbox") else True - pos_output = self.ui.posCheckbox.isChecked() if hasattr(self.ui, "posCheckbox") else True - trace_output_network = self.ui.traceCheckbox.isChecked() if hasattr(self.ui, "traceCheckbox") else False - - # Returned the values found as a dataclass for easier access return self.ExtractedInputs( marker_name=marker_name, start_epoch=start_epoch, @@ -2255,197 +492,14 @@ def extract_ui_values(self, rnx_path): gpx_output=gpx_output, pos_output=pos_output, trace_output_network=trace_output_network, + snx_output=snx_output, sinex_filename=self._sinex_filename, + marker_number=getattr(self, '_marker_number', None), ) - def _extract_observation_codes(self) -> dict: - """ - Extract selected observation codes from all constellation list widgets in priority order. - - Returns: - dict: Dictionary mapping constellation names to lists of selected codes in order - """ - obs_codes = {} - - list_widget_mapping = { - 'gps': 'gpsListWidget', - 'gal': 'galListWidget', - 'glo': 'gloListWidget', - 'bds': 'bdsListWidget', - 'qzs': 'qzsListWidget' - } - - for const_name, widget_name in list_widget_mapping.items(): - if not hasattr(self.ui, widget_name): - obs_codes[const_name] = [] - continue - - list_widget = getattr(self.ui, widget_name) - - # Extract checked items in their current order (priority order) - selected = [] - for i in range(list_widget.count()): - item = list_widget.item(i) - if item.checkState() == Qt.CheckState.Checked: - selected.append(item.text()) - - obs_codes[const_name] = selected - - return obs_codes - - def on_show_config(self): - """ - UI handler: reload config, apply UI values, write changes, then open the YAML. - """ - Logger.terminal("📄 Opening YAML configuration file...") - # Reload disk version before overwriting with GUI changes - self.execution.reload_config() - inputs = self.extract_ui_values(self.rnx_file) - self.execution.apply_ui_config(inputs) - self.execution.write_cached_changes() - - # Execution class will throw error when instantiated if the file doesn't exist and it can't create it - # This code is run after Execution class is instantiated within this file, thus never will occur - if not os.path.exists(GENERATED_YAML): - QMessageBox.warning( - None, - "File not found", - f"The file {GENERATED_YAML} does not exist." - ) - return - - self.on_open_config_in_editor(self.config_path) - - def on_open_config_in_editor(self, file_path): - """ - Open the config YAML file in the OS default editor/viewer. - - Arguments: - file_path (str): Absolute or relative path to the YAML file. - """ - import subprocess - import platform - - try: - abs_path = os.path.abspath(file_path) - - # Open the file with the appropriate method for the operating system - if platform.system() == "Windows": - os.startfile(abs_path) - return - - if platform.system() == "Darwin": # macOS - subprocess.run(["open", abs_path]) - - else: # Linux and other Unix-like systems - # When compiled with pyinstaller, LD_LIBRARY_PATH is modified which prevents external app opening - env = os.environ.copy() - original = env.get("LD_LIBRARY_PATH_ORIG") - if original: - env["LD_LIBRARY_PATH"] = original # Restore original value - else: - env.pop("LD_LIBRARY_PATH", None) # Clear the value to use sys defaults - subprocess.run(["xdg-open", abs_path], env=env) - - except Exception as e: - error_message = f"Cannot open config file:\n{file_path}\n\nError: {str(e)}" - Logger.terminal(f"Error: {error_message}") - QMessageBox.critical( - None, - "Error Opening File", - error_message - ) - - def on_run_pea(self): - """ - UI handler: validate time window and config, apply UI, then emit pea_ready. - """ - raw = self.ui.timeWindowValue.text() - - # --- Parse time window --- - try: - start_str, end_str = raw.split("to") - start_time = datetime.strptime(start_str.strip(), "%Y-%m-%d_%H:%M:%S") - end_time = datetime.strptime(end_str.strip(), "%Y-%m-%d_%H:%M:%S") - except ValueError: - QMessageBox.warning( - None, - "Format error", - "Time window must be in the format:\n" - "YYYY-MM-DD_HH:MM:SS to YYYY-MM-DD_HH:MM:SS" - ) - return - - if start_time > end_time: - QMessageBox.warning(None, "Time error", "Start time cannot be later than end time.") - return - - if not getattr(self, "config_path", None): - QMessageBox.warning( - None, - "No config file", - "Please click Show config and select a YAML file first." - ) - return - - # Store time window so MainWindow can use it later - self.start_time = start_time - self.end_time = end_time - - # --- Write updated config --- - try: - self.execution.reload_config() - inputs = self.extract_ui_values(self.rnx_file) - self.execution.apply_ui_config(inputs) # config only, no product archiving here - self.execution.write_cached_changes() - except Exception as e: - Logger.terminal(f"⚠️ Failed to apply config: {e}") - return - - # --- Emit signal for MainWindow --- - self.pea_ready.emit() - - def _on_reset_config_clicked(self): - """ - UI handler: reset the configuration file and UI to defaults. - Shows a confirmation dialog before proceeding. - """ - # Show confirmation dialog - reply = QMessageBox.question( - self.parent, - "Reset Configuration", - "This will reset all settings to their defaults.\n\n" - "• The configuration file will be regenerated from the template\n" - "• All UI fields will be cleared\n" - "• You will need to re-select your RINEX file and output directory\n\n" - "Are you sure you want to continue?", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No - ) - - if reply != QMessageBox.StandardButton.Yes: - return - - try: - # Stop any running background workers first - self.stop_all() - - # Reset the config file - self.execution.reset_config() + #endregion - # Reset the UI to defaults - self.reset_ui_to_defaults() - - Logger.terminal("🔄 Configuration and UI reset to defaults") - show_toast(self.parent, "🔄 Configuration and UI reset to defaults", duration=3000) - - except Exception as e: - Logger.terminal(f"⚠️ Failed to reset configuration: {e}") - QMessageBox.critical( - self.parent, - "Reset Failed", - f"Failed to reset configuration:\n{e}" - ) + #region User Manual def _open_user_manual(self): """ @@ -2457,7 +511,7 @@ def _open_user_manual(self): manual_path = USER_MANUAL_PATH if not manual_path.exists(): - Logger.terminal(f"⚠️ User manual not found at: {manual_path}") + Logger.workflow(f"⚠️ User manual not found at: {manual_path}") QMessageBox.warning( self.parent, "User Manual Not Found", @@ -2466,7 +520,7 @@ def _open_user_manual(self): ) return - Logger.terminal(f"📖 Opening user manual: {manual_path}") + Logger.workflow(f"📖 Opening user manual: {manual_path}") # Try to open the file with the default application if os.name == 'nt': # Windows @@ -2483,175 +537,29 @@ def _open_user_manual(self): # Fall back to browser for other platforms webbrowser.open(f'file://{manual_path.absolute()}') - Logger.terminal("✅ User manual opened successfully") - except Exception as e: - Logger.terminal(f"⚠️ Failed to open user manual: {e}") - QMessageBox.critical( - self.parent, - "Error Opening Manual", - f"Failed to open the user manual:\n{e}" - ) + Logger.workflow(f"⚠️ Failed to open user manual: {e}") + QMessageBox.critical(self.parent, "Error Opening Manual", f"Failed to open the user manual:\n{e}") - def reset_ui_to_defaults(self): - """ - Reset all UI fields to their default/initial states. - This is the "start from scratch" reset that clears all user inputs. - """ - # Clear internal state - self.rnx_file = None - self.output_dir = None - self.products_df = pd.DataFrame() - if hasattr(self, 'last_rinex_path'): - delattr(self, 'last_rinex_path') - if hasattr(self, 'valid_analysis_centers'): - self.valid_analysis_centers = [] - if hasattr(self, '_valid_project_series_df'): - delattr(self, '_valid_project_series_df') - if hasattr(self, '_valid_series_for_provider'): - delattr(self, '_valid_series_for_provider') - if hasattr(self, 'start_time'): - delattr(self, 'start_time') - if hasattr(self, 'end_time'): - delattr(self, 'end_time') - - # Reset MainWindow state - self.parent.rnx_file = None - self.parent.output_dir = None - - # Reset General Tab - - # Mode combo - reset to placeholder - self.ui.modeCombo.clear() - self.ui.modeCombo.addItem("Select one") - self.ui.modeCombo.setCurrentIndex(0) - - # Constellations combo - reset to placeholder - self.ui.constellationsCombo.clear() - self.ui.constellationsCombo.setEditable(True) - self.ui.constellationsCombo.lineEdit().clear() - self.ui.constellationsCombo.lineEdit().setPlaceholderText("Select one or more") - self.ui.constellationsValue.setText("Constellations") - # Clear any custom model - if hasattr(self.ui.constellationsCombo, '_constellation_model'): - delattr(self.ui.constellationsCombo, '_constellation_model') - if hasattr(self.ui.constellationsCombo, '_constellation_on_item_changed'): - delattr(self.ui.constellationsCombo, '_constellation_on_item_changed') - - # Time window - reset to placeholder text - self.ui.timeWindowButton.setText("Start / End") - self.ui.timeWindowValue.setText("Time Window") - - # Data interval - reset to placeholder - self.ui.dataIntervalButton.setText("Interval (Seconds)") - self.ui.dataIntervalValue.setText("Data interval") - - # Receiver type - reset to placeholder - self.ui.receiverTypeCombo.clear() - self.ui.receiverTypeCombo.addItem("Import text") - self.ui.receiverTypeCombo.setCurrentIndex(0) - if self.ui.receiverTypeCombo.lineEdit(): - self.ui.receiverTypeCombo.lineEdit().setText("Import text") - self.ui.receiverTypeValue.setText("Receiver Type") - - # Antenna type - reset to placeholder - self.ui.antennaTypeCombo.clear() - self.ui.antennaTypeCombo.addItem("Import text") - self.ui.antennaTypeCombo.setCurrentIndex(0) - if self.ui.antennaTypeCombo.lineEdit(): - self.ui.antennaTypeCombo.lineEdit().setText("Import text") - self.ui.antennaTypeValue.setText("") - - # Antenna offset - reset to default - self.ui.antennaOffsetButton.setText("0.0, 0.0, 0.0") - self.ui.antennaOffsetValue.setText("0.0, 0.0, 0.0") - - # Apriori position - reset to default - self.ui.aprioriPositionButton.setText("0.0, 0.0, 0.0") - - # PPP Provider - reset to placeholder - self.ui.pppProviderCombo.clear() - self.ui.pppProviderCombo.addItem("Select one") - self.ui.pppProviderCombo.setCurrentIndex(0) - - # PPP Series - reset to placeholder - self.ui.pppSeriesCombo.clear() - self.ui.pppSeriesCombo.addItem("Select one") - self.ui.pppSeriesCombo.setCurrentIndex(0) - - # PPP Project - reset to placeholder - self.ui.pppProjectCombo.clear() - self.ui.pppProjectCombo.addItem("Select one") - self.ui.pppProjectCombo.setCurrentIndex(0) - - # Reset Constellations Tab - - # Clear all constellation list widgets - list_widgets = ['gpsListWidget', 'galListWidget', 'gloListWidget', 'bdsListWidget', 'qzsListWidget'] - for widget_name in list_widgets: - if hasattr(self.ui, widget_name): - list_widget = getattr(self.ui, widget_name) - list_widget.clear() - list_widget.setEnabled(False) - - # Hide all constellation widgets and show placeholder - self._hide_all_constellation_widgets() - self._update_constellation_placeholder(True) - - # Reset Output Tab - - # Reset output checkboxes to defaults (POS and GPX true, TRACE false) - if hasattr(self.ui, 'posCheckbox'): - self.ui.posCheckbox.setChecked(True) - if hasattr(self.ui, 'gpxCheckbox'): - self.ui.gpxCheckbox.setChecked(True) - if hasattr(self.ui, 'traceCheckbox'): - self.ui.traceCheckbox.setChecked(False) - - # Reset Button States and Locks - - # Disable buttons that should be locked on startup - self.ui.outputButton.setEnabled(False) - self.ui.showConfigButton.setEnabled(False) - self.ui.processButton.setEnabled(False) - self.ui.stopAllButton.setEnabled(False) - - # Ensure launch buttons are enabled - self.ui.observationsButton.setEnabled(True) - self.ui.cddisCredentialsButton.setEnabled(True) + #endregion - # Reset Visualisation Panel - # Clear the visualisation panel - if hasattr(self.parent, 'visCtrl'): - self.parent.visCtrl.set_html_files([]) - # Clear the web view - if hasattr(self.ui, 'webEngineView'): - self.ui.webEngineView.setHtml("") + # region Thread Management - # Reset config tab to General - # Not really needed since the "Reset Config" button is in General, - # But just in case for the future / aesthetics - if hasattr(self.ui, 'configTabWidget'): - self.ui.configTabWidget.setCurrentIndex(0) + def stop_all(self): + """ + Best-effort stop for all background workers managed by the controller and sub-controllers. + """ + try: + self.general_tab.stop_all_workers() + self.constellations_tab.stop_bia_worker() + if hasattr(self, "parent"): + self.parent.setCursor(Qt.CursorShape.ArrowCursor) + except Exception: + pass # endregion - # region Utility Functions - - @staticmethod - def _set_combobox_by_value(combo: QComboBox, value: str): - """ - Helper: find a value in a combo and set current index if present. - - Arguments: - combo (QComboBox): Target combo box. - value (str): Text to search. - """ - if value is None: - return - idx = combo.findText(value) - if idx != -1: - combo.setCurrentIndex(idx) + #region Static Helpers @staticmethod def _select_rnx_file(parent) -> str: @@ -2659,11 +567,10 @@ def _select_rnx_file(parent) -> str: Open a file dialog to select a RINEX observation file. Arguments: - parent (Any): Parent widget. + parent: Parent widget. Returns: str: Selected file path or empty string. - """ path, _ = QFileDialog.getOpenFileName( parent, @@ -2679,11 +586,10 @@ def _select_output_dir(parent) -> str: Open a directory dialog to select the output folder. Arguments: - parent (Any): Parent widget. + parent: Parent widget. Returns: str: Selected directory path or empty string. - """ path = QFileDialog.getExistingDirectory(parent, "Select Output Directory") return path or "" @@ -2742,7 +648,7 @@ def parse_time_window(time_window_raw: str): time_window_raw (str): e.g., 'YYYY-MM-DD_HH:MM:SS to YYYY-MM-DD_HH:MM:SS'. Returns: - tuple[str, str]: (start_epoch, end_epoch) with underscores preserved for UI. + tuple[str, str]: (start_epoch, end_epoch) with underscores replaced by spaces. Example: >>> parse_time_window("2025-01-01_00:00:00 to 2025-01-02_00:00:00") @@ -2800,10 +706,57 @@ def parse_apriori_position(apriori_position_raw: str): except ValueError: raise ValueError("Invalid apriori position format. Expected: 'x, y, z'") + @staticmethod + def _get_mode_items() -> List[str]: + """ + Provide available processing modes for the UI combo. + + Returns: + list[str]: ['Static', 'Kinematic', 'Dynamic'] + + Example: + >>> InputController._get_mode_items() + ['Static', 'Kinematic', 'Dynamic'] + """ + return ["Static", "Kinematic", "Dynamic"] + + @staticmethod + def _get_constellations_items() -> List[str]: + """ + Provide available GNSS constellations for the UI combo. + + Returns: + list[str]: ['GPS', 'GAL', 'GLO', 'BDS', 'QZS'] + + Example: + >>> InputController._get_constellations_items() + ['GPS', 'GAL', 'GLO', 'BDS', 'QZS'] + """ + return ["GPS", "GAL", "GLO", "BDS", "QZS"] + + @staticmethod + def _get_ppp_series_items() -> List[str]: + """ + Provide available PPP series codes for the UI combo. + + Returns: + list[str]: ['ULT', 'RAP', 'FIN'] + + Example: + >>> InputController._get_ppp_series_items() + ['ULT', 'RAP', 'FIN'] + """ + return ["ULT", "RAP", "FIN"] + + #endregion + + #region ExtractedInputs Dataclass + @dataclass class ExtractedInputs: """ Dataclass container for parsed UI values and raw strings. + Produced by extract_ui_values() and consumed by Execution.apply_ui_config(). """ # Parsed / derived values marker_name: str @@ -2838,86 +791,38 @@ class ExtractedInputs: gpx_output: bool = True pos_output: bool = True trace_output_network: bool = False + snx_output: bool = False sinex_filename: str = None + marker_number: str = None - # endregion - - # region Statics - - @staticmethod - def _get_mode_items() -> List[str]: - """ - Provide available processing modes for the UI combo. - - Returns: - list[str]: ['Static', 'Kinematic', 'Dynamic'] - - Example: - >>> InputController._get_mode_items() - ['Static', 'Kinematic', 'Dynamic'] - """ - return ["Static", "Kinematic", "Dynamic"] - - @staticmethod - def _get_constellations_items() -> List[str]: - """ - Provide available GNSS constellations for the UI combo. - - Arguments: - None - - Returns: - list[str]: ['GPS', 'GAL', 'GLO', 'BDS', 'QZS'] - - Example: - >>> InputController._get_constellations_items() - ['GPS', 'GAL', 'GLO', 'BDS', 'QZS'] - """ - return ["GPS", "GAL", "GLO", "BDS", "QZS"] - - def _get_ppp_provider_items(self) -> List[str]: - """ - Provide available PPP providers from the cached products DataFrame. + #endregion - Returns: - list[str]: Provider names; empty when products list is not yet available. + #region CDDIS Credentials Dialog - Example: - >>> ctrl._get_ppp_provider_items() + def _open_cddis_credentials_dialog(self): """ - if hasattr(self, "valid_analysis_centers") and self.valid_analysis_centers: - return self.valid_analysis_centers - return [] - - @staticmethod - def _get_ppp_series_items() -> List[str]: + UI handler: open the CDDIS credentials dialog for Earthdata login. """ - Provide available PPP series codes for the UI combo. - - Returns: - list[str]: ['ULT', 'RAP', 'FIN'] - - Example: - >>> InputController._get_ppp_series_items() - ['ULT', 'RAP', 'FIN'] - """ - return ["ULT", "RAP", "FIN"] + dialog = CredentialsDialog(self.parent) + dialog.exec() - # endregion + #endregion +#region CDDIS Credentials Dialog Class class CredentialsDialog(QDialog): """ - UI controller class CredentialsDialog. + Modal dialog for entering NASA Earthdata credentials (username/password). + Saves credentials to .netrc for CDDIS access. """ def __init__(self, parent=None): """ - UI handler: initialize credential input widgets and layout. + Initialise credential input widgets and layout. Arguments: - parent (Any): Optional parent widget. + parent: Optional parent widget. """ super().__init__(parent) self.setWindowTitle("CDDIS Credentials") @@ -2944,7 +849,7 @@ def __init__(self, parent=None): def save_credentials(self): """ - UI handler: validate username/password, save to netrc, and close dialog. + Validate username/password, save to netrc, and close dialog. """ username = self.username_input.text().strip() password = self.password_input.text().strip() @@ -2965,41 +870,4 @@ def save_credentials(self): "✅ Credentials saved to:\n" + "\n".join(str(p) for p in paths)) self.accept() - -# Minimal unified stop entry for InputController background worker -def _safe_call_stop(obj): - """ - Safely call .stop() on an object if present, ignoring exceptions. - - Arguments: - obj (Any): Object that may implement stop(). - """ - try: - if obj is not None and hasattr(obj, "stop"): - obj.stop() - except Exception: - pass - - -def stop_all(self): - """ - Best-effort stop for the metadata PPPWorker started by the controller. - - Arguments: - self (InputController): Controller instance owning the worker/thread. - """ - try: - # Request the worker to stop - it will emit cancelled signal when done - if hasattr(self, "worker") and self.worker is not None: - self.worker.stop() - # Stop SINEX validation worker if running - if hasattr(self, "_stop_sinex_worker"): - self._stop_sinex_worker() - # Restore cursor when stopping - if hasattr(self, "parent"): - self.parent.setCursor(Qt.CursorShape.ArrowCursor) - except Exception: - pass - -# Bind without touching existing class body -setattr(InputController, "stop_all", stop_all) \ No newline at end of file +#endregion \ No newline at end of file diff --git a/scripts/GinanUI/app/controllers/output_config_controller.py b/scripts/GinanUI/app/controllers/output_config_controller.py new file mode 100644 index 000000000..580712d54 --- /dev/null +++ b/scripts/GinanUI/app/controllers/output_config_controller.py @@ -0,0 +1,89 @@ +""" +Controller for the Output configuration tab. + +Manages the following UI widgets: + - POS output checkbox (Positioning Solution file) + - GPX output checkbox (GPS Exchange Format file) + - TRACE output checkbox (trace log file) + - SNX output checkbox (SINEX file) + +This controller is intentionally minimal to allow easy expansion as +new output formats or options are added in the future. +""" + +from __future__ import annotations + +class OutputConfigController: + """ + Manages the Output configuration tab: output file type checkboxes. + + Arguments: + ui: The main window UI instance. + input_ctrl: The parent InputController instance (for accessing shared state). + """ + + def __init__(self, ui, input_ctrl): + """ + Initialise output config tab bindings. + + Arguments: + ui: The main window UI instance. + input_ctrl: The parent InputController that owns shared state. + """ + self.ui = ui + self.ctrl = input_ctrl # parent InputController + + #region UI Tooltips + + def setup_tooltips(self): + """ + Set up tooltips for all Output config tab widgets. + """ + self.ui.posCheckbox.setToolTip( + "Enable / disable Ginan (PEA) PPP Processing outputting a Positioning Solution (.POS) file" + ) + self.ui.gpxCheckbox.setToolTip( + "Enable / disable Ginan (PEA) PPP Processing outputting a GPS Exchange Format (.GPX) file" + ) + self.ui.traceCheckbox.setToolTip( + "Enable / disable Ginan (PEA) PPP Processing outputting a trace log (.TRACE) file" + ) + self.ui.snxCheckbox.setToolTip( + "Enable / disable Ginan (PEA) PPP Processing outputting a Solution Independent (.SNX) file" + ) + + #endregion + + #region Output Toggles + + def get_output_toggles(self) -> tuple[bool, bool, bool, bool]: + """ + Read the current state of the output checkboxes. + + Returns: + tuple[bool, bool, bool, bool]: (gpx_output, pos_output, trace_output_network, snx_output) + """ + gpx_output = self.ui.gpxCheckbox.isChecked() if hasattr(self.ui, "gpxCheckbox") else True + pos_output = self.ui.posCheckbox.isChecked() if hasattr(self.ui, "posCheckbox") else True + trace_output_network = self.ui.traceCheckbox.isChecked() if hasattr(self.ui, "traceCheckbox") else False + snx_output = self.ui.snxCheckbox.isChecked() if hasattr(self.ui, "snxCheckbox") else True + return gpx_output, pos_output, trace_output_network, snx_output + + #endregion + + #region Reset to Defaults + + def reset_to_defaults(self): + """ + Reset all Output config tab fields to their default states. + """ + if hasattr(self.ui, 'posCheckbox'): + self.ui.posCheckbox.setChecked(True) + if hasattr(self.ui, 'gpxCheckbox'): + self.ui.gpxCheckbox.setChecked(True) + if hasattr(self.ui, 'traceCheckbox'): + self.ui.traceCheckbox.setChecked(False) + if hasattr(self.ui, 'snxCheckbox'): + self.ui.snxCheckbox.setChecked(False) + + #endregion \ No newline at end of file diff --git a/scripts/GinanUI/app/controllers/visualisation_controller.py b/scripts/GinanUI/app/controllers/visualisation_controller.py index 85a9c0bb6..935f7c050 100644 --- a/scripts/GinanUI/app/controllers/visualisation_controller.py +++ b/scripts/GinanUI/app/controllers/visualisation_controller.py @@ -1,28 +1,22 @@ -# app/controllers/visualisation_controller.py -"""Controller responsible for everything inside the visualisation panel. - -Responsibilities ----------------- -1. Embed one of the generated HTML files into the QTextEdit area. -2. Maintain a list (indexed) of available HTML visualisations. -3. Provide a double-click handler and an explicit *Open* action that open the - current html in the user's default browser. - -NOTE: UI widgets for selecting visualisation (e.g. a ComboBox or QListWidget) - and an *Open* button are **not** yet present in the .ui file. This - controller exposes stub `bind_open_button()` / `bind_selector()` helpers - which can be called once those widgets are added. """ +Controller for the visualisation panel. + +Manages the following UI widgets and behaviours: + - Embedding generated HTML visualisation files into a QWebEngineView + - Maintaining an indexed list of available HTML visualisations + - Opening the current visualisation in the system's default browser or in an attached dialog widget + - Binding a QComboBox selector and Open button for plot navigation +""" + from __future__ import annotations import os import platform import subprocess -import sys from pathlib import Path from typing import List, Sequence, Optional -from PySide6.QtCore import QRect, QUrl, QObject, QEvent +from PySide6.QtCore import QUrl, QObject, Qt from PySide6.QtGui import QDesktopServices -from PySide6.QtWidgets import QTextEdit, QPushButton, QComboBox, QApplication +from PySide6.QtWidgets import QPushButton, QComboBox from PySide6.QtWebEngineWidgets import QWebEngineView from scripts.GinanUI.app.utils.logger import Logger @@ -33,35 +27,20 @@ class VisualisationController(QObject): """ - Manage interactions and rendering inside the visualisation panel. + Manages interactions and rendering inside the visualisation panel. Arguments: - ui (object): The main window UI object that exposes the visualisation widgets (e.g., `visualisationTextEdit`). - parent_window (QObject): The parent window/controller used as the QObject parent. - - Returns: - None: Constructor returns nothing. - - Example: - Function itself returns None; example shows how to instantiate and inspect state. - >>> controller = VisualisationController(ui, parent_window) - >>> controller.html_files - [] + ui: The main window UI object that exposes the visualisation widgets. + parent_window: The parent window/controller used as the QObject parent. """ def __init__(self, ui, parent_window): """ - Initialize controller state and install required event filters. + Initialise controller state and install required event filters. Arguments: ui: The main window UI instance. parent_window: The parent QMainWindow or controller. - - Example: - Function itself returns None; example shows initial empty html_files. - >>> ctrl = VisualisationController(ui, parent_window) - >>> ctrl.html_files - [] """ super().__init__(parent_window) self.ui = ui # Ui_MainWindow instance @@ -71,22 +50,59 @@ def __init__(self, ui, parent_window): self.external_base_url: Optional[str] = None self._selector: Optional[QComboBox] = None self._open_button: Optional[QPushButton] = None + self._enlarge_button: Optional[QPushButton] = None + + self.setup_tooltips() + + # region UI Tooltips + + def setup_tooltips(self): + """ + Set up tooltips for all Visualisation panel widgets. + """ + self.ui.enlargeButton.setToolTip("Enlarge the plot visualisation to a pop-out window") + self.ui.openInBrowserButton.setToolTip("Open the plot visualisation in your system's default web browser") + self.ui.visualisationSelectorCombo.setToolTip( + "Set the active plot visualisation being displayed\n" + "This list is automatically generated according to the files PEA outputted in its processing" + ) + + # endregion + + #region Plotting + + def build_from_execution(self): + """ + Generate visualisation HTML files from the execution model and load them. + """ + try: + exec_obj = getattr(self.parent, "execution", None) + if exec_obj is None: + from PySide6.QtWidgets import QMessageBox + QMessageBox.warning(self.ui, "Plot", "execution object is not set") + return + + new_html_paths = exec_obj.build_pos_plots() # default output to tests/resources/outputData/visual + + # Only use newly generated plots, not old ones from previous runs + new_html_paths.sort(key=lambda x: os.path.basename(x)) + + self.set_html_files(new_html_paths) + + except Exception as e: + from PySide6.QtWidgets import QMessageBox + QMessageBox.critical(self.ui, "Plot Error", str(e)) + + #endregion + + #region HTML Viewing - # --------------------------------------------------------------------- - # Public API (to be called from MainWindow / other controllers) - # --------------------------------------------------------------------- def set_html_files(self, paths: Sequence[str]): """ Register available HTML visualisation files and display the first one. Arguments: paths (Sequence[str]): List of file paths to HTML visualisations. - - Example: - Function itself returns None; example shows state update after call. - >>> controller.set_html_files(["plot1.html", "plot2.html"]) - >>> controller.current_index - 0 """ self.html_files = list(dict.fromkeys(paths)) # Refresh selector if bound @@ -94,30 +110,28 @@ def set_html_files(self, paths: Sequence[str]): self._refresh_selector() if self.html_files: self.display_html(0) - # Enable widgets once we have plots + # Show widgets once we have plots if self._selector: - self._selector.setEnabled(True) + self._selector.setVisible(True) if self._open_button: - self._open_button.setEnabled(True) + self._open_button.setVisible(True) + if self._enlarge_button: + self._enlarge_button.setVisible(True) else: - # Disable widgets if no plots available + # Hide widgets if no plots available if self._selector: - self._selector.setEnabled(False) + self._selector.setVisible(False) if self._open_button: - self._open_button.setEnabled(False) + self._open_button.setVisible(False) + if self._enlarge_button: + self._enlarge_button.setVisible(False) def display_html(self, index: int): """ Embed the HTML file at the given index into the visualisation panel. Arguments: - index (int): Zero-based index into `self.html_files`. - - Example: - Function itself returns None; example shows updated index. - >>> controller.display_html(0) - >>> controller.current_index - 0 + index (int): Zero-based index into self.html_files. """ if not isinstance(index, int) or not (0 <= index < len(self.html_files)): return @@ -127,18 +141,13 @@ def display_html(self, index: int): def open_current_external(self): """ - Open the currently displayed HTML in the system’s default web browser. - - Example: - Function itself returns None; example shows that return value is None. - >>> controller.open_current_external() is None - True + Open the currently displayed HTML in the system's default web browser. """ if self.current_index is None: return path = self.html_files[self.current_index] try: - url = QUrl.fromLocalFile(Path(path).resolve()) + url = QUrl.fromLocalFile(str(Path(path).resolve())) # Open the file with the appropriate method for the operating system if platform.system() == "Windows": @@ -161,24 +170,64 @@ def open_current_external(self): except Exception as e: Logger.console(f"Error occurred trying to open in browser: {e}") - # ------------------------------------------------------------------ - # Helpers for wiring additional UI elements - # ------------------------------------------------------------------ + def open_current_enlarged(self): + """ + Open the currently displayed HTML in a resizable embedded browser window + """ + if self.current_index is None: + return + path = self.html_files[self.current_index] + url = QUrl.fromLocalFile(str(Path(path).resolve())) + self._open_enlarged_window(url) + + def _open_enlarged_window(self, url: QUrl): + """ + Create and display a resizable window containing a QWebEngineView + + Arguments: + url (QUrl): The local file URL of the HTML visualisation to display + """ + from PySide6.QtWidgets import QDialog, QVBoxLayout + + dialog = QDialog(self.parent) + dialog.setWindowTitle("Visualisation") + dialog.setWindowFlags( + Qt.WindowType.Window | + Qt.WindowType.WindowMinimizeButtonHint | + Qt.WindowType.WindowMaximizeButtonHint | + Qt.WindowType.WindowCloseButtonHint + ) + dialog.resize(1200, 800) + dialog.setMinimumSize(600, 400) + + layout = QVBoxLayout(dialog) + layout.setContentsMargins(0, 0, 0, 0) + + webview = QWebEngineView(dialog) + webview.setUrl(url) + layout.addWidget(webview) + + dialog.show() + + # Keep a reference to prevent garbage collection before the dialog closes + self._enlarged_dialogs = getattr(self, "_enlarged_dialogs", []) + self._enlarged_dialogs.append(dialog) + dialog.finished.connect(lambda: self._enlarged_dialogs.remove(dialog)) + + #endregion + + #region Widget Binding + def bind_open_button(self, button: QPushButton): """ - Connect an *Open* button to open the current visualisation externally. + Connect an Open button to open the current visualisation externally. Arguments: button (QPushButton): The push button to connect to the handler. - - Example: - Function itself returns None; example shows valid binding. - >>> controller.bind_open_button(ui.openButton) is None - True """ self._open_button = button button.clicked.connect(self.open_current_external) - button.setEnabled(False) + button.setVisible(False) def bind_selector(self, combo: QComboBox): """ @@ -186,11 +235,6 @@ def bind_selector(self, combo: QComboBox): Arguments: combo (QComboBox): The combo box used as selector. - - Example: - Function itself returns None; example shows valid selector binding. - >>> controller.bind_selector(ui.comboBox) is None - True """ self._selector = combo @@ -200,27 +244,48 @@ def safe_display(): self.display_html(data) combo.currentIndexChanged.connect(lambda _: safe_display()) - combo.setEnabled(False) + combo.setVisible(False) self._refresh_selector() + def bind_enlarge_button(self, button: QPushButton): + """ + Connect an Enlarge button to open the current visualisation in a new window + + Arguments: + button (QPushButton): The push button to connect to the handler + """ + self._enlarge_button = button + button.clicked.connect(self.open_current_enlarged) + button.setVisible(False) + + #endregion + + #region Configuration + + def set_external_base_url(self, url: str): + """ + Set a base HTTP URL to prefer when opening visualisations externally. + + Arguments: + url (str): Base URL (a trailing slash is appended if missing). + """ + if not url.endswith('/'): + url += '/' + self.external_base_url = url + + #endregion + + #region Helper Functions + def _refresh_selector(self): """ Populate the selector combo box with available HTML files. - - Example: - # Function itself returns None; example shows refresh success. - >>> controller._refresh_selector() is None - True """ if not self._selector: return self._selector.clear() for idx, path in enumerate(self.html_files): - self._selector.addItem(f"#{idx} – {os.path.basename(path)}", userData=idx) - - # ------------------------------------------------------------------ - # Internal helpers - # ------------------------------------------------------------------ + self._selector.addItem(f"#{idx} — {os.path.basename(path)}", userData=idx) def _embed_html(self, file_path: str): """ @@ -245,64 +310,12 @@ def _embed_html(self, file_path: str): # Keep a reference to avoid GC (and for later access) self._webview = webview - # ------------------------------------------------------------------ - # Optional configuration - # ------------------------------------------------------------------ - def set_external_base_url(self, url: str): - """ - Set a base HTTP URL to prefer when opening visualisations externally. - - Arguments: - url (str): Base URL (a trailing slash is appended if missing). - - Example: - Function itself returns None; example shows URL assignment. - >>> controller.set_external_base_url("http://localhost:8000/") - >>> controller.external_base_url - 'http://localhost:8000/' - """ - if not url.endswith('/'): - url += '/' - self.external_base_url = url - - def build_from_execution(self): - """ - Generate visualisation HTML files from the execution model and load them. - - Example: - Function itself returns None; example checks that call succeeds. - >>> controller.build_from_execution() is None - True - """ - try: - exec_obj = getattr(self.parent, "execution", None) - if exec_obj is None: - from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self.ui, "Plot", "execution object is not set") - return - - new_html_paths = exec_obj.build_pos_plots() # default output to tests/resources/outputData/visual - - # Only use newly generated plots, not old ones from previous runs - new_html_paths.sort(key=lambda x: os.path.basename(x)) - - self.set_html_files(new_html_paths) - - except Exception as e: - from PySide6.QtWidgets import QMessageBox - QMessageBox.critical(self.ui, "Plot Error", str(e)) - def _find_existing_html_files(self): """ Locate and return paths of existing visualisation HTML files. Returns: - list[str]: A list of absolute paths to discovered HTML files.git - - Example: - Function returns a list; example checks returned type. - >>> isinstance(controller._find_existing_html_files(), list) - True + list[str]: A list of absolute paths to discovered HTML files. """ existing_files = [] @@ -314,4 +327,6 @@ def _find_existing_html_files(self): if self.external_base_url: pass - return existing_files \ No newline at end of file + return existing_files + + #endregion \ No newline at end of file diff --git a/scripts/GinanUI/app/controllers/yaml_config_controller.py b/scripts/GinanUI/app/controllers/yaml_config_controller.py new file mode 100644 index 000000000..fbb80fddf --- /dev/null +++ b/scripts/GinanUI/app/controllers/yaml_config_controller.py @@ -0,0 +1,744 @@ +""" +Controller for the YAML configuration tab. + +Manages the following UI widgets and behaviours: + - "Show Config" button + - "Reset Config" button + - "Overwrite Config with UI Values" toggle switch (Determines how the config file is written to) + - Opening the GinanYAMLInspector +""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path +from PySide6.QtCore import Qt, QUrl, QObject, Slot +from PySide6.QtWidgets import QFileDialog, QDialog, QVBoxLayout, QMessageBox +from PySide6.QtWebEngineWidgets import QWebEngineView +from PySide6.QtWebEngineCore import QWebEngineSettings, QWebEngineDownloadRequest +from PySide6.QtWebChannel import QWebChannel +from scripts.GinanUI.app.models.inspector import Inspector +from scripts.GinanUI.app.utils.common_dirs import GENERATED_YAML, INSPECTOR_HTML_PATH +from scripts.GinanUI.app.utils.logger import Logger +from scripts.GinanUI.app.utils.toast import show_toast + +class _InspectorBridge(QObject): + """ + Qt object exposed to the GinanYAMLInspector page via QWebChannel. + + The inspector's "Save file" button is intercepted by injected JavaScript + which calls bridge.saveYaml(yamlText) instead of triggering a file download. + This allows Ginan-UI to write the YAML directly to ppp_generated.yaml and + notify the user without any file-picker dialog. + """ + + def __init__(self, yaml_config_ctrl: "YAMLConfigController", parent=None): + super().__init__(parent) + self._ctrl = yaml_config_ctrl + + @Slot(str) + def saveYaml(self, yaml_text: str): + """ + Receive YAML text from the inspector page and write it to ppp_generated.yaml. + Called from JavaScript when the user clicks "Save file" in the inspector. + + Arguments: + yaml_text (str): The raw YAML string generated by the inspector. + """ + self._ctrl._on_inspector_save(yaml_text) + + +class YAMLConfigController: + """ + Manages the YAML configuration tab: output file type checkboxes. + + Arguments: + ui: The main window UI instance. + input_ctrl: The parent InputController instance (for accessing shared state). + """ + + def __init__(self, ui, input_ctrl): + """ + Initialise output config tab bindings. + + Arguments: + ui: The main window UI instance. + input_ctrl: The parent InputController that owns shared state. + """ + self.ui = ui + self.ctrl = input_ctrl # parent InputController + + # Button wiring + self.ui.showConfigButton.clicked.connect(self.on_show_config) + self.ui.showConfigButton.setCursor(Qt.CursorShape.PointingHandCursor) + self.ui.resetConfigButton.clicked.connect(self._on_reset_config_clicked) + self.ui.editConfigInInspectorButton.clicked.connect(self.open_inspector) + + # Keep a reference to the open inspector dialog (if any) to avoid garbage collection + self._inspector_dialog: QDialog | None = None + + # Bridge object for QWebChannel (inspector -> Python "Save file" interception) + self._bridge: _InspectorBridge | None = None + + # Inspector model: handles all non-UI work (HTML generation, JS building, YAML sanitisation / merging / writing) + # It is instantiated by _get_inspector() + # so the executable from the Execution model is resolved at first use rather + # than at controller construction time + self._inspector: Inspector | None = None + + # Initial button states + self.ui.showConfigButton.setEnabled(False) + + def _get_inspector(self) -> Inspector: + """ + Return the Inspector model singleton instance, creating it on first access if it doesn't already exist + + The PEA executable is taken from the Execution model so the inspector + can auto-generate its HTML asset via "pea -Y 4" when missing. + + Returns: + Inspector: The shared Inspector model instance. + """ + if self._inspector is None: + executable = getattr(self.ctrl.execution, "executable", None) + self._inspector = Inspector(executable=executable) + return self._inspector + + #region UI Tooltips + + def setup_tooltips(self): + """ + Set up tooltips for all Output config tab widgets. + """ + self.ui.yamlOverwriteCheckbox.setToolTip( + "Enable / disable overwriting the YAML config file with the values specified here within Ginan-UI." + "\nUseful for manual changes that Ginan-UI would otherwise overwrite." + ) + self.ui.showConfigButton.setToolTip( + "Generate and open the YAML configuration file.\n" + "You can review and modify advanced settings before processing.\n" + "Note: UI defined parameters will ALWAYS override manual config edits." + ) + self.ui.resetConfigButton.setToolTip( + "Delete and regenerate the YAML configuration file and start from a clean slate.\n" + "Note: Will delete all modifications to the existing file!" + ) + self.ui.editConfigInInspectorButton.setToolTip( + "Open the YAML configuration file in the GinanYAMLInspector external tool\n" + "Useful for large configuration changes that are not possible with what Ginan-UI exposes.\n" + "The current config will be pre-loaded. Use 'Save file' in the Inspector to write changes back." + ) + + #endregion + + #region YAML Toggles + + def get_yaml_toggles(self) -> bool: + """ + Read the current state of the YAML checkboxes + + Returns: + bool: yaml_overwrite + """ + yaml_overwrite = self.ui.yamlOverwriteCheckbox.isChecked() if hasattr(self.ui, "yamlOverwriteCheckbox") else True + return yaml_overwrite + + #endregion + + #region Config Handling + + def on_show_config(self): + """ + UI handler: reload config, apply UI values, write changes, then open the YAML. + If YAML overwrite is disabled, skip applying UI values and just open the file. + """ + Logger.workflow("📄 Opening YAML configuration file...") + yaml_overwrite = self.get_yaml_toggles() + + if yaml_overwrite: + self.ctrl.execution.reload_config() + inputs = self.ctrl.extract_ui_values(self.ctrl.rnx_file) + self.ctrl.execution.apply_ui_config(inputs) + self.ctrl.execution.write_cached_changes() + + # Execution class will throw error when instantiated if the file doesn't exist and it can't create it + # This code is run after Execution class is instantiated within this file, thus never will occur + if not os.path.exists(GENERATED_YAML): + QMessageBox.warning( + None, + "File not found", + f"The file {GENERATED_YAML} does not exist." + ) + return + + self._open_config_in_editor(self.ctrl.config_path) + + def _open_config_in_editor(self, file_path): + """ + Open the config YAML file in the OS default editor/viewer. + + Arguments: + file_path (str): Absolute or relative path to the YAML file. + """ + import platform + + try: + abs_path = os.path.abspath(file_path) + + # Open the file with the appropriate method for the operating system + if platform.system() == "Windows": + os.startfile(abs_path) + return + + if platform.system() == "Darwin": # macOS + subprocess.run(["open", abs_path]) + + else: # Linux and other Unix-like systems + # When compiled with pyinstaller, LD_LIBRARY_PATH is modified which prevents external app opening + env = os.environ.copy() + original = env.get("LD_LIBRARY_PATH_ORIG") + if original: + env["LD_LIBRARY_PATH"] = original # Restore original value + else: + env.pop("LD_LIBRARY_PATH", None) # Clear the value to use sys defaults + subprocess.run(["xdg-open", abs_path], env=env) + + except Exception as e: + error_message = f"Cannot open config file:\n{file_path}\n\nError: {str(e)}" + Logger.workflow(f"Error: {error_message}") + QMessageBox.critical(None, "Error Opening File", error_message) + + def _on_reset_config_clicked(self): + """ + UI handler: reset the configuration file and UI to defaults. + Shows a confirmation dialog before proceeding. + """ + reply = QMessageBox.question( + self.ctrl.parent, + "Reset Configuration", + "This will reset all settings to their defaults.\n\n" + "• The configuration file will be regenerated from the template\n" + "• All UI fields will be cleared\n" + "• You will need to re-select your RINEX file and output directory\n\n" + "Are you sure you want to continue?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply != QMessageBox.StandardButton.Yes: + return + + try: + self.ctrl.stop_all() + self.ctrl.execution.reset_config() + self.ctrl._reset_ui_to_defaults() + Logger.workflow("🔄 Configuration and UI reset to defaults") + show_toast(self.ctrl.parent, "🔄 Configuration and UI reset to defaults", duration=3000) + + except Exception as e: + Logger.workflow(f"⚠️ Failed to reset configuration: {e}") + QMessageBox.critical(self.ctrl.parent, "Reset Failed", f"Failed to reset configuration:\n{e}") + + #endregion + + #region Reset to Defaults + + def reset_to_defaults(self): + """ + Reset all YAML config tab fields to their default states. + """ + if hasattr(self.ui, 'yamlOverwriteCheckbox'): + self.ui.yamlOverwriteCheckbox.setChecked(True) + + #endregion + + #region GinanYAMLInspector + + def _ui_is_ready(self) -> bool: + """ + Return True if the UI has enough state populated to safely call extract_ui_values() + + Checks that a RINEX file has been selected and that the time window field contains + a value that looks like a valid range (i.e. it contains " to "). This mirrors the + minimum requirements of parse_time_window() so we avoid raising inside + _prepare_config_for_inspector() when the user opens the inspector before loading a + RINEX file + + Returns: + bool: True if the UI is sufficiently populated for config extraction. + """ + if not self.ctrl.rnx_file: + return False + time_window = self.ui.timeWindowValue.text() if hasattr(self.ui, "timeWindowValue") else "" + return " to " in time_window + + def _prepare_config_for_inspector(self) -> bool: + """ + Ensure ppp_generated.yaml is up-to-date before opening the inspector + + Applies UI values to the config if "Overwrite Config with UI Values" is enabled + AND the UI has been fully populated (RINEX loaded, time window set). Otherwise + falls back silently to whatever config is already on disk. + + Returns: + bool: True if the config file is ready on disk, False on error. + """ + yaml_overwrite = self.get_yaml_toggles() + ui_ready = self._ui_is_ready() + + if yaml_overwrite and ui_ready: + try: + self.ctrl.execution.reload_config() + inputs = self.ctrl.extract_ui_values(self.ctrl.rnx_file) + self.ctrl.execution.apply_ui_config(inputs) + self.ctrl.execution.write_cached_changes() + Logger.workflow("📄 Config updated with current UI values for GinanYAMLInspector") + except Exception as e: + # Log but do not block - fall through to open with existing file + Logger.workflow(f"⚠️ Could not apply UI values to config for GinanYAMLInspector: {e}") + elif yaml_overwrite and not ui_ready: + Logger.workflow("📄 RINEX not yet loaded - Opening GinanYAMLInspector with existing config file") + else: + Logger.workflow("📄 YAML overwrite disabled - Opening GinanYAMLInspector with existing config file") + + return GENERATED_YAML.exists() + + def _on_inspector_save(self, yaml_text: str): + """ + Callback triggered when the user clicks "Save file" in the GinanYAMLInspector + + Delegates the sanitise / parse / merge / write / repair / fallback flow to the + Inspector model and handles the UI-side feedback: toasts, error dialogs, reloading + the Execution config, and repopulating the UI fields from the merged config so the + UI stays in sync with what is now on disk. + + Arguments: + yaml_text (str): Raw YAML string produced by the inspector's "Generate yaml" step + """ + try: + merged = self._get_inspector().merge_and_save(yaml_text) + except ValueError as e: + # Empty input or non-mapping output - show a toast and abort quietly + show_toast(self.ctrl.parent, f"⚠️ Inspector save aborted: {e}", duration=4000) + return + except Exception as e: + Logger.workflow(f"❌ Failed to save GinanYAMLInspector config: {e}") + QMessageBox.critical( + self.ctrl.parent, + "Save Failed", + f"Could not merge the inspector config with the existing file:\n{e}" + ) + return + + # Reload the saved config into the Execution model + reload_ok = False + try: + self.ctrl.execution.reload_config() + Logger.workflow("🔄 Config reloaded from GinanYAMLInspector") + reload_ok = True + except Exception as e: + Logger.workflow(f"⚠️ Config written to disk but Execution reload raised: {e}") + + # Repopulate UI fields from the merged config so the UI stays in sync. + # Use the freshly reloaded execution config if available; otherwise fall back + # to the merged dict we computed above so the UI still updates even when + # the reload failed. + ui_config = self.ctrl.execution.config if reload_ok else merged + try: + self.populate_ui_from_config(ui_config) + except Exception as e: + Logger.workflow(f"⚠️ Config saved but UI repopulation raised: {e}") + + show_toast(self.ctrl.parent, "✅ GinanYAMLInspector config saved", duration=4000) + + def populate_ui_from_config(self, config: dict): + """ + Read values from a loaded YAML config dict and push them back into the + relevant Ginan-UI widgets so the UI stays in sync after an inspector save + + Only fields that are present in the config are updated - missing keys are + silently skipped so that partial inspector exports do not blank out fields + that were not touched + + The following UI areas are covered: + - Time window (start/end epoch) and epoch interval + - Receiver type, antenna type, antenna offset, apriori position + - GNSS constellation selection (General tab multi-select combo) + - Observation code priorities per constellation (Constellation tab list widgets) + - Output toggles (POS, GPX, TRACE, SNX) + - Processing mode (Static / Kinematic / Dynamic / Custom via process_noise value) + + Arguments: + config (dict): Loaded ruamel.yaml CommentedMap (or plain dict) from + ppp_generated.yaml after the inspector merge + """ + ui = self.ui + + # Helper: safely navigate a nested dict by dot-path + def _get(path: str, default=None): + keys = path.split(".") + node = config + for k in keys: + if not isinstance(node, dict) or k not in node: + return default + node = node[k] + return node + + # Time window + start = _get("processing_options.epoch_control.start_epoch") + end = _get("processing_options.epoch_control.end_epoch") + if start and end: + # Normalise ruamel PlainScalarString / datetime objects to plain str + start_str = str(start).strip("'\"").replace(" ", "_") + end_str = str(end).strip("'\"").replace(" ", "_") + time_text = f"{start_str} to {end_str}" + if hasattr(ui, "timeWindowButton"): + ui.timeWindowButton.setText(time_text) + if hasattr(ui, "timeWindowValue"): + ui.timeWindowValue.setText(time_text) + + # Epoch interval + interval = _get("processing_options.epoch_control.epoch_interval") + if interval is not None: + interval_text = f"{int(interval) if float(interval) == int(float(interval)) else float(interval)} s" + if hasattr(ui, "dataIntervalButton"): + ui.dataIntervalButton.setText(interval_text) + if hasattr(ui, "dataIntervalValue"): + ui.dataIntervalValue.setText(interval_text) + + # Receiver / antenna metadata + # Look up the marker key that corresponds to the currently active RINEX file. + # There can be multiple non-global keys under receiver_options (one per station); + # we only update the UI fields when the key matches the active marker so we don't + # accidentally populate values from a different station's block. + # If no RINEX is loaded yet, fall back to the first non-global key found. + receiver_options = _get("receiver_options", {}) + active_marker = None + if self.ctrl.rnx_file: + from scripts.GinanUI.app.controllers.input_controller import InputController + active_marker = InputController.extract_marker_name(self.ctrl.rnx_file) + + def _find_marker_key(rec_opts: dict, preferred: str | None) -> str | None: + """Return the receiver_options key to use for UI population. + + Tries preferred (active marker) first; falls back to the first + non-global key if preferred is absent or no RINEX is loaded. + """ + if preferred and preferred in rec_opts: + return preferred + return next((k for k in rec_opts if k != "global"), None) + + marker_key = _find_marker_key(receiver_options, active_marker) + + def _strip_yaml_comment(value: str) -> str: + """Strip a trailing inline YAML comment from a plain-scalar value. + + The inspector copies the raw YAML line content including any inline + comment, e.g. 'SEPT POLARXSSS # #USER_SET (string)'. Only the part + before the first ' #' sequence is the actual value. + """ + # Split on ' #' but not on '#' that appears inside a word (unlikely + # in these fields, but safe to be conservative with the leading space). + idx = value.find(' #') + return value[:idx].strip() if idx != -1 else value.strip() + + if marker_key: + rec_block = receiver_options[marker_key] + + receiver_type = rec_block.get("receiver_type") if isinstance(rec_block, dict) else None + if receiver_type is not None: + cleaned = _strip_yaml_comment(str(receiver_type)) + if cleaned: + # Mirror the same combo + label update that the popup dialog performs + if hasattr(ui, "receiverTypeCombo"): + ui.receiverTypeCombo.clear() + ui.receiverTypeCombo.addItem(cleaned) + if ui.receiverTypeCombo.lineEdit(): + ui.receiverTypeCombo.lineEdit().setText(cleaned) + if hasattr(ui, "receiverTypeValue"): + ui.receiverTypeValue.setText(cleaned) + + antenna_type = rec_block.get("antenna_type") if isinstance(rec_block, dict) else None + if antenna_type is not None: + cleaned = _strip_yaml_comment(str(antenna_type)) + if cleaned: + # Mirror the same combo + label update that the popup dialog performs + if hasattr(ui, "antennaTypeCombo"): + ui.antennaTypeCombo.clear() + ui.antennaTypeCombo.addItem(cleaned) + if ui.antennaTypeCombo.lineEdit(): + ui.antennaTypeCombo.lineEdit().setText(cleaned) + if hasattr(ui, "antennaTypeValue"): + ui.antennaTypeValue.setText(cleaned) + + # Antenna offset [E, N, U] + offset = rec_block.get("models", {}).get("eccentricity", {}).get("offset") if isinstance(rec_block, dict) else None + if offset and len(offset) == 3: + offset_text = ", ".join(str(v) for v in offset) + if hasattr(ui, "antennaOffsetButton"): + ui.antennaOffsetButton.setText(offset_text) + if hasattr(ui, "antennaOffsetValue"): + ui.antennaOffsetValue.setText(offset_text) + + # Apriori position [X, Y, Z] + apriori = rec_block.get("apriori_position") if isinstance(rec_block, dict) else None + if apriori and len(apriori) == 3: + apriori_text = ", ".join(str(v) for v in apriori) + if hasattr(ui, "aprioriPositionButton"): + ui.aprioriPositionButton.setText(apriori_text) + + # GNSS constellation selection and code priorities + # Both the General tab multi-select and the Constellation tab list widgets + # are driven together from sys_options in the YAML. + # + # Constellation selection: + # _update_constellations_multiselect() rebuilds the checkable combo model + # with only the enabled constellations pre-checked, and internally calls + # sync_list_widgets_to_selection() to show/hide the per-constellation list + # widgets accordingly. + # + # Code priorities: + # _setup_list_widget() populates each QListWidget with the codes from the + # YAML's code_priorities list (in order). All items in that list are checked + # because the inspector only emits codes that were selected - unchecked codes + # from a previous RINEX load are not present in the YAML export and therefore + # are not shown at all after an inspector save. + const_map = { + "gps": ("GPS", "gpsListWidget"), + "gal": ("GAL", "galListWidget"), + "glo": ("GLO", "gloListWidget"), + "bds": ("BDS", "bdsListWidget"), + "qzs": ("QZS", "qzsListWidget"), + } + sys_options = _get("processing_options.gnss_general.sys_options", {}) + if isinstance(sys_options, dict): + enabled_labels = [ + label + for key, (label, _) in const_map.items() + if sys_options.get(key, {}).get("process", False) + ] + + # Rebuild the General tab constellation combo with the enabled set + if enabled_labels: + self.ctrl.general_tab._update_constellations_multiselect( + ", ".join(enabled_labels) + ) + else: + # No constellations enabled - clear the combo and show placeholder + if hasattr(ui, "constellationsValue"): + ui.constellationsValue.setText("") + self.ctrl.constellations_tab._update_placeholder(True) + + # Repopulate each constellation list widget from code_priorities + for yaml_key, (label, widget_name) in const_map.items(): + if not hasattr(ui, widget_name): + continue + list_widget = getattr(ui, widget_name) + const_block = sys_options.get(yaml_key) + if not isinstance(const_block, dict): + continue + raw_codes = const_block.get("code_priorities") + if raw_codes is None: + continue + # Normalise to a plain list of uppercase strings + codes = [str(c).strip() for c in raw_codes if str(c).strip()] + if codes: + # All codes in the YAML list are the selected/checked ones - + # pass the same set as both the full list and the enabled set + self.ctrl.constellations_tab._setup_list_widget( + list_widget, codes, set(codes) + ) + else: + # Empty list - clear the widget but leave it enabled so the user + # can see that the constellation has no codes assigned + list_widget.clear() + + # Output toggles + gpx_out = _get("outputs.gpx.output") + pos_out = _get("outputs.pos.output") + trace_out = _get("outputs.trace.output_network") + snx_out = _get("outputs.sinex.output") + + if gpx_out is not None and hasattr(ui, "gpxCheckbox"): + ui.gpxCheckbox.setChecked(bool(gpx_out)) + if pos_out is not None and hasattr(ui, "posCheckbox"): + ui.posCheckbox.setChecked(bool(pos_out)) + if trace_out is not None and hasattr(ui, "traceCheckbox"): + ui.traceCheckbox.setChecked(bool(trace_out)) + if snx_out is not None and hasattr(ui, "snxCheckbox"): + ui.snxCheckbox.setChecked(bool(snx_out)) + + # Processing mode + # Inferred from estimation_parameters.receivers.global.pos.process_noise: + # 0 -> Static + # 30 -> Kinematic + # 100 -> Dynamic + # anything else -> Custom (shown as a non-standard label) + # + # modeCombo uses _bind_combo which repopulates ["Static", "Kinematic", + # "Dynamic"] only when the user opens the dropdown. At rest the combo just + # displays whatever text is in it, so we can set an arbitrary label by + # clearing and adding a single item. The next time the user opens the popup + # the standard items are restored normally by the bound showPopup hook. + process_noise = _get("estimation_parameters.receivers.global.pos.process_noise") + if process_noise is not None and hasattr(ui, "modeCombo"): + try: + noise_val = int(float( + process_noise[0] + if hasattr(process_noise, '__iter__') and not isinstance(process_noise, str) + else process_noise + )) + mode_label = {0: "Static", 30: "Kinematic", 100: "Dynamic"}.get( + noise_val, f"Custom ({noise_val})" + ) + combo = ui.modeCombo + combo.clear() + combo.addItem(mode_label) + except (TypeError, ValueError, IndexError): + pass + + def open_inspector(self): + """ + UI handler: open the GinanYAMLInspector in an embedded Qt dialog + + Before opening: + 1. The ppp_generated.yaml config is written/updated (if YAML overwrite is on) + 2. The inspector HTML is generated via "pea -Y 4" if it does not already exist + + Once loaded: + - The current ppp_generated.yaml is auto-imported into the inspector's fields + - The "Save file" button is intercepted so that saving merges the inspector + output back onto ppp_generated.yaml (preserving keys the inspector omitted) + rather than doing a destructive overwrite + + The inspector remains fully usable outside Ginan-UI - the Ginan-UI-specific + behaviour (auto-import, save intercept) is only wired up when opened from here + """ + inspector = self._get_inspector() + + # Step 1: Ensure config is up to date on disk + config_ready = self._prepare_config_for_inspector() + if not config_ready: + QMessageBox.warning( + self.ctrl.parent, + "Config Not Found", + f"The config file does not exist yet:\n{GENERATED_YAML}\n\n" + "Please select a RINEX file first so the config can be generated." + ) + return + + # Step 2: Ensure the inspector HTML exists + if not inspector.ensure_inspector_html(): + QMessageBox.warning( + self.ctrl.parent, + "Inspector Not Available", + f"The GinanYAMLInspector HTML file could not be found or generated.\n\n" + f"Expected location:\n{INSPECTOR_HTML_PATH}\n\n" + "You can generate it manually by running:\n" + " pea -Y 4\n" + "and placing the output at the path above." + ) + return + + Logger.workflow("🔍 Opening GinanYAMLInspector...") + + # Step 3: Read the YAML content now (before any async load completes) + yaml_content = inspector.read_current_config() + + # Step 4: Build the inspector dialog + dialog = QDialog(self.ctrl.parent) + dialog.setWindowTitle("GinanYAMLInspector") + dialog.setWindowFlags( + Qt.WindowType.Window | + Qt.WindowType.WindowMinimizeButtonHint | + Qt.WindowType.WindowMaximizeButtonHint | + Qt.WindowType.WindowCloseButtonHint + ) + dialog.resize(1200, 800) + dialog.setMinimumSize(600, 400) + + layout = QVBoxLayout(dialog) + layout.setContentsMargins(0, 0, 0, 0) + + webview = QWebEngineView(dialog) + + settings = webview.settings() + settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptEnabled, True) + settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True) + settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True) + + # Step 5: Set up QWebChannel so the page can call bridge.saveYaml() + self._bridge = _InspectorBridge(self) + channel = QWebChannel(webview.page()) + channel.registerObject("bridge", self._bridge) + webview.page().setWebChannel(channel) + + # Step 6: Inject qwebchannel.js via QWebEngineScript so it is available before + # any page script runs. This guarantees qt.webChannelTransport exists when our + # setup JS calls new QWebChannel(...). + from PySide6.QtWebEngineCore import QWebEngineScript + qwc_script = QWebEngineScript() + qwc_script.setName("qwebchannel_init") + qwc_script.setSourceUrl(QUrl("qrc:///qtwebchannel/qwebchannel.js")) + qwc_script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation) + qwc_script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld) + webview.page().scripts().insert(qwc_script) + + # Step 7: On load, run the Ginan-UI setup script (auto-import + save intercept) + ginan_ui_js = Inspector.build_ginan_ui_js(yaml_content) + + def on_load_finished(ok: bool): + if not ok: + Logger.workflow("⚠️ GinanYAMLInspector page failed to load.") + return + webview.page().runJavaScript(ginan_ui_js) + + webview.loadFinished.connect(on_load_finished) + + # Step 8: Fallback download handler - if the save intercept somehow does not + # fire (e.g. opened outside Ginan-UI then re-used), catch the browser download, + # merge it into ppp_generated.yaml, and notify the user. + profile = webview.page().profile() + + def handle_download(download: QWebEngineDownloadRequest): + """ + Fallback: browser download triggered - ask for save path, then merge back. + """ + suggested = download.downloadFileName() or "output.yaml" + + save_path, _ = QFileDialog.getSaveFileName( + dialog, + "Save Inspector YAML", + str(Path.home() / suggested) + ) + + if not save_path: + download.cancel() + return + + download.setDownloadDirectory(str(Path(save_path).parent)) + download.setDownloadFileName(Path(save_path).name) + download.accept() + + Logger.workflow(f"💾 GinanYAMLInspector config saved via browser download to: {save_path}") + + # Merge the downloaded file back into ppp_generated.yaml + try: + downloaded_text = Path(save_path).read_text(encoding="utf-8") + self._on_inspector_save(downloaded_text) + except Exception as e: + Logger.workflow(f"⚠️ Could not merge GinanYAMLInspector download into ppp_generated.yaml: {e}") + + profile.downloadRequested.connect(handle_download) + + webview.setUrl(QUrl.fromLocalFile(str(INSPECTOR_HTML_PATH))) + layout.addWidget(webview) + + # Keep a reference so the dialog is not garbage collected while open + self._inspector_dialog = dialog + dialog.finished.connect(lambda: setattr(self, "_inspector_dialog", None)) + + dialog.show() + + #endregion \ No newline at end of file diff --git a/scripts/GinanUI/app/main_window.py b/scripts/GinanUI/app/main_window.py index 303b4b3af..7e9e93a7d 100644 --- a/scripts/GinanUI/app/main_window.py +++ b/scripts/GinanUI/app/main_window.py @@ -1,27 +1,32 @@ +""" +Main application window for Ginan-UI. + +Owns the top-level QMainWindow, initialises and wires together all controllers +(InputController, VisualisationController), manages the Logger system, and handles +the CDDIS credential check dialog shown on startup. Also owns the PEA execution +and download worker threads, and provides the log_message() slot used by Logger +to route messages to the Workflow and Console widgets. +""" + from pathlib import Path from typing import Optional - -from PySide6.QtCore import QUrl, Signal, QThread, Slot, Qt, QRegularExpression, QCoreApplication -from scripts.GinanUI.app.utils.logger import Logger -from PySide6.QtWidgets import QMainWindow, QDialog, QVBoxLayout, QPushButton, QComboBox, QMessageBox -from PySide6.QtWebEngineWidgets import QWebEngineView +from PySide6.QtCore import QCoreApplication, QRegularExpression, QThread, QUrl, Qt, Signal, Slot from PySide6.QtGui import QTextCursor, QTextDocument - +from PySide6.QtWidgets import QMainWindow, QMessageBox +from scripts.GinanUI.app.models.archive_manager import archive_old_outputs, archive_products, archive_products_if_selection_changed +from scripts.GinanUI.app.models.execution import INPUT_PRODUCTS_PATH, Execution +from scripts.GinanUI.app.controllers.input_controller import InputController +from scripts.GinanUI.app.controllers.visualisation_controller import VisualisationController +from scripts.GinanUI.app.utils.cddis_connection import get_username_from_netrc, test_cddis_connection, write_email from scripts.GinanUI.app.utils.cddis_credentials import validate_netrc as gui_validate_netrc -from scripts.GinanUI.app.models.execution import Execution +from scripts.GinanUI.app.utils.logger import Logger from scripts.GinanUI.app.utils.toast import show_toast from scripts.GinanUI.app.utils.ui_compilation import compile_ui -from scripts.GinanUI.app.controllers.input_controller import InputController -from scripts.GinanUI.app.controllers.visualisation_controller import VisualisationController -from scripts.GinanUI.app.utils.cddis_email import get_username_from_netrc, write_email, test_cddis_connection -from scripts.GinanUI.app.utils.workers import PeaExecutionWorker, DownloadWorker -from scripts.GinanUI.app.models.archive_manager import archive_products_if_selection_changed, archive_products, archive_old_outputs -from scripts.GinanUI.app.models.execution import INPUT_PRODUCTS_PATH +from scripts.GinanUI.app.utils.workers import DownloadWorker, PeaExecutionWorker # Optional toggle for development visualization testing test_visualisation = False - def setup_main_window(): import sys IS_FROZEN = getattr(sys, 'frozen', False) @@ -36,6 +41,8 @@ def setup_main_window(): class MainWindow(QMainWindow): log_signal = Signal(str) + #region Initialisation + def __init__(self): super().__init__() @@ -100,6 +107,7 @@ def __init__(self): # Visualisation widgets self.visCtrl.bind_open_button(self.ui.openInBrowserButton) + self.visCtrl.bind_enlarge_button(self.ui.enlargeButton) self.visCtrl.bind_selector(self.ui.visualisationSelectorCombo) @@ -125,7 +133,7 @@ def __init__(self): self.metadata_worker.atx_downloaded.connect(self._on_atx_downloaded) self.metadata_thread.start() else: - Logger.terminal("⚠️ Skipping metadata download - running in offline mode") + Logger.workflow("⚠️ Skipping metadata download - running in offline mode") # Added: wire an optional stop-all button if present in the UI if hasattr(self.ui, "stopAllButton") and self.ui.stopAllButton: @@ -133,14 +141,70 @@ def __init__(self): elif hasattr(self.ui, "btnStopAll") and self.ui.btnStopAll: self.ui.btnStopAll.clicked.connect(self.on_stopAllClicked) - def log_message(self, msg: str, channel = "terminal"): - """Append a log line to the specified text channel""" - if channel == "terminal": - self.ui.terminalTextEdit.append(msg) - elif channel == "console": - self.ui.consoleTextEdit.append(msg) - else: - raise ValueError("[MainWindow] Invalid channel for log_message") + def _fix_macos_tab_styling(self): + """ + Fix tab widget styling on macOS where native styling overrides custom stylesheets. + This method applies a comprehensive stylesheet directly to the QTabBar to ensure + consistent appearance across all platforms. + """ + import platform + + # On macOS, we need to be more aggressive with styling to override native appearance + if platform.system() == "Darwin": + # Import QStyleFactory to optionally force Fusion style + from PySide6.QtWidgets import QStyleFactory + + # Force Fusion style on the tab widget to disable native macOS rendering + fusion_style = QStyleFactory.create("Fusion") + if fusion_style: + self.ui.logTabWidget.setStyle(fusion_style) + self.ui.configTabWidget.setStyle(fusion_style) + + # Apply comprehensive stylesheet to ensure consistent appearance + tab_bar_stylesheet = """ + QTabWidget::pane { + border: none; + background-color: #2c5d7c; + } + + QTabBar { + background-color: transparent; + alignment: left; + } + + QTabBar::tab { + background-color: #1a3a4d; + color: white; + padding: 8px 16px; + margin-right: 2px; + border: none; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + min-width: 60px; + } + + QTabBar::tab:selected { + background-color: #2c5d7c; + color: white; + font-weight: bold; + } + + QTabBar::tab:hover:!selected { + background-color: #234a5f; + } + + QTabBar::tab:!selected { + margin-top: 2px; + } + """ + + # Apply the stylesheet to the tab widget + self.ui.logTabWidget.setStyleSheet(tab_bar_stylesheet) + self.ui.configTabWidget.setStyleSheet(tab_bar_stylesheet) + + #endregion + + #region Processing State def _set_processing_state(self, processing: bool): """Enable / disable UI elements during processing""" @@ -182,6 +246,10 @@ def _set_processing_state(self, processing: bool): self.ui.posCheckbox.setEnabled(enabled) self.ui.gpxCheckbox.setEnabled(enabled) self.ui.traceCheckbox.setEnabled(enabled) + self.ui.snxCheckbox.setEnabled(enabled) + + # "YAML" config tab + self.ui.yamlOverwriteCheckbox.setEnabled(enabled) # Update button text to show processing state if processing: @@ -192,103 +260,9 @@ def _set_processing_state(self, processing: bool): self.ui.processButton.setText("Process") self.setCursor(Qt.CursorShape.ArrowCursor) - def _setup_worker_thread(self, worker, finished_callback, progress_callback=None, thread_attr=None, worker_attr=None): - """ - Helper method to set up a worker in a QThread with standard cleanup. - - Args: - worker: The worker object to run in the thread - finished_callback: Callback to connect to worker.finished signal - progress_callback: Optional callback to connect to worker.progress signal - thread_attr: Optional attribute name to clear when thread finishes - worker_attr: Optional attribute name to clear when thread finishes - - Returns: - tuple: (thread, worker) for storing references - """ - thread = QThread() - worker.moveToThread(thread) + #endregion - # Connect started signal - thread.started.connect(worker.run) - - # Connect finished signal - worker.finished.connect(finished_callback) - - # Connect progress signal if provided - if progress_callback and hasattr(worker, 'progress'): - worker.progress.connect(progress_callback) - - # Connect cleanup signals - worker.finished.connect(thread.quit) - worker.finished.connect(worker.deleteLater) - thread.finished.connect(thread.deleteLater) - - # Clear our references when thread finishes to avoid accessing deleted objects - if thread_attr and worker_attr: - def clear_references(): - if hasattr(self, thread_attr) and getattr(self, thread_attr) is thread: - setattr(self, thread_attr, None) - if hasattr(self, worker_attr) and getattr(self, worker_attr) is worker: - setattr(self, worker_attr, None) - - thread.finished.connect(clear_references) - - return thread, worker - - def _cleanup_thread(self, thread_attr: str, worker_attr: str): - """ - Request cancellation of a running thread and move it to _pending_threads - - Args: - thread_attr: Name of the thread attribute (e.g., 'download_thread') - worker_attr: Name of the worker attribute (e.g., 'download_worker') - """ - worker = getattr(self, worker_attr, None) - thread = getattr(self, thread_attr, None) - - # Try to stop the worker - try: - if worker is not None and hasattr(worker, 'stop'): - worker.stop() - except RuntimeError: - pass # Object already deleted - - # Check if thread is still running (with safety check for deleted objects) - thread_running = False - try: - if thread is not None: - thread_running = thread.isRunning() - except RuntimeError: - pass # C++ object already deleted - - if thread_running: - # Disconnect old signals to prevent callbacks to stale state - try: - worker.finished.disconnect() - if hasattr(worker, 'cancelled'): - worker.cancelled.disconnect() - if hasattr(worker, 'progress'): - worker.progress.disconnect() - except (TypeError, RuntimeError): - pass # Already disconnected or object deleted - - # Keep reference alive until thread actually finishes - old_thread = thread - - def cleanup_old_thread(): - if old_thread in self._pending_threads: - self._pending_threads.remove(old_thread) - - try: - old_thread.finished.connect(cleanup_old_thread) - self._pending_threads.append(old_thread) - except RuntimeError: - pass # Object already deleted - - # Clear current references so new thread can be created - setattr(self, worker_attr, None) - setattr(self, thread_attr, None) + #region Process & Download Workflows def on_files_ready(self, rnx_path: str, out_path: str): self.rnx_file = rnx_path @@ -296,18 +270,18 @@ def on_files_ready(self, rnx_path: str, out_path: str): def _on_process_clicked(self): if not self.rnx_file or not self.output_dir: - Logger.terminal("⚠️ Please select RINEX and output directory first") + Logger.workflow("⚠️ Please select RINEX and output directory first") return # Check if in offline mode if self.offline_mode: - Logger.terminal("⚠️ Cannot process: Ginan-UI is running in offline mode (no internet connection)") + Logger.workflow("⚠️ Cannot process: Ginan-UI is running in offline mode (no internet connection)") self._show_processing_offline_warning() return # Prevent multiple simultaneous processing if self.is_processing: - Logger.terminal("⚠️ Processing already in progress. Please wait...") + Logger.workflow("⚠️ Processing already in progress. Please wait...") return # Lock the "Process" button and set processing state @@ -325,12 +299,12 @@ def _on_process_clicked(self): ) self.last_ppp_selection = current_selection if archive_dir: - Logger.terminal(f"📦 Archived old PPP products → {archive_dir}") + Logger.workflow(f"📦 Archived old PPP products → {archive_dir}") visual_dir = Path(self.output_dir) / "visual" output_archive = archive_old_outputs(Path(self.output_dir), visual_dir) if output_archive: - Logger.terminal(f"📦 Archived old outputs → {output_archive}") + Logger.workflow(f"📦 Archived old outputs → {output_archive}") # List products to be downloaded x = self.inputCtrl.products_df @@ -354,12 +328,12 @@ def _on_process_clicked(self): self.download_thread.setObjectName("ProductDownloadWorker") - Logger.terminal("📡 Starting PPP product downloads...") + Logger.workflow("📡 Starting PPP product downloads...") self.download_thread.start() @Slot(str, int) def _on_download_progress(self, filename: str, percent: int): - """Update progress display in-place at the bottom of the UI terminal.""" + """Update progress display in-place at the bottom of the UI workflow terminal.""" self.download_progress[filename] = percent total_length = 20 @@ -369,109 +343,43 @@ def _on_download_progress(self, filename: str, percent: int): search_pattern = QRegularExpression(f"^{filename[:30]}.+%$") # Work with cursor & doc - cursor = self.ui.terminalTextEdit.textCursor() + cursor = self.ui.workflowTextEdit.textCursor() cursor.movePosition(QTextCursor.End) flags = QTextDocument.FindFlag.FindBackward - found_cursor = self.ui.terminalTextEdit.document().find(search_pattern, cursor, flags) + found_cursor = self.ui.workflowTextEdit.document().find(search_pattern, cursor, flags) - on_latest_5_lines = self.ui.terminalTextEdit.document().blockCount() - found_cursor.blockNumber() <= 5 + on_latest_5_lines = self.ui.workflowTextEdit.document().blockCount() - found_cursor.blockNumber() <= 5 if found_cursor.hasSelection() and on_latest_5_lines: found_cursor.movePosition(QTextCursor.EndOfLine) # Replaces final percent symbol too found_cursor.movePosition(QTextCursor.StartOfLine, QTextCursor.KeepAnchor) found_cursor.removeSelectedText() found_cursor.insertText(output) else: # Make new progress bar - self.ui.terminalTextEdit.setTextCursor(cursor) + self.ui.workflowTextEdit.setTextCursor(cursor) cursor.insertText("\n" + output) cursor.movePosition(QTextCursor.End) - self.ui.terminalTextEdit.setTextCursor(cursor) + self.ui.workflowTextEdit.setTextCursor(cursor) def _on_atx_downloaded(self, filename: str): self.atx_required_for_rnx_extraction = True - Logger.terminal(f"✅ ATX file {filename} installed - ready for RINEX parsing") + Logger.workflow(f"✅ ATX file {filename} installed - ready for RINEX parsing") def _on_metadata_download_finished(self, message): - Logger.terminal(message) + Logger.workflow(message) self.metadata_downloaded = True self.inputCtrl.try_enable_process_button() def _on_download_finished(self, message): - Logger.terminal(message) + Logger.workflow(message) self._start_pea_execution() def _on_download_error(self, msg): - Logger.terminal(f"⚠️ PPP download error: {msg}") + Logger.workflow(f"⚠️ PPP download error: {msg}") self._set_processing_state(False) - def _start_pea_execution(self): - Logger.terminal("⚙️ Starting PEA execution in background...") - - # Clean up any existing PEA thread before starting a new one - self._cleanup_thread('pea_thread', 'pea_worker') - - # Reset stop flag for new execution - self.execution.reset_stop_flag() - - self.pea_worker = PeaExecutionWorker(self.execution) - self.pea_thread, _ = self._setup_worker_thread( - self.pea_worker, - self._on_pea_finished, - thread_attr='pea_thread', - worker_attr='pea_worker' - ) - - self.pea_thread.setObjectName("PeaExecutionWorker") - - self.pea_thread.start() - - def _on_pea_finished(self): - Logger.terminal("✅ PEA processing completed") - show_toast(self, "✅ PEA Processing complete!", 3000) - self._run_visualisation() - self._set_processing_state(False) - - def _on_pea_error(self, msg: str): - Logger.terminal(f"⚠️ PEA execution failed: {msg}") - self._set_processing_state(False) - - def _run_visualisation(self): - all_html_files = [] - - # Check checkbox states to determine which visualisations to generate - pos_enabled = self.ui.posCheckbox.isChecked() if hasattr(self.ui, "posCheckbox") else True - trace_enabled = self.ui.traceCheckbox.isChecked() if hasattr(self.ui, "traceCheckbox") else False + #endregion - # Generate POS plots - if pos_enabled: - try: - Logger.terminal("📊 Generating position plots from PEA output...") - pos_html_files = self.execution.build_pos_plots() - if pos_html_files: - all_html_files.extend(pos_html_files) - else: - Logger.terminal("⚠️ No position plots found") - except Exception as err: - Logger.terminal(f"⚠️ Position plot generation failed: {err}") - - # Generate TRACE residual plots - if trace_enabled: - try: - Logger.terminal("📊 Generating trace residual plots from PEA output...") - trace_html_files = self.execution.build_trace_plots() - if trace_html_files: - all_html_files.extend(trace_html_files) - else: - Logger.terminal("⚠️ No trace plots found") - except Exception as err: - Logger.terminal(f"⚠️ Trace plot generation failed: {err}") - - # Update the visualisation panel with all generated plots (or empty list) - self.visCtrl.set_html_files(all_html_files) - - if not all_html_files: - Logger.terminal("📊 No visualisations generated - selector disabled") - # Clear the web view display - self.ui.webEngineView.setHtml("") + #region CDDIS Credentials def _validate_cddis_credentials_once(self): """ @@ -480,38 +388,38 @@ def _validate_cddis_credentials_once(self): """ ok, where = gui_validate_netrc() if not ok and hasattr(self.ui, "cddisCredentialsButton"): - Logger.terminal("⚠️ No Earthdata credentials. Opening CDDIS Credentials dialog...") + Logger.workflow("⚠️ No Earthdata credentials. Opening CDDIS Credentials dialog...") self.ui.cddisCredentialsButton.click() ok, where = gui_validate_netrc() if not ok: - Logger.terminal(f"❌ Credentials invalid: {where}") + Logger.workflow(f"❌ Credentials invalid: {where}") return - Logger.terminal(f"✅ Earthdata Credentials found: {where}") + Logger.workflow(f"✅ Earthdata Credentials found: {where}") ok_user, email_candidate = get_username_from_netrc() if not ok_user: - Logger.terminal(f"❌ Cannot read username from .netrc: {email_candidate}") + Logger.workflow(f"❌ Cannot read username from .netrc: {email_candidate}") return # Wrap connection test in try-except to handle network errors gracefully try: ok_conn, why = test_cddis_connection() if not ok_conn: - Logger.terminal( + Logger.workflow( f"❌ CDDIS connectivity check failed: {why}. Please verify Earthdata credentials via the CDDIS Credentials dialog" ) self._show_offline_warning("Connection test failed", why) return - Logger.terminal(f"✅ CDDIS connectivity check passed in {why.split(' ')[-2]} seconds") + Logger.workflow(f"✅ CDDIS connectivity check passed in {why.split(' ')[-2]} seconds") # Connection successful - set email write_email(email_candidate) - Logger.terminal(f"✉️ EMAIL set to: {email_candidate}") + Logger.workflow(f"✉️ EMAIL set to: {email_candidate}") except Exception as e: # Network error (no internet, DNS failure, timeout, etc.) error_msg = str(e) - Logger.terminal(f"⚠️ No internet connection detected: {error_msg}") + Logger.workflow(f"⚠️ No internet connection detected: {error_msg}") self._show_offline_warning("No internet connection", error_msg) return @@ -539,9 +447,9 @@ def _show_offline_warning(self, title: str, details: str): msg = QMessageBox(self) msg.setIcon(QMessageBox.Icon.Warning) - msg.setWindowTitle("Ginan-UI - No Internet Connection") + msg.setWindowTitle("Ginan-UI - Failed Internet / Server Connection") msg.setText( - "Ginan-UI requires internet access to function properly

" + "Ginan-UI requires internet access to NASA's CDDIS server function properly. Please ensure your device has a stable internet connection, and your CDDIS credentials are correct

" "The following features will be unavailable:" ) msg.setInformativeText( @@ -561,10 +469,207 @@ def _show_offline_warning(self, title: str, details: str): # Also show a toast notification show_toast(self, "⚠️ Running in offline mode - limited functionality", 8000) + #endregion + + #region PEA Execution + + def _start_pea_execution(self): + Logger.workflow("⚙️ Starting PEA execution in background...") + + # Clean up any existing PEA thread before starting a new one + self._cleanup_thread('pea_thread', 'pea_worker') + + # Reset stop flag for new execution + self.execution.reset_stop_flag() + + self.pea_worker = PeaExecutionWorker(self.execution) + self.pea_thread, _ = self._setup_worker_thread( + self.pea_worker, + self._on_pea_finished, + thread_attr='pea_thread', + worker_attr='pea_worker' + ) + + self.pea_thread.setObjectName("PeaExecutionWorker") + + self.pea_thread.start() + + def _on_pea_finished(self): + Logger.workflow("✅ PEA processing completed") + show_toast(self, "✅ PEA Processing complete!", 3000) + self._run_visualisation() + self._set_processing_state(False) + + def _on_pea_error(self, msg: str): + Logger.workflow(f"⚠️ PEA execution failed: {msg}") + self._set_processing_state(False) + + #endregion + + #region Worker Thread Management + + def _setup_worker_thread(self, worker, finished_callback, progress_callback=None, thread_attr=None, worker_attr=None): + """ + Helper method to set up a worker in a QThread with standard cleanup. + + Args: + worker: The worker object to run in the thread + finished_callback: Callback to connect to worker.finished signal + progress_callback: Optional callback to connect to worker.progress signal + thread_attr: Optional attribute name to clear when thread finishes + worker_attr: Optional attribute name to clear when thread finishes + + Returns: + tuple: (thread, worker) for storing references + """ + thread = QThread() + worker.moveToThread(thread) + + # Connect started signal + thread.started.connect(worker.run) + + # Connect finished signal + worker.finished.connect(finished_callback) + + # Connect progress signal if provided + if progress_callback and hasattr(worker, 'progress'): + worker.progress.connect(progress_callback) + + # Connect cleanup signals + worker.finished.connect(thread.quit) + worker.finished.connect(worker.deleteLater) + thread.finished.connect(thread.deleteLater) + + # Clear our references when thread finishes to avoid accessing deleted objects + if thread_attr and worker_attr: + def clear_references(): + if hasattr(self, thread_attr) and getattr(self, thread_attr) is thread: + setattr(self, thread_attr, None) + if hasattr(self, worker_attr) and getattr(self, worker_attr) is worker: + setattr(self, worker_attr, None) + + thread.finished.connect(clear_references) + + return thread, worker + + def _cleanup_thread(self, thread_attr: str, worker_attr: str): + """ + Request cancellation of a running thread and move it to _pending_threads + + Args: + thread_attr: Name of the thread attribute (e.g., 'download_thread') + worker_attr: Name of the worker attribute (e.g., 'download_worker') + """ + worker = getattr(self, worker_attr, None) + thread = getattr(self, thread_attr, None) + + # Try to stop the worker + try: + if worker is not None and hasattr(worker, 'stop'): + worker.stop() + except RuntimeError: + pass # Object already deleted + + # Check if thread is still running (with safety check for deleted objects) + thread_running = False + try: + if thread is not None: + thread_running = thread.isRunning() + except RuntimeError: + pass # C++ object already deleted + + if thread_running: + # Disconnect old signals to prevent callbacks to stale state + try: + worker.finished.disconnect() + if hasattr(worker, 'cancelled'): + worker.cancelled.disconnect() + if hasattr(worker, 'progress'): + worker.progress.disconnect() + except (TypeError, RuntimeError): + pass # Already disconnected or object deleted + + # Keep reference alive until thread actually finishes + old_thread = thread + + def cleanup_old_thread(): + if old_thread in self._pending_threads: + self._pending_threads.remove(old_thread) + + try: + old_thread.finished.connect(cleanup_old_thread) + self._pending_threads.append(old_thread) + except RuntimeError: + pass # Object already deleted + + # Clear current references so new thread can be created + setattr(self, worker_attr, None) + setattr(self, thread_attr, None) + + #endregion + + #region Plotting Visualisation + + def _run_visualisation(self): + all_html_files = [] + + # Check checkbox states to determine which visualisations to generate + pos_enabled = self.ui.posCheckbox.isChecked() if hasattr(self.ui, "posCheckbox") else True + trace_enabled = self.ui.traceCheckbox.isChecked() if hasattr(self.ui, "traceCheckbox") else False + + # Generate POS plots + if pos_enabled: + try: + Logger.workflow("📊 Generating position plots from PEA output...") + pos_html_files = self.execution.build_pos_plots() + if pos_html_files: + all_html_files.extend(pos_html_files) + else: + Logger.workflow("⚠️ No position plots found") + except Exception as err: + Logger.workflow(f"⚠️ Position plot generation failed: {err}") + + # Generate TRACE residual plots + if trace_enabled: + try: + Logger.workflow("📊 Generating trace residual plots from PEA output...") + trace_html_files = self.execution.build_trace_plots() + if trace_html_files: + all_html_files.extend(trace_html_files) + else: + Logger.workflow("⚠️ No trace plots found") + except Exception as err: + Logger.workflow(f"⚠️ Trace plot generation failed: {err}") + + # Update the visualisation panel with all generated plots (or empty list) + self.visCtrl.set_html_files(all_html_files) + + if not all_html_files: + Logger.workflow("📊 No visualisations generated - selector disabled") + # Clear the web view display + self.ui.webEngineView.setHtml("") + + #endregion + + #region Logging + + def log_message(self, msg: str, channel = "workflow"): + """Append a log line to the specified text channel""" + if channel == "workflow": + self.ui.workflowTextEdit.append(msg) + elif channel == "console": + self.ui.consoleTextEdit.append(msg) + else: + raise ValueError("[MainWindow] Invalid channel for log_message") + + #endregion + + #region Stop & Cleanup + # Added: unified stop entry, wired to an optional UI button @Slot() def on_stopAllClicked(self): - Logger.terminal("🛑 Stop requested - stopping all running tasks...") + Logger.workflow("🛑 Stop requested - stopping all running tasks...") # Stop the metadata worker in InputController, if present try: @@ -616,63 +721,4 @@ def closeEvent(self, event): # Accept the close event event.accept() - def _fix_macos_tab_styling(self): - """ - Fix tab widget styling on macOS where native styling overrides custom stylesheets. - This method applies a comprehensive stylesheet directly to the QTabBar to ensure - consistent appearance across all platforms. - """ - import platform - - # On macOS, we need to be more aggressive with styling to override native appearance - if platform.system() == "Darwin": - # Import QStyleFactory to optionally force Fusion style - from PySide6.QtWidgets import QStyleFactory - - # Force Fusion style on the tab widget to disable native macOS rendering - fusion_style = QStyleFactory.create("Fusion") - if fusion_style: - self.ui.logTabWidget.setStyle(fusion_style) - self.ui.configTabWidget.setStyle(fusion_style) - - # Apply comprehensive stylesheet to ensure consistent appearance - tab_bar_stylesheet = """ - QTabWidget::pane { - border: none; - background-color: #2c5d7c; - } - - QTabBar { - background-color: transparent; - alignment: left; - } - - QTabBar::tab { - background-color: #1a3a4d; - color: white; - padding: 8px 16px; - margin-right: 2px; - border: none; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - min-width: 60px; - } - - QTabBar::tab:selected { - background-color: #2c5d7c; - color: white; - font-weight: bold; - } - - QTabBar::tab:hover:!selected { - background-color: #234a5f; - } - - QTabBar::tab:!selected { - margin-top: 2px; - } - """ - - # Apply the stylesheet to the tab widget - self.ui.logTabWidget.setStyleSheet(tab_bar_stylesheet) - self.ui.configTabWidget.setStyleSheet(tab_bar_stylesheet) \ No newline at end of file + #endregion \ No newline at end of file diff --git a/scripts/GinanUI/app/models/archive_manager.py b/scripts/GinanUI/app/models/archive_manager.py index c457b1d6f..5735c8af7 100644 --- a/scripts/GinanUI/app/models/archive_manager.py +++ b/scripts/GinanUI/app/models/archive_manager.py @@ -1,83 +1,20 @@ -# app/utils/archive_manager.py +""" +Manages archival and restoration of GNSS product files. + +Provides functions to move product files (SP3, CLK, BIA, etc.) into timestamped +archive subfolders, restore previously archived products to avoid re-downloading, +and archive old output files before a new processing run +""" from pathlib import Path import shutil import os from datetime import datetime from typing import Optional, Dict, Any - from scripts.GinanUI.app.utils.common_dirs import INPUT_PRODUCTS_PATH - from scripts.GinanUI.app.utils.logger import Logger - -def archive_old_outputs(output_dir: Path, visual_dir: Path = None): - """ - Moves existing output files to an archive directory to keep the workspace clean. - - THIS FUNCTION LOOKS FOR ALL TXT, LOG, JSON, POS, GPX, TRACE files. - DON'T USE THE INPUT PRODUCTS DIRECTORY. - - :param output_dir: Path to the user-selected output directory. - :param visual_dir: Optional path to associated visualisation directory. - """ - # Move visual folder contents if it exists (visual_dir is typically output_dir / "visual") - if visual_dir is None: - visual_dir = output_dir / "visual" - - # First, collect all files that would be archived (only .POS, .GPX, .TRACE) - files_to_archive = [] - for ext in [".pos", ".POS", ".gpx", ".GPX", ".trace", ".TRACE"]: - files_to_archive.extend(list(output_dir.glob(f"*{ext}"))) - - # Check visual directory for files - visual_files_to_archive = [] - if visual_dir.exists() and visual_dir.is_dir(): - visual_files_to_archive = [f for f in visual_dir.glob("*") if f.is_file()] - - # Only proceed if there's something to archive - if not files_to_archive and not visual_files_to_archive: - Logger.console("📂 No previous outputs found to archive.") - return - - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - archive_dir = output_dir / "archive" / timestamp - - # Make the visual directory - os.makedirs(archive_dir, exist_ok=True) - - # Move .pos, .gpx, .trace from output_dir - moved_files = 0 - for file in files_to_archive: - try: - shutil.move(str(file), str(archive_dir / file.name)) - moved_files += 1 - except Exception as e: - Logger.console(f"Failed to archive {file.name}: {e}") - - # Move visual folder contents - if visual_files_to_archive: - visual_archive = archive_dir / "visual" - - # Make the visual archive directory - os.makedirs(visual_archive, exist_ok=True) - - for visual_file in visual_files_to_archive: - try: - shutil.move(str(visual_file), str(visual_archive / visual_file.name)) - moved_files += 1 - except Exception as e: - Logger.console(f"Failed to archive {visual_file.name}: {e}") - # Remove the now-empty visual archive directory - try: - visual_dir.rmdir() - except OSError: - pass # Directory not empty or other issue - - if moved_files > 0: - Logger.console(f"📦 Archived {moved_files} old output file(s) to: {archive_dir}") - else: - Logger.console("📂 No previous outputs found to archive.") +#region Product Archival def archive_products(products_dir: Path = INPUT_PRODUCTS_PATH, reason: str = "manual", startup_archival: bool = False, include_patterns: Optional[list[str]] = None) -> Optional[Path]: @@ -105,6 +42,8 @@ def archive_products(products_dir: Path = INPUT_PRODUCTS_PATH, reason: str = "ma "*.sp3.Z", # precise orbit (old format) "*.clk.Z", # clock files (old format) "*.bia.Z", # biases (old format) + "*_ocean.BLQ", # generated ocean tide loading + "*_atmos.BLQ", # generated atmospheric tide loading ] # Uncompressed counterparts to clean up after archiving the compressed versions @@ -134,7 +73,7 @@ def archive_products(products_dir: Path = INPUT_PRODUCTS_PATH, reason: str = "ma "tables/ALOAD*", "tables/OLOAD*", "tables/gpt_*.grd", - "tables/qzss_*" + "tables/qzss_*", "tables/igrf*coeffs.txt", "tables/DE436*", "tables/fes2014*.dat", @@ -206,11 +145,117 @@ def archive_products(products_dir: Path = INPUT_PRODUCTS_PATH, reason: str = "ma Logger.console("No matching product files found to archive.") return None +def archive_old_outputs(output_dir: Path, visual_dir: Path = None): + """ + Moves existing output files to an archive directory to keep the workspace clean. + + THIS FUNCTION LOOKS FOR ALL TXT, LOG, JSON, POS, GPX, TRACE, SNX files. + DON'T USE THE INPUT PRODUCTS DIRECTORY. + + :param output_dir: Path to the user-selected output directory. + :param visual_dir: Optional path to associated visualisation directory. + """ + # Move visual folder contents if it exists (visual_dir is typically output_dir / "visual") + if visual_dir is None: + visual_dir = output_dir / "visual" + + # First, collect all files that would be archived (only .POS, .GPX, .TRACE, .SNX) + files_to_archive = [] + for ext in [".pos", ".POS", ".gpx", ".GPX", ".trace", ".TRACE", ".snx", ".SNX"]: + files_to_archive.extend(list(output_dir.glob(f"*{ext}"))) + + # Check visual directory for files + visual_files_to_archive = [] + if visual_dir.exists() and visual_dir.is_dir(): + visual_files_to_archive = [f for f in visual_dir.glob("*") if f.is_file()] + + # Only proceed if there's something to archive + if not files_to_archive and not visual_files_to_archive: + Logger.console("📂 No previous outputs found to archive.") + return + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + archive_dir = output_dir / "archive" / timestamp + + # Make the visual directory + os.makedirs(archive_dir, exist_ok=True) + + # Move .pos, .gpx, .trace from output_dir + moved_files = 0 + for file in files_to_archive: + try: + shutil.move(str(file), str(archive_dir / file.name)) + moved_files += 1 + except Exception as e: + Logger.console(f"Failed to archive {file.name}: {e}") + + # Move visual folder contents + if visual_files_to_archive: + visual_archive = archive_dir / "visual" + + # Make the visual archive directory + os.makedirs(visual_archive, exist_ok=True) + + for visual_file in visual_files_to_archive: + try: + shutil.move(str(visual_file), str(visual_archive / visual_file.name)) + moved_files += 1 + except Exception as e: + Logger.console(f"Failed to archive {visual_file.name}: {e}") + # Remove the now-empty visual archive directory + try: + visual_dir.rmdir() + except OSError: + pass # Directory not empty or other issue + + if moved_files > 0: + Logger.console(f"📦 Archived {moved_files} old output file(s) to: {archive_dir}") + else: + Logger.console("📂 No previous outputs found to archive.") + + +def archive_products_if_rinex_changed(current_rinex: Path, last_rinex: Optional[Path], products_dir: Path) -> Optional[Path]: + """ + If the RINEX file has changed since last load, archive the cached products. + """ + if last_rinex and current_rinex.resolve() == last_rinex.resolve(): + Logger.console("RINEX file unchanged, skipping product cleanup.") + return None + + Logger.console("RINEX file changed, archiving old products.") + # Shouldn't remove BRDC if date isn't changed but would require extracting current and last rnx + return archive_products(products_dir, reason="rinex_change") + + +def archive_products_if_selection_changed(current_selection: Dict[str, Any], last_selection: Optional[Dict[str, Any]], + products_dir: Path) -> Optional[Path]: + """ + If the PPP product selection (AC/project/solution) has changed, archive the cached products. + Excludes BRDC and finals.data.iau2000.txt since they are reusable. + """ + if last_selection and current_selection == last_selection: + Logger.console("[Archiver] PPP product selection unchanged, skipping product cleanup.") + return None + + if last_selection: + diffs = {k: (last_selection.get(k), current_selection.get(k)) + for k in set(last_selection) | set(current_selection) + if last_selection.get(k) != current_selection.get(k)} + Logger.console(f"[Archiver] PPP selection changed → differences: {diffs}") + + return archive_products(products_dir,reason="ppp_selection_change") + +#endregion + +#region Product Restoration From Archive # File extensions eligible for archive restoration (dynamic PPP products only) RESTORABLE_EXTENSIONS = {".SP3", ".BIA", ".CLK", ".SNX", ".RNX"} # ".RNX" is for BRDC files RESTORABLE_CHECKSUM_PREFIX = "SHA512SUMS" +# Static products that happen to share a restorable extension but should not be restored like dynamic products +STATIC_PRODUCT_PREFIXES = {"igs_satellite_metadata"} + def restore_from_archive(filename: str, products_dir: Path = INPUT_PRODUCTS_PATH) -> Optional[Path]: """ Search the archive directory for a previously archived product file and restore it @@ -235,6 +280,9 @@ def restore_from_archive(filename: str, products_dir: Path = INPUT_PRODUCTS_PATH # Determine if this file is eligible for restoration is_checksum = filename.startswith(RESTORABLE_CHECKSUM_PREFIX) if not is_checksum: + # Skip static products that share a restorable extension (e.g. igs_satellite_metadata.snx) + if any(filename.startswith(prefix) for prefix in STATIC_PRODUCT_PREFIXES): + return None # Check if the uncompressed extension is one we care about bare_name = filename.removesuffix(".gz").removesuffix(".Z").removesuffix(".gzip") ext = Path(bare_name).suffix.upper() @@ -268,37 +316,4 @@ def restore_from_archive(filename: str, products_dir: Path = INPUT_PRODUCTS_PATH return None -def archive_products_if_rinex_changed(current_rinex: Path, - last_rinex: Optional[Path], - products_dir: Path) -> Optional[Path]: - """ - If the RINEX file has changed since last load, archive the cached products. - """ - if last_rinex and current_rinex.resolve() == last_rinex.resolve(): - Logger.console("RINEX file unchanged, skipping product cleanup.") - return None - - Logger.console("RINEX file changed, archiving old products.") - # Shouldn't remove BRDC if date isn't changed but would require extracting current and last rnx - return archive_products(products_dir, reason="rinex_change") - - -def archive_products_if_selection_changed(current_selection: Dict[str, Any], - last_selection: Optional[Dict[str, Any]], - products_dir: Path) -> Optional[Path]: - """ - If the PPP product selection (AC/project/solution) has changed, archive the cached products. - Excludes BRDC and finals.data.iau2000.txt since they are reusable. - """ - if last_selection and current_selection == last_selection: - Logger.console("[Archiver] PPP product selection unchanged, skipping product cleanup.") - return None - - if last_selection: - diffs = {k: (last_selection.get(k), current_selection.get(k)) - for k in set(last_selection) | set(current_selection) - if last_selection.get(k) != current_selection.get(k)} - Logger.console(f"[Archiver] PPP selection changed → differences: {diffs}") - - return archive_products(products_dir,reason="ppp_selection_change") - +#endregion diff --git a/scripts/GinanUI/app/models/dl_products.py b/scripts/GinanUI/app/models/dl_products.py index 77f79292d..4dd78d0fe 100644 --- a/scripts/GinanUI/app/models/dl_products.py +++ b/scripts/GinanUI/app/models/dl_products.py @@ -1,29 +1,45 @@ -import gzip, hashlib, os, shutil, unlzw3, requests -import pandas as pd +""" +GNSS product downloading and validation for Ginan-UI. + +Handles all interaction with the CDDIS archive to fetch PPP products (SP3, CLK, +BIA, ION, TRO), broadcast ephemeris (BRDC), static metadata, and IGS CRD SINEX +files. Also provides functions to enumerate valid analysis centres for a given +date range, retrieve constellation support from SP3 headers, parse BIA code +priorities, and validate downloaded files against SHA512 checksums. +""" + +import gzip +import hashlib +import os +import shutil import numpy as np +import pandas as pd +import requests +import unlzw3 from bs4 import BeautifulSoup, SoupStrainer from datetime import datetime, timedelta from pathlib import Path -from typing import Optional, Callable, Dict, Generator, List - -from scripts.GinanUI.app.utils.cddis_email import get_netrc_auth -from scripts.GinanUI.app.utils.common_dirs import INPUT_PRODUCTS_PATH +from typing import Callable, Dict, Generator, List, Optional +from scripts.GinanUI.app.utils.cddis_connection import get_netrc_auth +from scripts.GinanUI.app.utils.common_dirs import INPUT_PRODUCTS_PATH, TABLES_PRODUCTS_PATH from scripts.GinanUI.app.utils.gn_functions import GPSDate from scripts.GinanUI.app.utils.logger import Logger from scripts.GinanUI.app.models.archive_manager import restore_from_archive +# Constants BASE_URL = "https://cddis.nasa.gov/archive" GPS_ORIGIN = np.datetime64("1980-01-06 00:00:00") # Magic date from gn_functions MAX_RETRIES = 3 # download attempts CHUNK_SIZE = 8192 # 8 KiB COMPRESSED_FILETYPE = (".gz", ".gzip", ".Z") # ignore any others (maybe add crx2rnx using hatanaka package) -# repro3 fallback constants +# repro3/ fallback constants REPRO3_PROJECT = "R03" # Project code for reproduction products REPRO3_4TH_CHAR_RANGE = range(10) # 4th character can be 0-9, prioritise lower numbers REPRO3_PRIORITY_GPS_WEEK_START = 730 # Start of GPS week range where repro3 products are better quality REPRO3_PRIORITY_GPS_WEEK_END = 2138 # End of GPS week range where repro3 products are better quality +# The five valid constellations used within Ginan-UI CONSTELLATION_MAP = { 'G': 'GPS', 'R': 'GLO', @@ -32,6 +48,13 @@ 'J': 'QZS', } +# Loading grid netCDF files for computing ocean and atmospheric tide loading BLQ files +LOADING_GRID_URLS = [ + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/loading/oceantide.nc", + "https://peanpod.s3.ap-southeast-2.amazonaws.com/aux/loading/atmtide.nc", +] + +# Static metadata product files that are always required METADATA = [ "https://files.igs.org/pub/station/general/igs_satellite_metadata.snx", "https://files.igs.org/pub/station/general/igs20.atx", @@ -48,16 +71,15 @@ "https://datacenter.iers.org/data/latestVersion/finals.data.iau2000.txt" ] - +# Checksum constants CHECKSUM_FILENAME = "SHA512SUMS" -# File types that should be validated against SHA512SUMS -CHECKSUM_VALIDATED_FORMATS = {"SP3", "BIA", "CLK", "SNX"} +CHECKSUM_VALIDATED_FORMATS = {"SP3", "BIA", "CLK", "SNX"} # File types that should be validated against SHA512SUMS +#region Helper Functions def date_to_gpswk(date: datetime) -> int: return int(GPSDate(np.datetime64(date)).gpswk) - def gpswk_to_date(gps_week: int, gps_day: int = 0) -> datetime: return GPSDate(GPS_ORIGIN + np.timedelta64(gps_week, "W") + np.timedelta64(gps_day, "D")).as_datetime @@ -97,6 +119,66 @@ def _is_in_repro3_priority_range(start_time: datetime, end_time: datetime) -> bo return (REPRO3_PRIORITY_GPS_WEEK_START <= start_week <= REPRO3_PRIORITY_GPS_WEEK_END and REPRO3_PRIORITY_GPS_WEEK_START <= end_week <= REPRO3_PRIORITY_GPS_WEEK_END) +def filter_minimum_covering_products(products: pd.DataFrame, start_time: datetime, end_time: datetime) -> pd.DataFrame: + """ + For each (analysis_center, solution_type, project, format) group, retain only the + minimum set of non-overlapping files needed to cover [start_time, end_time]. + + This is particularly important for NRT products which are published + hourly, this causes the broad overlap filter in get_product_dataframe() + to return far more files than are actually required. + + Strategy: starting from the latest file whose start time is at or before start_time, + greedily pick the file that extends coverage the furthest forward, repeating until + end_time is covered. + + :param products: products dataframe from get_product_dataframe() + :param start_time: the start of the required time window + :param end_time: the end of the required time window + :returns: filtered dataframe containing only the minimum required files + """ + if products.empty: + return products + + kept_indices = [] + + for _keys, group in products.groupby(["analysis_center", "solution_type", "project", "format"]): + group = group.sort_values("date").reset_index() + + coverage_reached = start_time + + while coverage_reached < end_time: + # Candidates: files whose nominal coverage window overlaps the remaining uncovered range + candidates = group[ + (group["date"] <= coverage_reached) & + (group["date"] + group["period"] > coverage_reached) + ] + + if candidates.empty: + # No file starts at or before coverage_reached - fall back to the earliest file + # that starts after it (gap in available products) + candidates = group[group["date"] > coverage_reached] + if candidates.empty: + break # No more files available at all + + # Among candidates, pick the one whose end extends furthest forward + candidates = candidates.copy() + candidates["_product_end"] = candidates["date"] + candidates["period"] + best = candidates.loc[candidates["_product_end"].idxmax()] + + new_coverage = best["_product_end"] + if new_coverage <= coverage_reached: + # No forward progress possible - avoid infinite loop + break + + kept_indices.append(best["index"]) + coverage_reached = new_coverage + + if not kept_indices: + # Fallback: return original if something went wrong (e.g. no overlap at all found) + return products + + return products.loc[products.index.isin(kept_indices)].reset_index(drop=True) def get_repro3_product_dataframe(start_time: datetime, end_time: datetime, target_files: List[str] = None) -> pd.DataFrame: """ @@ -193,8 +275,10 @@ def get_repro3_product_dataframe(start_time: datetime, end_time: datetime, targe products = products.drop(columns=["_4th_char"]) products = products.reset_index(drop=True) - return products + # Reduce to the minimum set of files that cover the requested time window + products = filter_minimum_covering_products(products, start_time, end_time) + return products def str_to_datetime(date_time_str): """ @@ -207,7 +291,6 @@ def str_to_datetime(date_time_str): except ValueError: raise ValueError("Invalid datetime format. Use YYYY-MM-DDTHH:MM (e.g. 2025-05-01_00:00:00)") - def get_product_dataframe(start_time: datetime, end_time: datetime, target_files: List[str] = None) -> pd.DataFrame: """ Retrieves a DataFrame of available products for given time window and target files from CDDIS archive. @@ -309,8 +392,14 @@ def get_product_dataframe(start_time: datetime, end_time: datetime, target_files products = products.drop_duplicates(subset=["analysis_center", "project", "date", "solution_type", "format"], keep="first") products = products.reset_index(drop=True) + # Reduce to the minimum set of files that cover the requested time window + products = filter_minimum_covering_products(products, start_time, end_time) + return products +#endregion + +#region Retrieve Valid Product Information def get_valid_analysis_centers(data: pd.DataFrame) -> set[str]: """ @@ -385,7 +474,6 @@ def get_valid_analysis_centers(data: pd.DataFrame) -> set[str]: return centers - def get_valid_series_for_provider(data: pd.DataFrame, provider: str) -> List[str]: """ Get list of valid series (with all required files) for a specific provider. @@ -446,7 +534,6 @@ def get_product_dataframe_with_repro3_fallback(start_time: datetime, end_time: d # Outside priority range: use main directory first, fallback to repro3 if needed return _try_main_first(start_time, end_time, target_files) - def _try_main_first(start_time: datetime, end_time: datetime, target_files: List[str] = None) -> pd.DataFrame: """ Try main directory first, fallback to repro3 if no valid PPP providers found. @@ -461,7 +548,7 @@ def _try_main_first(start_time: datetime, end_time: datetime, target_files: List products = get_product_dataframe(start_time, end_time, target_files) if products.empty: - Logger.terminal("📦 No products found in main directory, checking /repro3/...") + Logger.workflow("📦 No products found in main directory, checking /repro3/...") return _try_repro3_fallback(start_time, end_time, target_files) # Check if we have valid PPP providers @@ -472,10 +559,9 @@ def _try_main_first(start_time: datetime, end_time: datetime, target_files: List return products # No valid PPP providers found, try repro3 fallback - Logger.terminal("📦 No valid PPP providers in main directory, checking /repro3/...") + Logger.workflow("📦 No valid PPP providers in main directory, checking /repro3/...") return _try_repro3_fallback(start_time, end_time, target_files, main_products=products) - def _try_repro3_first(start_time: datetime, end_time: datetime, target_files: List[str] = None) -> pd.DataFrame: """ Try /repro3/ directory first @@ -500,14 +586,14 @@ def _try_repro3_first(start_time: datetime, end_time: datetime, target_files: Li break if not repro3_exists_for_any_week: - Logger.terminal("📦 /repro3/ directory does not exist, falling back to main directory...") + Logger.workflow("📦 /repro3/ directory does not exist, falling back to main directory...") return _try_main_directory_fallback(start_time, end_time, target_files) # Fetch products from repro3 repro3_products = get_repro3_product_dataframe(start_time, end_time, target_files) if repro3_products.empty: - Logger.terminal("📦 No products found in /repro3/ directory, falling back to main directory...") + Logger.workflow("📦 No products found in /repro3/ directory, falling back to main directory...") return _try_main_directory_fallback(start_time, end_time, target_files) # Check if repro3 has valid PPP providers @@ -517,10 +603,9 @@ def _try_repro3_first(start_time: datetime, end_time: datetime, target_files: Li return repro3_products # No valid providers in repro3, try main directory as fallback - Logger.terminal("📦 No valid PPP providers in /repro3/, falling back to main directory...") + Logger.workflow("📦 No valid PPP providers in /repro3/, falling back to main directory...") return _try_main_directory_fallback(start_time, end_time, target_files, repro3_products=repro3_products) - def _try_main_directory_fallback(start_time: datetime, end_time: datetime, target_files: List[str] = None, repro3_products: pd.DataFrame = None) -> pd.DataFrame: """ Internal helper to attempt fetching products from main directory as a fallback @@ -536,25 +621,24 @@ def _try_main_directory_fallback(start_time: datetime, end_time: datetime, targe if products.empty: if repro3_products is not None and not repro3_products.empty: - Logger.terminal("⚠️ No valid PPP providers found") + Logger.workflow("⚠️ No valid PPP providers found") return repro3_products - Logger.terminal("⚠️ No valid PPP providers found") + Logger.workflow("⚠️ No valid PPP providers found") return pd.DataFrame() # Check if main directory has valid PPP providers valid_centers = get_valid_analysis_centers(products) if valid_centers: - Logger.terminal(f"✅ Found valid PPP providers in main directory: {', '.join(sorted(valid_centers))}") + Logger.workflow(f"✅ Found valid PPP providers in main directory: {', '.join(sorted(valid_centers))}") return products # No valid providers in main either - Logger.terminal("⚠️ No valid PPP providers found") + Logger.workflow("⚠️ No valid PPP providers found") if repro3_products is not None and not repro3_products.empty: return repro3_products return products - def _try_repro3_fallback(start_time: datetime, end_time: datetime, target_files: List[str] = None, main_products: pd.DataFrame = None) -> pd.DataFrame: """ Internal helper to attempt fetching products from repro3 directory. @@ -579,9 +663,9 @@ def _try_repro3_fallback(start_time: datetime, end_time: datetime, target_files: break if not repro3_exists_for_any_week: - Logger.terminal("📦 repro3 directory does not exist for this time range") + Logger.workflow("📦 repro3 directory does not exist for this time range") if main_products is not None and not main_products.empty: - Logger.terminal("⚠️ No valid PPP providers found") + Logger.workflow("⚠️ No valid PPP providers found") return main_products return pd.DataFrame() @@ -589,9 +673,9 @@ def _try_repro3_fallback(start_time: datetime, end_time: datetime, target_files: repro3_products = get_repro3_product_dataframe(start_time, end_time, target_files) if repro3_products.empty: - Logger.terminal("📦 No products found in repro3 directory") + Logger.workflow("📦 No products found in repro3 directory") if main_products is not None and not main_products.empty: - Logger.terminal("⚠️ No valid PPP providers found") + Logger.workflow("⚠️ No valid PPP providers found") return main_products return pd.DataFrame() @@ -601,35 +685,17 @@ def _try_repro3_fallback(start_time: datetime, end_time: datetime, target_files: if repro3_valid_centers: return repro3_products else: - Logger.terminal("⚠️ No valid PPP providers found") + Logger.workflow("⚠️ No valid PPP providers found") if main_products is not None and not main_products.empty: return main_products return repro3_products -def extract_file(filepath: Path, keep_compressed: bool = True) -> Path: - """ - Extracts [".gz", ".gzip", ".Z"] files with gzip and unlzw3 respectively. - By default, the compressed file is retained alongside the extracted version - so that it can be archived and later validated against SHA-512 checksums. - - :param filepath: compressed file path - :param keep_compressed: if True, retain the compressed file after extraction - :return: path to extracted file - """ - finalpath = ".".join(str(filepath).split(".")[:-1]) - if str(filepath.name).endswith((".gz", ".gzip")): - with gzip.open(filepath, "rb") as f_in, open(finalpath, "wb") as f_out: - shutil.copyfileobj(f_in, f_out) - elif str(filepath.name).endswith(".Z"): - decompressed_data = unlzw3.unlzw(filepath) - with open(finalpath, "wb") as f_out: - f_out.write(decompressed_data) - if not keep_compressed: - filepath.unlink() - return Path(finalpath) +#endregion +#region SHA-512 Checksum Validation -# region SHA-512 Checksum Validation +# Cache of downloaded checksum files: (gps_week, use_repro3) -> parsed checksums dict +_checksum_cache: Dict[tuple, dict] = {} def get_checksum_url(gps_week: int, use_repro3: bool = False) -> str: """ @@ -673,7 +739,7 @@ def download_checksum_file(gps_week: int, session: requests.Session, download_di return restored url = get_checksum_url(gps_week, use_repro3) - Logger.terminal(f"📥 Downloading checksum file {CHECKSUM_FILENAME} for GPS week {gps_week}{' (repro3)' if use_repro3 else ''}...") + Logger.workflow(f"📥 Downloading checksum file {CHECKSUM_FILENAME} for GPS week {gps_week}{' (repro3)' if use_repro3 else ''}...") for attempt in range(MAX_RETRIES): if stop_requested and stop_requested(): @@ -702,14 +768,14 @@ def download_checksum_file(gps_week: int, session: requests.Session, download_di progress_callback(local_filename, percent) os.rename(partial_path, local_path) - Logger.terminal(f"✅ Downloaded checksum file {local_filename}") + Logger.workflow(f"✅ Downloaded checksum file {local_filename}") return local_path except requests.RequestException as e: if attempt < MAX_RETRIES - 1: - Logger.terminal(f"⚠️ Checksum download attempt {attempt + 1}/{MAX_RETRIES} failed: {e}") + Logger.workflow(f"⚠️ Checksum download attempt {attempt + 1}/{MAX_RETRIES} failed: {e}") else: - Logger.terminal(f"⚠️ Failed to download {CHECKSUM_FILENAME} for GPS week {gps_week} after {MAX_RETRIES} attempts: {e}") + Logger.workflow(f"⚠️ Failed to download {CHECKSUM_FILENAME} for GPS week {gps_week} after {MAX_RETRIES} attempts: {e}") return None @@ -737,13 +803,13 @@ def parse_checksum_file(checksum_path: Path) -> dict: try: int(hex_hash, 16) except ValueError: - Logger.terminal(f"⚠️ Invalid hex hash in SHA512SUMS for {parts[1].strip()}, skipping entry") + Logger.workflow(f"⚠️ Invalid hex hash in SHA512SUMS for {parts[1].strip()}, skipping entry") continue checksums[parts[1].strip()] = hex_hash except Exception as e: - Logger.terminal(f"⚠️ Failed to parse checksum file {checksum_path}: {e}") + Logger.workflow(f"⚠️ Failed to parse checksum file {checksum_path}: {e}") if not checksums: - Logger.terminal(f"⚠️ No valid checksum entries found in {checksum_path.name}") + Logger.workflow(f"⚠️ No valid checksum entries found in {checksum_path.name}") return checksums def compute_sha512(filepath: Path) -> str: @@ -771,14 +837,14 @@ def validate_checksum(filepath: Path, checksums: dict) -> Optional[bool]: expected_hash = checksums.get(filename) if expected_hash is None: - Logger.terminal(f"⚠️ No checksum entry found for {filename} in SHA512SUMS (file may be corrupted or incomplete)") + Logger.workflow(f"⚠️ No checksum entry found for {filename} in SHA512SUMS (file may be corrupted or incomplete)") return None # Verify the expected hash is valid hex before comparing try: int(expected_hash, 16) except ValueError: - Logger.terminal(f"⚠️ Invalid checksum hash in SHA512SUMS for {filename}, skipping validation") + Logger.workflow(f"⚠️ Invalid checksum hash in SHA512SUMS for {filename}, skipping validation") return None actual_hash = compute_sha512(filepath) @@ -790,9 +856,6 @@ def validate_checksum(filepath: Path, checksums: dict) -> Optional[bool]: Logger.console(f"❌ Checksum mismatch: {filename} | Expected: {expected_hash[:16]}... Got: {actual_hash[:16]}...") return False -# Cache of downloaded checksum files: (gps_week, use_repro3) -> parsed checksums dict -_checksum_cache: Dict[tuple, dict] = {} - def get_checksums_for_week(gps_week: int, session: requests.Session, download_dir: Path = INPUT_PRODUCTS_PATH, use_repro3: bool = False, progress_callback: Optional[Callable] = None, stop_requested: Optional[Callable] = None) -> Optional[dict]: @@ -824,10 +887,11 @@ def get_checksums_for_week(gps_week: int, session: requests.Session, download_di # endregion +#region Product Downloading from CDDIS Archives + def download_file(url: str, session: requests.Session, download_dir: Path = INPUT_PRODUCTS_PATH, - progress_callback: Optional[Callable] = None, - stop_requested: Callable = None, checksums: Optional[dict] = None, - keep_compressed: bool = True) -> Path: + progress_callback: Optional[Callable] = None, stop_requested: Callable = None, + checksums: Optional[dict] = None, keep_compressed: bool = True) -> Path: """ Checks if file already exists (additionally in compressed or .part forms). Uses provided session for CDDIS files (session made during startup). @@ -854,7 +918,7 @@ def download_file(url: str, session: requests.Session, download_dir: Path = INPU if checksums is not None: result = validate_checksum(filepath, checksums) if result is False: - Logger.terminal(f"⚠️ Existing file {filepath.name} failed checksum, re-downloading...") + Logger.workflow(f"⚠️ Existing file {filepath.name} failed checksum, re-downloading...") filepath.unlink(missing_ok=True) # Fall through to download below else: @@ -877,14 +941,14 @@ def download_file(url: str, session: requests.Session, download_dir: Path = INPU if filepath.exists(): result = validate_checksum(filepath, checksums) if result is False: - Logger.terminal(f"⚠️ Compressed file {filepath.name} failed checksum, re-downloading...") + Logger.workflow(f"⚠️ Compressed file {filepath.name} failed checksum, re-downloading...") filepath.unlink(missing_ok=True) potential_decompressed.unlink(missing_ok=True) # Fall through to download below else: return potential_decompressed else: - Logger.terminal(f"⚠️ Cannot verify checksum for {filepath.name} (compressed file missing), re-downloading to validate...") + Logger.workflow(f"⚠️ Cannot verify checksum for {filepath.name} (compressed file missing), re-downloading to validate...") potential_decompressed.unlink(missing_ok=True) # Fall through to download below else: @@ -897,7 +961,7 @@ def download_file(url: str, session: requests.Session, download_dir: Path = INPU if checksums is not None and filepath.name in checksums: result = validate_checksum(restored, checksums) if result is False: - Logger.terminal(f"⚠️ Archived file {restored.name} failed checksum validation, re-downloading...") + Logger.workflow(f"⚠️ Archived file {restored.name} failed checksum validation, re-downloading...") restored.unlink(missing_ok=True) # Fall through to download below else: @@ -915,11 +979,11 @@ def download_file(url: str, session: requests.Session, download_dir: Path = INPU if _partial.exists(): # Resume partial downloads headers = {"Range": f"bytes={_partial.stat().st_size}-"} - Logger.terminal(f"Resuming download of {filepath.name} from byte {_partial.stat().st_size}") + Logger.workflow(f"Resuming download of {filepath.name} from byte {_partial.stat().st_size}") else: # Download whole file headers = {"Range": "bytes=0-"} - Logger.terminal(f"Starting new download of {filepath.name}") + Logger.workflow(f"Starting new download of {filepath.name}") os.makedirs(_partial.parent, exist_ok=True) # Hack?! for windows error when open(_partial, "wb") not creating new files @@ -963,7 +1027,7 @@ def download_file(url: str, session: requests.Session, download_dir: Path = INPU if checksums is not None: result = validate_checksum(filepath, checksums) if result is False: - Logger.terminal(f"⚠️ Deleting corrupted file {filepath.name} and retrying...") + Logger.workflow(f"⚠️ Deleting corrupted file {filepath.name} and retrying...") filepath.unlink(missing_ok=True) continue @@ -972,29 +1036,31 @@ def download_file(url: str, session: requests.Session, download_dir: Path = INPU else: return filepath except requests.RequestException as e: - Logger.terminal(f"Failed attempt {i} to download {filepath.name}: {e}") + Logger.workflow(f"Failed attempt {i} to download {filepath.name}: {e}") raise (Exception(f"Failed to download {filepath.name} after {MAX_RETRIES} attempts")) - -def get_brdc_urls(start_time: datetime, end_time: datetime) -> list[str]: +def extract_file(filepath: Path, keep_compressed: bool = True) -> Path: """ - Generates a list of BRDC file URLs for the specified date range. + Extracts [".gz", ".gzip", ".Z"] files with gzip and unlzw3 respectively. + By default, the compressed file is retained alongside the extracted version + so that it can be archived and later validated against SHA-512 checksums. - :param start_time: Start of the date range - :param end_time: End of the date range - :returns: List URLs to download BRDC files + :param filepath: compressed file path + :param keep_compressed: if True, retain the compressed file after extraction + :return: path to extracted file """ - urls = [] - reference_dt = start_time - while int((end_time - reference_dt).total_seconds()) > 0: - day = reference_dt.strftime("%j") - filename = f"BRDC00IGS_R_{reference_dt.year}{day}0000_01D_MN.rnx.gz" - url = f"{BASE_URL}/gnss/data/daily/{reference_dt.year}/brdc/{filename}" - urls.append(url) - reference_dt += timedelta(days=1) - return urls - + finalpath = ".".join(str(filepath).split(".")[:-1]) + if str(filepath.name).endswith((".gz", ".gzip")): + with gzip.open(filepath, "rb") as f_in, open(finalpath, "wb") as f_out: + shutil.copyfileobj(f_in, f_out) + elif str(filepath.name).endswith(".Z"): + decompressed_data = unlzw3.unlzw(filepath) + with open(finalpath, "wb") as f_out: + f_out.write(decompressed_data) + if not keep_compressed: + filepath.unlink() + return Path(finalpath) def download_metadata(download_dir: Path = INPUT_PRODUCTS_PATH, progress_callback: Optional[Callable] = None, atx_callback: Optional[Callable] = None): @@ -1012,6 +1078,35 @@ def download_metadata(download_dir: Path = INPUT_PRODUCTS_PATH, if atx_callback and download.name == "igs20.atx": atx_callback(download.name) +def download_loading_grids(download_dir: Path = TABLES_PRODUCTS_PATH, + progress_callback: Optional[Callable] = None, + stop_requested: Optional[Callable] = None): + """ + Download ocean and atmospheric tide loading grid netCDF files if not already present. + + :param download_dir: Directory to save the loading grid files + :param progress_callback: Reports (description, percent) for progress updates + :param stop_requested: Bool callback. Returns early if stop is requested + """ + download_dir.mkdir(parents=True, exist_ok=True) + + _sesh = requests.Session() + + for url in LOADING_GRID_URLS: + if stop_requested and stop_requested(): + return + + filename = url.split("/")[-1] + filepath = download_dir / filename + + if filepath.exists(): + Logger.workflow(f"📁 Loading grid already exists: {filename}") + continue + + Logger.workflow(f"📥 Downloading loading grid: {filename}") + download_file(url, _sesh, download_dir=download_dir, + progress_callback=progress_callback, + stop_requested=stop_requested) def download_products(products: pd.DataFrame, download_dir: Path = INPUT_PRODUCTS_PATH, dl_urls: list = None, progress_callback: Optional[Callable] = None, @@ -1076,7 +1171,7 @@ def download_products(products: pd.DataFrame, download_dir: Path = INPUT_PRODUCT if dl_urls: downloads.extend(dl_urls) - Logger.terminal(f"📦 {len(downloads)} files to check or download") + Logger.workflow(f"📦 {len(downloads)} files to check or download") download_dir.mkdir(parents=True, exist_ok=True) (download_dir / "tables").mkdir(parents=True, exist_ok=True) for url in downloads: @@ -1097,6 +1192,25 @@ def download_products(products: pd.DataFrame, download_dir: Path = INPUT_PRODUCT is_tables = (fin_dir != download_dir) yield download_file(url, _sesh, fin_dir, progress_callback, stop_requested, checksums, keep_compressed=not is_tables) + +def get_brdc_urls(start_time: datetime, end_time: datetime) -> list[str]: + """ + Generates a list of BRDC file URLs for the specified date range. + + :param start_time: Start of the date range + :param end_time: End of the date range + :returns: List URLs to download BRDC files + """ + urls = [] + reference_dt = start_time + while int((end_time - reference_dt).total_seconds()) > 0: + day = reference_dt.strftime("%j") + filename = f"BRDC00IGS_R_{reference_dt.year}{day}0000_01D_MN.rnx.gz" + url = f"{BASE_URL}/gnss/data/daily/{reference_dt.year}/brdc/{filename}" + urls.append(url) + reference_dt += timedelta(days=1) + return urls + def _get_repro3_filename_and_url(row: pd.Series, gps_week: int, session: requests.Session = None) -> tuple: """ Determine the correct filename and URL for a repro3 product. @@ -1128,6 +1242,7 @@ def _get_repro3_filename_and_url(row: pd.Series, gps_week: int, session: request url = f"{BASE_URL}/gnss/products/{gps_week}/repro3/{filename}" return filename, url +#endregion #region SP3 Product Validation @@ -1594,7 +1709,6 @@ def download_bia_satellite_section(url: str, session: requests.Session, progress return None - def _check_bia_termination(content: str, force_return: bool = False) -> tuple[bool, Optional[str]]: """ Check if we should stop downloading and extract the satellite bias section. @@ -1793,10 +1907,8 @@ def parse_bia_code_priorities(bia_content: str) -> dict: return code_priorities -def get_bia_code_priorities_for_selection(products_df: pd.DataFrame, - provider: str, series: str, project: str, - progress_callback: Optional[Callable] = None, - stop_requested: Optional[Callable] = None) -> Optional[dict]: +def get_bia_code_priorities_for_selection(products_df: pd.DataFrame, provider: str, series: str, project: str, + progress_callback: Optional[Callable] = None, stop_requested: Optional[Callable] = None) -> Optional[dict]: """ Download and parse BIA file for a specific provider/series/project combination to extract available code priorities per constellation. @@ -1847,7 +1959,7 @@ def get_bia_code_priorities_for_selection(products_df: pd.DataFrame, _log_bia_code_priorities(code_priorities, provider, series, project) return code_priorities - Logger.terminal(f"📥 Validating constellation signal frequencies against BIA file for {provider}/{series}/{project}...") + Logger.workflow(f"📥 Validating constellation signal frequencies against BIA file for {provider}/{series}/{project}...") # Download satellite bias section bia_content = download_bia_satellite_section(url, session, progress_callback=progress_callback, stop_requested=stop_requested) @@ -1861,7 +1973,6 @@ def get_bia_code_priorities_for_selection(products_df: pd.DataFrame, _log_bia_code_priorities(code_priorities, provider, series, project) return code_priorities - def _try_read_local_bia(local_uncompressed: Path, local_compressed: Path, compressed_filename: str, provider: str, series: str, project: str) -> Optional[str]: """ @@ -1918,7 +2029,6 @@ def _try_read_local_bia(local_uncompressed: Path, local_compressed: Path, return None - def _read_compressed_bia(filepath: Path) -> Optional[str]: """ Read and decompress a .gz or .Z compressed BIA file. @@ -1937,7 +2047,6 @@ def _read_compressed_bia(filepath: Path) -> Optional[str]: Logger.console(f"Failed to decompress {filepath.name}: {e}") return None - def _log_bia_code_priorities(code_priorities: dict, provider: str, series: str, project: str): """Log extracted BIA code priorities.""" Logger.console(f"✅ Extracted code priorities for {provider}/{series}/{project}:") @@ -1999,23 +2108,36 @@ def download_sinex_file(target_date: datetime, download_dir: Path = INPUT_PRODUC url = get_sinex_url(target_date, use_repro3=use_repro3) + # Check the file actually exists on the server before attempting any downloads. + # The IGS CRD SINEX is often missing for recent dates (e.g. ultra-rapid / rapid + # products), so a quick HEAD request avoids a long, noisy retry storm of 404s. + try: + head = session.head(url, timeout=10, allow_redirects=True) + if head.status_code == 404: + Logger.workflow(f"ℹ️ SINEX file not available on server: {url.split('/')[-1]}") + return None + except requests.RequestException as e: + # Network hiccup on the existence check - fall through and let the + # download (with its own retries) decide whether the file is reachable. + Logger.console(f"SINEX existence check failed ({e}), proceeding to download attempt") + # Fetch checksums for the SINEX file's GPS week checksums = get_checksums_for_week(gps_week, session, download_dir, use_repro3, progress_callback, stop_requested) for attempt in range(max_retries): if stop_requested and stop_requested(): - Logger.terminal("🛑 SINEX download cancelled") + Logger.workflow("🛑 SINEX download cancelled") return None try: filepath = download_file(url, session, download_dir, progress_callback, stop_requested, checksums) - Logger.terminal(f"✅ SINEX file downloaded: {filepath.name}") + Logger.workflow(f"✅ SINEX file downloaded: {filepath.name}") return filepath except Exception as e: if attempt < max_retries - 1: - Logger.terminal(f"⚠️ SINEX download attempt {attempt + 1}/{max_retries} failed: {e}") + Logger.workflow(f"⚠️ SINEX download attempt {attempt + 1}/{max_retries} failed: {e}") else: - Logger.terminal(f"❌ Failed to download SINEX file after {max_retries} attempts: {e}") + Logger.workflow(f"❌ Failed to download SINEX file after {max_retries} attempts: {e}") return None @@ -2335,53 +2457,54 @@ def download_and_validate_sinex(target_date: datetime, marker_name: str, receive return sinex_path, results except Exception as e: - Logger.terminal(f"❌ Error reading SINEX file: {e}") + Logger.workflow(f"❌ Error reading SINEX file: {e}") return sinex_path, {'error': f'Failed to read SINEX file: {e}'} def log_sinex_validation_results(results: dict, marker_name: str): """ - Log SINEX validation results to the terminal. + Log SINEX validation results to the workflow terminal. :param results: Validation results dictionary from validate_sinex_values() :param marker_name: Marker name for logging context """ if 'error' in results: - Logger.terminal(f"❌ SINEX validation error: {results['error']}") + Logger.workflow(f"❌ SINEX validation error: {results['error']}") return if not results['marker_found']: - Logger.terminal(f"⚠️ Marker '{marker_name}' not found in SINEX file - validation skipped") + Logger.workflow(f"⚠️ Marker '{marker_name}' not found in SINEX file - validation skipped") return all_valid = True has_validations = False - Logger.terminal(f"📋 SINEX validation results for marker '{marker_name}':") + Logger.workflow(f"📋 SINEX validation results for marker '{marker_name}':") for field in ['receiver_type', 'antenna_type', 'antenna_offset', 'apriori_position']: field_result = results.get(field, {}) message = field_result.get('message', '') if field_result.get('valid') is True: - Logger.terminal(f" ✅ {field.replace('_', ' ').title()}: {message}") + Logger.workflow(f" ✅ {field.replace('_', ' ').title()}: {message}") has_validations = True elif field_result.get('valid') is False: - Logger.terminal(f" ⚠️ {field.replace('_', ' ').title()}: {message}") + Logger.workflow(f" ⚠️ {field.replace('_', ' ').title()}: {message}") all_valid = False has_validations = True elif message: # valid is None but there's a message (info only, no comparison made) - Logger.terminal(f" ℹ️ {field.replace('_', ' ').title()}: {message}") + Logger.workflow(f" ℹ️ {field.replace('_', ' ').title()}: {message}") if has_validations: if all_valid: - Logger.terminal(f"✅ All SINEX validations passed for marker '{marker_name}'") + Logger.workflow(f"✅ All SINEX validations passed for marker '{marker_name}'") else: - Logger.terminal(f"⚠️ Some SINEX validations failed for marker '{marker_name}' - please review the above warnings") + Logger.workflow(f"⚠️ Some SINEX validations failed for marker '{marker_name}' - please review the above warnings") else: - Logger.terminal(f"ℹ️ SINEX data found for marker '{marker_name}' but no comparisons were made (RINEX values may be missing)") + Logger.workflow(f"ℹ️ SINEX data found for marker '{marker_name}' but no comparisons were made (RINEX values may be missing)") # endregion +# Test if __name__ == "__main__": # Test whole file download sesh = requests.Session() diff --git a/scripts/GinanUI/app/models/execution.py b/scripts/GinanUI/app/models/execution.py index 40eebd5d8..52cc086e5 100644 --- a/scripts/GinanUI/app/models/execution.py +++ b/scripts/GinanUI/app/models/execution.py @@ -1,38 +1,30 @@ +""" +PEA execution model for Ginan-UI. + +Manages the lifecycle of a Ginan PEA processing run: locating the PEA binary +(bundled or on PATH), building and writing the YAML config from user inputs, +launching the PEA subprocess, streaming its output, and generating post-run +visualisations. Also exposes the INPUT_PRODUCTS_PATH and GENERATED_YAML +constants re-exported from common_dirs for use by other modules. +""" + import os import platform import shutil -import subprocess import signal +import subprocess import threading import time -from importlib.resources import files - -from ruamel.yaml.scalarstring import PlainScalarString -from ruamel.yaml.comments import CommentedSeq, CommentedMap from pathlib import Path -from scripts.GinanUI.app.utils.yaml import load_yaml, write_yaml, normalise_yaml_value +from ruamel.yaml.comments import CommentedMap, CommentedSeq +from ruamel.yaml.scalarstring import PlainScalarString +from scripts.GinanUI.app.utils.common_dirs import GENERATED_YAML, INPUT_PRODUCTS_PATH, TABLES_PRODUCTS_PATH, TEMPLATE_PATH +from scripts.GinanUI.app.utils.logger import Logger +from scripts.GinanUI.app.utils.yaml import load_yaml, normalise_yaml_value, write_yaml + +# Imports external of Ginan-UI entirely from scripts.plot_pos import plot_pos_files from scripts.plot_trace_res import plot_trace_res_files -from scripts.GinanUI.app.utils.common_dirs import GENERATED_YAML, TEMPLATE_PATH, INPUT_PRODUCTS_PATH - -# Import the new logger -try: - from scripts.GinanUI.app.utils.logger import Logger -except ImportError: - # Fallback if logger not yet in the correct location - class Logger: - @staticmethod - def terminal(msg): - print(f"[TERMINAL] {msg}") - - @staticmethod - def console(msg): - print(f"[CONSOLE] {msg}") - - @staticmethod - def both(msg): - print(f"[BOTH] {msg}") - def get_pea_exec(): """ @@ -135,6 +127,64 @@ def get_pea_exec(): f" - {ginan_root / 'bin' / 'pea' if 'ginan_root' in locals() else 'Could not determine ginan root'}" ) +def get_interpolate_loading_exec(): + """ + Locate the interpolate_loading binary using the same search strategy as get_pea_exec(). + + :return: Path to executable or str of PATH callable + :raises RuntimeError: If interpolate_loading binary cannot be found + """ + import sys + + binary_name = "interpolate_loading" + + # 1. Check if running in PyInstaller bundle + if getattr(sys, 'frozen', False): + base_path = Path(sys._MEIPASS) + + if platform.system().lower() == "darwin": + pea_path = base_path.parent / "Resources" / "bin" / binary_name + if pea_path.exists(): + return pea_path + pea_path = base_path / "bin" / binary_name + if pea_path.exists(): + return pea_path + else: + exe_name = f"{binary_name}.exe" if platform.system().lower() == "windows" else binary_name + pea_path = base_path / "bin" / exe_name + if pea_path.exists(): + return pea_path + + # 2. Check if binary is on PATH + if shutil.which(binary_name): + Logger.console(f"✅ Found {binary_name} on PATH: {shutil.which(binary_name)}") + return binary_name + + # 3. Try to find binary relative to this script's location + try: + current_file = Path(__file__).resolve() + ginan_root = current_file.parents[4] + loading_binary = ginan_root / "bin" / binary_name + + if loading_binary.exists() and loading_binary.is_file(): + if not os.access(loading_binary, os.X_OK): + try: + loading_binary.chmod(loading_binary.stat().st_mode | 0o111) + except Exception as e: + raise RuntimeError(f"⚠️ {binary_name} found at {loading_binary} but is not executable: {e}") + + Logger.console(f"✅ Found {binary_name} binary at: {loading_binary}") + return loading_binary + + except Exception as e: + Logger.console(f"⚠️ Error while searching for {binary_name} relative to script location: {e}") + + raise RuntimeError( + f"{binary_name} executable not found. Please ensure:\n" + f"1. You have built the Ginan binaries (should be at ginan/bin/{binary_name})\n" + f"2. You are running GinanUI from within the ginan directory structure, or\n" + f"3. The '{binary_name}' executable is available on your system PATH" + ) class Execution: def __init__(self, config_path: Path = GENERATED_YAML): @@ -145,7 +195,8 @@ def __init__(self, config_path: Path = GENERATED_YAML): """ self.config_path = config_path self.executable = get_pea_exec() # the PEA executable - self.changes = False # Flag to track if config has been changed + self.changes = False # Flag to track if config has been changed + self.yaml_overwrite = True # Whether UI changes should be written to the YAML file self._procs = [] self._stop_event = threading.Event() @@ -163,6 +214,8 @@ def __init__(self, config_path: Path = GENERATED_YAML): self.config = load_yaml(config_path) + #region YAML Config Manipulation + def reload_config(self): """ Force reload of the YAML config from disk into memory. @@ -237,6 +290,20 @@ def edit_config(self, key_path: str, value, add_field=False): node[final_key] = value + def set_loading_params(self, marker_name: str, marker_number: str, apriori_position: list): + """ + Store loading BLQ parameters without applying the full UI config + Used when YAML overwrite is disabled so that ensure_loading_blq() + can still generate BLQ files if needed + + :param marker_name: 4-character station marker name (e.g. 'ALIC') + :param marker_number: DOMES marker number (e.g. '50137M0014') or None + :param apriori_position: [X, Y, Z] ECEF coordinates in metres + """ + self._loading_marker_name = marker_name + self._loading_marker_number = marker_number + self._loading_apriori_position = apriori_position + def apply_ui_config(self, inputs): """ Applies UI settings to **cached** config. **Call write_cached_changes()** to write them. @@ -281,6 +348,7 @@ def apply_ui_config(self, inputs): self.edit_config("outputs.gpx.output", bool(inputs.gpx_output), True) self.edit_config("outputs.pos.output", bool(inputs.pos_output), True) self.edit_config("outputs.trace.output_network", bool(inputs.trace_output_network), True) + self.edit_config("outputs.sinex.output", bool(inputs.snx_output), True) # 2. Replace 'TEST' receiver block with real marker name if "TEST" in self.config.get("receiver_options", {}): @@ -351,6 +419,11 @@ def apply_ui_config(self, inputs): if sinex_filename: self._add_sinex_to_config(sinex_filename) + # 7. Store loading BLQ parameters for ensure_loading_blq() during execute_config() + self._loading_marker_name = inputs.marker_name + self._loading_marker_number = getattr(inputs, 'marker_number', None) + self._loading_apriori_position = inputs.apriori_position + def _add_sinex_to_config(self, sinex_filename: str): """ Append the SINEX filename to the config's inputs.snx_files list. @@ -408,16 +481,353 @@ def _add_sinex_to_config(self, sinex_filename: str): self.config["inputs"]["snx_files"] = new_seq except Exception as e: - Logger.terminal(f"⚠️ Failed to write SINEX to config: {e}") + Logger.workflow(f"⚠️ Failed to write SINEX to config: {e}") + + def _station_in_blq(self, blq_path: Path, marker_name: str) -> bool: + """ + Check whether a station (by 4-character marker name) already has an entry in a BLQ file. + + BLQ station entries start with two leading spaces followed by the + 4-character marker name. The marker may be followed by a space and + DOMES number (e.g. " ALIC 50137M001"), trailing whitespace only + (e.g. " AGGO "), or nothing before the newline (e.g. " AGGO"). + Comment lines starting with "$$" are ignored. + + :param blq_path: Path to the BLQ file + :param marker_name: 4-character station marker name (e.g. 'ALIC') + :return: True if the station is found in the BLQ file + """ + if not blq_path.exists(): + return False + + upper_marker = marker_name.upper() + # Match " XXXX" at start of line: 2 spaces + exact 4-char code, + # then verify the next character (if any) is whitespace to avoid + # false positives like "ALIC2" + entry_prefix = f" {upper_marker}" + prefix_len = len(entry_prefix) + try: + with blq_path.open('r', encoding='utf-8', errors='replace') as f: + for line in f: + if line.startswith("$$"): + continue + if line.upper().startswith(entry_prefix): + # Ensure the character after the marker (if present) is whitespace + if len(line) <= prefix_len or line[prefix_len].isspace(): + return True + except Exception as e: + Logger.console(f"⚠️ Error reading BLQ file {blq_path}: {e}") + + return False + + def _station_in_configured_blq_files(self, config_key: str, marker_name: str) -> bool: + """ + Check whether a station is present in any of the non-wildcard BLQ files + listed under the given YAML config key. + + Only checks concrete filenames from the config (not wildcard patterns like + '*_ocean.BLQ'), resolved relative to inputs_root. This ensures we only + validate against files that PEA will actually read. + + :param config_key: Dot-separated YAML key (e.g. 'inputs.tides.ocean_tide_loading_blq_files') + :param marker_name: 4-character station marker name (e.g. 'ALIC') + :return: True if the station is found in any configured non-wildcard BLQ file + """ + try: + keys = config_key.split(".") + node = self.config + for k in keys: + node = node[k] + except (KeyError, TypeError): + return False + + if not isinstance(node, (list, CommentedSeq)): + return False + + # Resolve inputs_root for building absolute paths + inputs_root = self.config.get("inputs", {}).get("inputs_root", "") + inputs_root = Path(str(inputs_root)) if inputs_root else INPUT_PRODUCTS_PATH + + for entry in node: + entry_str = str(entry).strip() + # Skip wildcard patterns - those will pick up generated files automatically + if "*" in entry_str or "?" in entry_str: + continue + + # Resolve the BLQ file path relative to inputs_root + blq_path = Path(inputs_root) / entry_str + if self._station_in_blq(blq_path, marker_name): + return True + + return False + + def ensure_loading_blq(self, marker_name: str, marker_number: str, + apriori_position: list, progress_callback=None, stop_requested=None): + """ + Ensure ocean and atmospheric tide loading BLQ files exist for the given station. + + Reads the BLQ file lists from the YAML config and checks only the + non-wildcard entries that PEA will actually use. If the station is not + found, downloads the loading grid netCDF files (if needed) and runs + interpolate_loading to generate station-specific BLQ files. + + :param marker_name: 4-character station marker name (e.g. 'ALIC') + :param marker_number: DOMES marker number (e.g. '50137M0014') or None + :param apriori_position: [X, Y, Z] ECEF coordinates in metres + :param progress_callback: Optional (description, percent) callback + :param stop_requested: Optional bool callback for cancellation + :raises RuntimeError: If interpolate_loading fails + """ + from scripts.GinanUI.app.models.dl_products import download_loading_grids + + if not marker_name or not apriori_position or all(v == 0.0 for v in apriori_position): + Logger.workflow("⚠️ Missing marker name or apriori position - skipping loading BLQ generation") + return + + # Check BLQ files that are actually referenced in the YAML config + ocean_present = self._station_in_configured_blq_files("inputs.tides.ocean_tide_loading_blq_files", marker_name) + atmos_present = self._station_in_configured_blq_files("inputs.tides.atmos_tide_loading_blq_files", marker_name) + + if ocean_present and atmos_present: + Logger.workflow(f"✅ Station '{marker_name}' already present in configured ocean and atmospheric loading BLQ files") + return + + # Locate the interpolate_loading binary + try: + loading_exec = get_interpolate_loading_exec() + except RuntimeError as e: + Logger.workflow(f"⚠️ {e} - skipping loading BLQ generation") + return + + # Download loading grid files if not already present + Logger.workflow("📥 Ensuring loading grid files are available...") + if progress_callback: + progress_callback("Downloading loading grids", 10) + download_loading_grids( + download_dir=TABLES_PRODUCTS_PATH, + progress_callback=progress_callback, + stop_requested=stop_requested, + ) + + if stop_requested and stop_requested(): + return + + ocean_grid = TABLES_PRODUCTS_PATH / "oceantide.nc" + atmos_grid = TABLES_PRODUCTS_PATH / "atmtide.nc" + + if not ocean_grid.exists() or not atmos_grid.exists(): + Logger.workflow("⚠️ Loading grid files not available - skipping loading BLQ generation") + return + + # Build the --code argument: 'ALIC 50137M0014' or just 'ALIC' + if marker_number: + station_code = f"{marker_name} {marker_number}" + else: + station_code = marker_name + + # Build XYZ location arguments from apriori_position + x, y, z = apriori_position + + # Generate ocean loading BLQ if needed + if not ocean_present: + if stop_requested and stop_requested(): + return + + ocean_output = INPUT_PRODUCTS_PATH / f"{marker_name}_ocean.BLQ" + Logger.workflow(f"🌊 Computing ocean tide loading for '{station_code}'...") + if progress_callback: + progress_callback("Computing ocean loading", 40) + + self._run_interpolate_loading( + loading_exec, "o", ocean_grid, station_code, x, y, z, ocean_output + ) + + if ocean_output.exists(): + Logger.workflow(f"✅ Ocean loading BLQ generated: {ocean_output.name}") + self._update_blq_config("inputs.tides.ocean_tide_loading_blq_files", + ocean_output.name) + else: + Logger.workflow("⚠️ Ocean loading BLQ file was not generated") + + # Generate atmospheric loading BLQ if needed + if not atmos_present: + if stop_requested and stop_requested(): + return + + atmos_output = INPUT_PRODUCTS_PATH / f"{marker_name}_atmos.BLQ" + Logger.workflow(f"🌬️ Computing atmospheric tide loading for '{station_code}'...") + if progress_callback: + progress_callback("Computing atmospheric loading", 70) + + self._run_interpolate_loading( + loading_exec, "a", atmos_grid, station_code, x, y, z, atmos_output + ) + + if atmos_output.exists(): + Logger.workflow(f"✅ Atmospheric loading BLQ generated: {atmos_output.name}") + self._update_blq_config("inputs.tides.atmos_tide_loading_blq_files", + atmos_output.name) + else: + Logger.workflow("⚠️ Atmospheric loading BLQ file was not generated") + + # Write BLQ config updates to disk (always allowed, even when yaml_overwrite is disabled) + if self.changes: + self.write_cached_changes() + + if progress_callback: + progress_callback("Loading BLQ complete", 100) + + def _run_interpolate_loading(self, loading_exec, loading_type: str, grid_path: Path, + station_code: str, x: float, y: float, z: float, output_path: Path): + """ + Execute the interpolate_loading binary for a single loading type. + + :param loading_exec: Path or name of the interpolate_loading executable + :param loading_type: 'o' for ocean or 'a' for atmospheric + :param grid_path: Path to the loading grid netCDF file + :param station_code: Station code string for --code (e.g. 'ALIC 50137M0014') + :param x: ECEF X coordinate + :param y: ECEF Y coordinate + :param z: ECEF Z coordinate + :param output_path: Path for the output BLQ file + :raises RuntimeError: If the subprocess returns a non-zero exit code + """ + command = [ + str(loading_exec), + "--type", loading_type, + "--grid", str(grid_path), + "--code", station_code, + "--xyz", + "--location", str(x), str(y), str(z), + "--output", str(output_path), + ] + + Logger.console(f"🚀 Running: {' '.join(command)}") + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + timeout=120, + ) + + if result.stdout: + for line in result.stdout.strip().splitlines(): + Logger.console(line) + + if result.returncode != 0: + stderr_msg = result.stderr.strip() if result.stderr else "No error output" + Logger.workflow(f"⚠️ interpolate_loading exited with code {result.returncode}: {stderr_msg}") + raise RuntimeError(f"interpolate_loading failed (exit code {result.returncode}): {stderr_msg}") + + except subprocess.TimeoutExpired: + Logger.workflow("⚠️ interpolate_loading timed out after 120 seconds") + raise RuntimeError("interpolate_loading timed out") + + def _update_blq_config(self, key_path: str, blq_filename: str): + """ + Append a generated BLQ filename to the existing BLQ file list in the YAML config. + + Does not add the filename if it is already present or if an existing + wildcard pattern already covers it. Preserves existing entries + (e.g. the global OLOAD_GO.BLQ / ALOAD_GO.BLQ and any wildcards). + + :param key_path: Dot-separated YAML key path (e.g. 'inputs.tides.ocean_tide_loading_blq_files') + :param blq_filename: New BLQ filename relative to inputs_root (e.g. 'ALIC_ocean.BLQ') + """ + import fnmatch + + try: + keys = key_path.split(".") + node = self.config + for k in keys[:-1]: + node = node[k] + final_key = keys[-1] + + existing = node.get(final_key) + + if isinstance(existing, CommentedSeq): + existing_strs = [str(item) for item in existing] + + # Skip if an existing wildcard pattern already covers this filename + for entry in existing_strs: + if fnmatch.fnmatch(blq_filename, entry): + return + + # Skip if the exact filename is already present + if blq_filename not in existing_strs: + existing.append(normalise_yaml_value(blq_filename)) + existing.fa.set_block_style() + self.changes = True + else: + # No existing list - create one with just the new file + new_seq = CommentedSeq([normalise_yaml_value(blq_filename)]) + new_seq.fa.set_block_style() + self.edit_config(key_path, new_seq, False) + except Exception as e: + Logger.workflow(f"⚠️ Failed to update BLQ config at {key_path}: {e}") def write_cached_changes(self): write_yaml(self.config_path, self.config) self.changes = False + #endregion + + #region PEA Processing Execution + + def _ensure_loading_before_pea(self): + """ + Check whether the station needs loading BLQ files generated and run + ensure_loading_blq() synchronously before PEA execution begins. + + When YAML overwrite is disabled, reloads the config from disk first so + that BLQ updates are applied on top of the user's manual edits rather + than the in-memory UI-applied config. + + Uses marker_name, marker_number, and apriori_position stored by + apply_ui_config() or set_loading_params() to ensure the correct + station is checked even when receiver_options contains keys from + previous runs. + """ + try: + marker_name = getattr(self, '_loading_marker_name', None) + marker_number = getattr(self, '_loading_marker_number', None) + apriori_position = getattr(self, '_loading_apriori_position', None) + + if not marker_name: + return + + if not apriori_position or all(v == 0.0 for v in apriori_position): + Logger.workflow("⚠️ No valid apriori position - skipping loading BLQ check") + return + + # When YAML overwrite is disabled, reload from disk so BLQ updates + # are applied on top of the user's manual edits + if not self.yaml_overwrite: + self.reload_config() + + def check_stop(): + return self._stop_event.is_set() + + self.ensure_loading_blq( + marker_name=marker_name, + marker_number=marker_number, + apriori_position=list(apriori_position), + stop_requested=check_stop, + ) + + except Exception as e: + Logger.workflow(f"⚠️ Loading BLQ pre-check failed (non-fatal): {e}") + def execute_config(self): """ If changes were made since last write, writes config, then executes pea with config. + Ensures ocean/atmospheric loading BLQ files are generated before PEA runs. All PEA output is logged to the console widget. + + When YAML overwrite is disabled, skips writing UI changes to the config file + but still allows BLQ updates via ensure_loading_blq(). """ # Check if executable is available if self.executable is None: @@ -426,10 +836,20 @@ def execute_config(self): # clear stop flag before each run self.reset_stop_flag() - if self.changes: + if self.changes and self.yaml_overwrite: self.write_cached_changes() self.changes = False + # Ensure loading BLQ files exist before PEA execution + self._ensure_loading_before_pea() + + # Reset yaml_overwrite back to default for next run + self.yaml_overwrite = True + + if self._stop_event.is_set(): + Logger.console("🛑 Execution stopped by user during loading BLQ generation") + return + command = [self.executable, "--config", str(self.config_path)] workdir = str(Path(self.config_path).parent) @@ -534,6 +954,10 @@ def stop_all(self): def reset_stop_flag(self): self._stop_event.clear() + #endregion + + #region Visualisation Plotting + def build_pos_plots(self, out_dir=None): """ Search for .pos and .POS files directly under outputs_root (not in archive/visual), @@ -559,31 +983,49 @@ def build_pos_plots(self, out_dir=None): pos_files = list(root.glob("*.pos")) + list(root.glob("*.POS")) if pos_files: - Logger.terminal(f"📂 Found {len(pos_files)} .pos files in {root}:") + Logger.workflow(f"📂 Found {len(pos_files)} .pos files in {root}:") for f in pos_files: - Logger.terminal(f" • {f.name}") + Logger.workflow(f" • {f.name}") else: - Logger.terminal(f"⚠️ No .pos files found in {root}") + Logger.workflow(f"⚠️ No .pos files found in {root}") + + # Separate forward and smoothed POS files into two groups + forward_pos = [f for f in pos_files if "_smoothed" not in f.stem.lower()] + smoothed_pos = [f for f in pos_files if "_smoothed" in f.stem.lower()] htmls = [] - for pos_path in pos_files: + + # Plot forward (regular) POS files as one unified set + if forward_pos: try: - base_name = pos_path.stem - save_prefix = out_dir / f"plot_{base_name}" + forward_paths = [str(f) for f in forward_pos] + save_prefix = out_dir / "plot_pos" + html_files = plot_pos_files( + input_files=forward_paths, + save_prefix=str(save_prefix) + ) + htmls.extend(html_files) + except Exception as e: + Logger.workflow(f"[plot_pos] ❌ Failed for forward pos files: {e}") + # Plot smoothed POS files as a separate unified set + if smoothed_pos: + try: + smoothed_paths = [str(f) for f in smoothed_pos] + save_prefix = out_dir / "plot_pos_smoothed" html_files = plot_pos_files( - input_files=[str(pos_path)], + input_files=smoothed_paths, save_prefix=str(save_prefix) ) htmls.extend(html_files) except Exception as e: - Logger.terminal(f"[plot_pos] ❌ Failed for {pos_path.name}: {e}") + Logger.workflow(f"[plot_pos] ❌ Failed for smoothed pos files: {e}") # Final summary if htmls: - Logger.terminal(f"✅ Generated {len(htmls)} plot(s) → saved in {out_dir}") + Logger.workflow(f"✅ Generated {len(htmls)} plot(s) → saved in {out_dir}") else: - Logger.terminal("⚠️ No plots were generated.") + Logger.workflow("⚠️ No plots were generated.") return htmls @@ -620,11 +1062,11 @@ def build_trace_plots(self, out_dir=None): trace_files = list(root.glob("*.TRACE")) + list(root.glob("*.trace")) if trace_files: - Logger.terminal(f"📂 Found {len(trace_files)} .TRACE files in {root}:") + Logger.workflow(f"📂 Found {len(trace_files)} .TRACE files in {root}:") for f in trace_files: - Logger.terminal(f" • {f.name}") + Logger.workflow(f" • {f.name}") else: - Logger.terminal(f"⚠️ No .TRACE files found in {root}") + Logger.workflow(f"⚠️ No .TRACE files found in {root}") return [] htmls = [] @@ -644,12 +1086,14 @@ def build_trace_plots(self, out_dir=None): ) htmls.extend(html_files) except Exception as e: - Logger.terminal(f"[plot_trace_res] ❌ Failed to generate trace plots: {e}") + Logger.workflow(f"[plot_trace_res] ❌ Failed to generate trace plots: {e}") # Final summary if htmls: - Logger.terminal(f"✅ Generated {len(htmls)} trace plot(s) → saved in {out_dir}") + Logger.workflow(f"✅ Generated {len(htmls)} trace plot(s) → saved in {out_dir}") else: - Logger.terminal("⚠️ No trace plots were generated.") + Logger.workflow("⚠️ No trace plots were generated.") + + return htmls - return htmls \ No newline at end of file + #endregion \ No newline at end of file diff --git a/scripts/GinanUI/app/models/inspector.py b/scripts/GinanUI/app/models/inspector.py new file mode 100644 index 000000000..4818e0009 --- /dev/null +++ b/scripts/GinanUI/app/models/inspector.py @@ -0,0 +1,439 @@ +""" +GinanYAMLInspector model for Ginan-UI. + +Provides the data and I/O layer that creates the GinanYAMLInspector integration: +It ensures that the inspector's HTML asset exists (auto-generating it via "pea -Y 4" +when missing), builds the JavaScript that auto-imports the current config and +intercepts the inspector's "Save file" button via QWebChannel, sanitising the +YAML text emitted by the inspector, deep-merging it onto the existing +ppp_generated.yaml so keys the inspector does not know about are preserved, and +writing the result back to disk with a clean-write fallback when ruamel.yaml's +comment-preserving output cannot be re-parsed. + +The owning controller is responsible for all UI presentation, including +showing the inspector dialog and surfacing any errors raised here. +""" + +import re +import subprocess +from pathlib import Path +from ruamel.yaml import YAML as RuamelYAML +from scripts.GinanUI.app.utils.common_dirs import GENERATED_YAML, INSPECTOR_HTML_PATH +from scripts.GinanUI.app.utils.logger import Logger +from scripts.GinanUI.app.utils.yaml import load_yaml, write_yaml + + +class Inspector: + """ + Model for the GinanYAMLInspector integration. + + Exposes a small, controller-facing API: ensure the HTML asset exists, + read the current config text, build the JS that wires auto-import and the save intercept, + and merge / save the inspector output + + Arguments: + executable: Path-like to the PEA executable, used only when the inspector + HTML must be auto-generated via "pea -Y 4". Optional - if not + provided, ensure_inspector_html() will fail gracefully when + the HTML is missing. + """ + + def __init__(self, executable=None): + self.executable = executable + + #region Inspector HTML Generation + + def ensure_inspector_html(self) -> bool: + """ + Ensure the GinanYAMLInspector HTML file exists at INSPECTOR_HTML_PATH + + Attempts to generate it. If pea is not available or + the generation fails, logs a warning but does not raise - the caller must + check the return value and inform the user. + + Returns: + bool: True if the inspector HTML file exists (or was just generated). + """ + if INSPECTOR_HTML_PATH.exists(): + return True + + Logger.workflow("🔧 GinanYAMLInspector not found - Attempting to generate...") + + if self.executable is None: + Logger.workflow("⚠️ PEA executable not provided - cannot auto-generate GinanYAMLInspector HTML.") + return False + + try: + INSPECTOR_HTML_PATH.parent.mkdir(parents=True, exist_ok=True) + + # Run pea -Y which writes GinanYAMLInspector.html to the working directory + result = subprocess.run( + [str(self.executable), "-Y", "4"], + capture_output=True, + text=True, + cwd=str(INSPECTOR_HTML_PATH.parent), + timeout=30, + ) + + if result.returncode != 0: + Logger.workflow(f"⚠️ 'pea -Y 4' exited with code {result.returncode}: {result.stderr.strip()}") + + # pea writes GinanYAMLInspector.html into its working directory + generated = INSPECTOR_HTML_PATH.parent / "GinanYamlInspector.html" + if generated.exists(): + if generated != INSPECTOR_HTML_PATH: + generated.rename(INSPECTOR_HTML_PATH) + Logger.workflow(f"✅ GinanYAMLInspector HTML generated: {INSPECTOR_HTML_PATH}") + return True + + Logger.workflow("⚠️ 'pea -Y 4' ran but GinanYAMLInspector.html was not found in the output directory") + return False + + except FileNotFoundError: + Logger.workflow("⚠️ PEA executable not found - cannot auto-generate GinanYAMLInspector HTML") + return False + except subprocess.TimeoutExpired: + Logger.workflow("⚠️ 'pea -Y 4' timed out after 30 seconds - cannot auto-generate GinanYAMLInspector") + return False + except Exception as e: + Logger.workflow(f"⚠️ Failed to generate GinanYAMLInspector HTML: {e}") + return False + + #endregion + + #region Import Config to Inspector + + @staticmethod + def read_current_config() -> str: + """ + Read the current ppp_generated.yaml content for inspector auto-import + + Returns the raw YAML text, or an empty string if the file cannot be read + Failures are logged but never raised - the caller can still open the + inspector with an empty pre-load if reading the config fails + + Returns: + str: Raw YAML text from ppp_generated.yaml, or "" on any error + """ + try: + return GENERATED_YAML.read_text(encoding="utf-8") + except Exception as e: + Logger.workflow(f"⚠️ Could not read config for GinanYAMLInspector auto-import: {e}") + return "" + + #endregion + + #region JS Builder + + @staticmethod + def build_ginan_ui_js(yaml_content: str) -> str: + """ + Build the JavaScript that is injected into the GinanYAMLInspector page when it + is opened from Ginan-UI + + Two things are wired up: + 1. Auto-import - the current ppp_generated.yaml content is passed in as a JS + string and fed directly to the inspector's file-input change handler so the + inspector pre-populates its fields without requiring user interaction + 2. Save intercept - the "Save file" (#create) button's default download action + is replaced with a call to bridge.saveYaml() over QWebChannel, routing the + generated YAML text back to Python for merging and writing to disk + + The YAML content is embedded directly as a string literal rather than fetched + via file:// URL - this sidesteps Qt WebEngine's local-content security policy + which blocks cross-origin file:// fetches in practice + + Arguments: + yaml_content (str): The raw text of ppp_generated.yaml to pre-load + + Returns: + str: JavaScript source ready to pass to QWebEnginePage.runJavaScript() + """ + # Escape the YAML text so it is safe to embed inside a JS template literal + # Backticks, backslashes and ${...} are the only characters that need escaping + # inside a JS template literal + escaped_yaml = ( + yaml_content + .replace("\\", "\\\\") + .replace("`", "\\`") + .replace("${", "\\${") + ) + + js = f""" + (function() {{ + + // 1. Auto-import ppp_generated.yaml + var yamlText = `{escaped_yaml}`; + + function doImport() {{ + var input = document.getElementById("inputfile"); + if (!input) {{ + console.warn("GinanYAMLInspector (Ginan-UI): #inputfile not found, retrying..."); + setTimeout(doImport, 200); + return; + }} + var file = new File([yamlText], "ppp_generated.yaml", {{ type: "text/plain" }}); + var dt = new DataTransfer(); + dt.items.add(file); + input.files = dt.files; + input.dispatchEvent(new Event("change")); + console.log("GinanYAMLInspector (Ginan-UI): auto-imported ppp_generated.yaml"); + }} + doImport(); + + // 2. Intercept "Save file" → QWebChannel bridge + // qt.webChannelTransport is injected by Qt before the page loads (via + // QWebEnginePage.scripts()), so it is guaranteed to exist here + new QWebChannel(qt.webChannelTransport, function(channel) {{ + var bridge = channel.objects.bridge; + if (!bridge) {{ + console.warn("GinanYAMLInspector (Ginan-UI): bridge not found in QWebChannel"); + return; + }} + + var createBtn = document.getElementById("create"); + if (!createBtn) {{ + console.warn("GinanYAMLInspector (Ginan-UI): #create button not found"); + return; + }} + + // Use capture phase so this listener fires before the original download handler + createBtn.addEventListener("click", function(evt) {{ + evt.stopImmediatePropagation(); + evt.preventDefault(); + var textbox = document.getElementById("textbox"); + var yaml = textbox ? textbox.value : ""; + if (!yaml || yaml === "generated yaml file") {{ + alert('Please click "Generate yaml" first to produce the YAML content before saving.'); + return; + }} + bridge.saveYaml(yaml); + }}, true); + + console.log("GinanYAMLInspector (Ginan-UI): Save file button intercepted via QWebChannel"); + }}); + + }})(); + """ + return js + + #endregion + + #region YAML Transformations + + @staticmethod + def deep_merge(base: dict, override: dict) -> dict: + """ + Recursively merge *override* onto *base*, returning *base* modified in-place + + Keys present in *base* but absent from *override* are left untouched, which is + exactly the behaviour needed when merging a partial inspector export back onto + the full ppp_generated.yaml so that keys the inspector does not know about (e.g. + constellation blocks that were not checked, trace output flags, etc.) are preserved. + + Lists are replaced wholesale - the inspector always emits a complete list for any + key it writes, so element-level merging would produce duplicates. + + Arguments: + base (dict): The existing full config (will be mutated). + override (dict): The partial config from the inspector export. + + Returns: + dict: *base* after merging. + """ + for key, override_val in override.items(): + if key in base and isinstance(base[key], dict) and isinstance(override_val, dict): + Inspector.deep_merge(base[key], override_val) + else: + base[key] = override_val + return base + + @staticmethod + def fix_inspector_yaml(yaml_text: str) -> str: + """ + Pre-process the raw YAML text from the GinanYAMLInspector + + GinanYAMLInspector copies the values directly from its HTML input fields into the + generated YAML without adding quotes to them. Two classes of value break ruamel.yaml: + + 1. Wildcard glob patterns containing "*" (e.g. *.CLK, BRDC*, *_ocean.BLQ) + In YAML, a bare "*" is the alias indicator, so ruamel raises + "found undefined alias" when it encounters these unquoted + + 2. Trailing whitespace on lines, which can confuse ruamel's indentation + parser in certain edge cases + + Arguments: + yaml_text (str): Raw YAML string from the inspector textbox + + Returns: + str: Sanitised YAML string safe to pass to ruamel.yaml.load() + """ + # Strip trailing whitespace from every line + yaml_text = "\n".join(line.rstrip() for line in yaml_text.splitlines()) + + # Quote wildcard patterns in flow sequences: [*.CLK] -> ['*.CLK'] + def _quote_flow_wildcards(m): + items = m.group(1).split(',') + fixed = [] + for item in items: + s = item.strip() + if s and '*' in s and not (s.startswith('"') or s.startswith("'")): + s = f"'{s}'" + fixed.append(s) + return '[' + ', '.join(fixed) + ']' + + yaml_text = re.sub(r'\[([^\]]*\*[^\]]*)\]', _quote_flow_wildcards, yaml_text) + + # Quote wildcard patterns in block list items: " - *.CLK" -> " - '*.CLK'" + def _quote_block_wildcards(m): + indent, value = m.group(1), m.group(2) + if '*' in value and not (value.startswith('"') or value.startswith("'")): + value = f"'{value}'" + return f"{indent}- {value}" + + yaml_text = re.sub( + r'^(\s*)- (\S*\*\S*)$', + _quote_block_wildcards, + yaml_text, + flags=re.MULTILINE, + ) + + return yaml_text + + @staticmethod + def fix_written_yaml(yaml_text: str) -> str: + """ + Repair YAML text that ruamel.yaml has written incorrectly + + ruamel.yaml occasionally collapses a parent mapping key and its first child + onto a single line, e.g.: + + outputs: metadata: + config_description: ... + + instead of the correct: + + outputs: + metadata: + config_description: ... + + This produces "mapping values are not allowed here" when the file is + subsequently parsed. The fix splits any line that contains two mapping + keys separated by a space (i.e. "key1: key2:") into separate indented + lines, restoring the correct block-mapping structure + + Arguments: + yaml_text (str): YAML text as written by ruamel.yaml + + Returns: + str: Corrected YAML text + """ + fixed_lines = [] + for line in yaml_text.splitlines(): + # Detect lines of the form "key1: key2:" where key2 is itself + # a bare mapping key (no value after it, or only whitespace/comment) + # Pattern: optional spaces, plain-scalar key, ": ", another plain-scalar + # key ending with ":" and nothing else (or just a comment) + m = re.match( + r'^(\s*)([A-Za-z0-9_]+):\s+([A-Za-z0-9_]+):\s*(#.*)?$', + line + ) + if m: + outer_indent = m.group(1) + outer_key = m.group(2) + inner_key = m.group(3) + comment = m.group(4) or "" + inner_indent = outer_indent + " " + fixed_lines.append(f"{outer_indent}{outer_key}:") + fixed_lines.append(f"{inner_indent}{inner_key}:{(' ' + comment) if comment else ''}") + else: + fixed_lines.append(line) + return "\n".join(fixed_lines) + + #endregion + + #region Export Config from Inspector + + def merge_and_save(self, yaml_text: str) -> dict: + """ + Sanitise, merge, and write the inspector's YAML output to ppp_generated.yaml + + The inspector only emits keys that are checked in its UI, so a direct overwrite + would discard keys that Ginan-UI depends on (constellation blocks, trace flags, + etc.). Instead, the inspector output is deep-merged onto the existing + ppp_generated.yaml so unchecked / unknown keys are preserved + + After merging, the result is validated before being committed locally + If the comment-preserving write produces a file that ruamel cannot re-read + (such as a known ruamel edge case with certain comment / value combinations), a clean write + without comment preservation is used as a fallback so the save will always succeeds + + Arguments: + yaml_text (str): Raw YAML string produced by the inspector's "Generate yaml" step + + Returns: + dict: The merged config dict that was written to disk. The caller can use + this to repopulate UI fields if reloading from disk fails. + + Raises: + ValueError: If yaml_text is empty / whitespace-only, or if the inspector + output does not parse to a YAML mapping. + Exception: Any other failure during parse / merge / write / validate is + re-raised so the caller can surface it to the user. + """ + if not yaml_text or not yaml_text.strip(): + Logger.workflow("⚠️ GinanYAMLInspector returned empty YAML - saving aborted") + raise ValueError("Inspector returned empty YAML") + + # Pre-process the inspector output: quote wildcard glob patterns that + # would be misread as YAML aliases, and strip trailing whitespace. + sanitised_text = self.fix_inspector_yaml(yaml_text) + + # Parse the inspector output + _yaml = RuamelYAML() + inspector_data = _yaml.load(sanitised_text) + if not isinstance(inspector_data, dict): + raise ValueError("Inspector YAML did not parse to a mapping") + + # Load the existing full config so we can merge onto it + if GENERATED_YAML.exists(): + existing_data = load_yaml(GENERATED_YAML) + else: + existing_data = {} + + # Deep-merge: inspector values win, but keys absent from the inspector are kept + merged = self.deep_merge(existing_data, inspector_data) + + # Write with comment preservation and validate the result is re-parseable + # ruamel.yaml has an edge case where it collapses a parent key and its + # first child onto the same line (e.g. "outputs: metadata:"), producing + # a file that cannot be re-parsed. _fix_written_yaml detects and repairs + # this before we validate, so the corrected content is what ends up on disk + GENERATED_YAML.parent.mkdir(parents = True, exist_ok = True) + write_yaml(GENERATED_YAML, merged) + + # Read back, repair any collapsed-key lines, and rewrite + written_text = GENERATED_YAML.read_text(encoding = "utf-8") + fixed_text = self.fix_written_yaml(written_text) + if fixed_text != written_text: + GENERATED_YAML.write_text(fixed_text, encoding = "utf-8") + + # Validate the (possibly repaired) file is re-parseable + try: + load_yaml(GENERATED_YAML) + except Exception: + # Fall back: write a clean copy without ruamel's comment machinery + _clean_yaml = RuamelYAML() + _clean_yaml.default_flow_style = False + _clean_yaml.indent(mapping=4, sequence=4, offset=4) + _clean_yaml.width = 4096 + with GENERATED_YAML.open("w", encoding="utf-8") as f: + _clean_yaml.dump(dict(merged), f) + # Final validation — if this also fails, surface the error + load_yaml(GENERATED_YAML) + + Logger.workflow(f"✅ GinanYAMLInspector config saved to: {GENERATED_YAML}") + return merged + + #endregion diff --git a/scripts/GinanUI/app/models/rinex_extractor.py b/scripts/GinanUI/app/models/rinex_extractor.py index d1534e4b3..211c27146 100644 --- a/scripts/GinanUI/app/models/rinex_extractor.py +++ b/scripts/GinanUI/app/models/rinex_extractor.py @@ -1,12 +1,18 @@ +""" +Extracts metadata from RINEX observation files for use in Ginan-UI config generation. + +Parses RINEX v2, v3, and v4 headers to extract marker name, receiver and antenna +types, antenna offsets, approximate position, observation time window, data interval, +and per-constellation observation codes. Results are used to pre-populate the UI +and generate the PEA YAML config. +""" + import re from datetime import datetime -from pathlib import Path - from scripts.GinanUI.app.utils.logger import Logger from scripts.GinanUI.app.utils.yaml import load_yaml from scripts.GinanUI.app.utils.common_dirs import GENERATED_YAML - class RinexExtractor: def __init__(self, rinex_path: str): self.rinex_path = rinex_path @@ -46,6 +52,8 @@ def extract_rinex_data(self, rinex_path: str): } current_obs_system = None # Track the current system for continuation lines + #region Helper Functions + def format_time(year, month, day, hour, minute, second): """ Helper function to format the parameters into a usable time string for RNX extraction @@ -113,6 +121,8 @@ def extract_obs_types_v3(line: str, obs_data: str): # Some other system we don't care about (e.g., "S", "I") return (first_char, []) + #endregion + rinex_version = None previous_observation_dt = None epoch_interval = None @@ -120,16 +130,18 @@ def extract_obs_types_v3(line: str, obs_data: str): start_epoch = None end_epoch = None marker_name = None + marker_number = None receiver_type = None antenna_type = None antenna_offset = None apriori_position = None + # Read the RINEX file line and add each line to "lines" with open(rinex_path, "r", errors="replace") as f: lines = f.readlines() - i = 0 - n = len(lines) + i = 0 # Currently iterated upon RINEX line + n = len(lines) # Number of lines in RINEX file # ---------- Header ---------- while i < n: @@ -144,6 +156,7 @@ def extract_obs_types_v3(line: str, obs_data: str): pass if in_header: + #region Extract RINEX - v2 Header # ----- RINEX v2 header ----- if rinex_version and rinex_version < 3.0: if label == "# / TYPES OF OBSERV": @@ -186,6 +199,8 @@ def extract_obs_types_v3(line: str, obs_data: str): raw_marker = line[0:60].strip() # v2: first 4 chars are the station ID marker_name = raw_marker[:4] if len(raw_marker) >= 4 else raw_marker + elif label == "MARKER NUMBER": + marker_number = line[0:20].strip() or None elif label == "REC # / TYPE / VERS": receiver_type = line[20:40].strip() elif label == "ANT # / TYPE": @@ -212,6 +227,9 @@ def extract_obs_types_v3(line: str, obs_data: str): elif label == "END OF HEADER": in_header = False break + #endregion + + #region Extract RINEX - v3 / v4 Header # ----- RINEX v3/v4 header ----- else: if label == "SYS / # / OBS TYPES": @@ -266,6 +284,8 @@ def extract_obs_types_v3(line: str, obs_data: str): pass elif label == "MARKER NAME": marker_name = line[0:60].strip() + elif label == "MARKER NUMBER": + marker_number = line[0:20].strip() or None elif label == "REC # / TYPE / VERS": receiver_type = line[20:40].strip() elif label == "ANT # / TYPE": @@ -292,9 +312,12 @@ def extract_obs_types_v3(line: str, obs_data: str): elif label == "END OF HEADER": in_header = False break + + #endregion else: break # safety + # Check if we found the RINEX version in the header if rinex_version is None: raise ValueError("Could not determine RINEX version.") @@ -303,7 +326,9 @@ def extract_obs_types_v3(line: str, obs_data: str): r'^\s*\d{2,4}\s+\d{1,2}\s+\d{1,2}\s+\d{1,2}\s+\d{1,2}\s+[0-9.]' ) + # ---------- Body ---------- if rinex_version < 3.0: + # region Extract RINEX - v2 Body # ---------- RINEX v2 body ---------- # YY or YYYY MM DD hh mm ss.sssssss FLAG NSAT [SATLIST...] epoch_re = re.compile( @@ -362,8 +387,9 @@ def extract_obs_types_v3(line: str, obs_data: str): found_constellations.add(system_mapping[sys]) i = j - + #endregion else: + #region Extract RINEX - v3 / v4 Body # ---------- RINEX v3/v4 body ---------- while i < n: line = lines[i] @@ -410,6 +436,7 @@ def extract_obs_types_v3(line: str, obs_data: str): found_constellations.add(system_mapping[sys]) i = j + #endregion # ---------- Safety checks ---------- if not start_epoch: @@ -419,6 +446,7 @@ def extract_obs_types_v3(line: str, obs_data: str): if epoch_interval is None: raise ValueError("Epoch interval could not be determined") + #region Extract RINEX - v2 Type Conversion # ---------- RINEX v2 observation type conversion ---------- # Convert v2 obs types (C1, C2, P1, P2) to v3 codes using YAML config mappings if rinex_version and rinex_version < 3.0: @@ -468,6 +496,9 @@ def extract_obs_types_v3(line: str, obs_data: str): # Clean up temporary attribute if hasattr(self, '_v2_obs_types'): delattr(self, '_v2_obs_types') + #endregion + + #region Observation Codes Management # Cull observation types to only L-codes (converting C to L) def cull_observation_codes(obs_list): @@ -497,7 +528,7 @@ def cull_observation_codes(obs_list): # Return sorted list to maintain consistent order return sorted(list(l_codes)) - # Apply culling to all observation types (only for v3/v4 files) + # Apply culling to all observation types (only for v3 / v4 files) # For v2 files, codes are already converted to L-codes with correct priority order if not (rinex_version and rinex_version < 3.0): obs_types_by_system['G'] = cull_observation_codes(obs_types_by_system['G']) @@ -612,12 +643,16 @@ def reorder_by_priority(rinex_codes, priority_order): Logger.console(f"QZS (J): {obs_types_by_system['J']}") Logger.console("======================================================") + #endregion + + # Final RINEX extraction result return { "rinex_version": rinex_version, "start_epoch": start_epoch, "end_epoch": end_epoch, "epoch_interval": epoch_interval, "marker_name": marker_name, + "marker_number": marker_number, "receiver_type": receiver_type, "antenna_type": antenna_type, "antenna_offset": antenna_offset, diff --git a/scripts/GinanUI/app/resources/Yaml/default_config.yaml b/scripts/GinanUI/app/resources/Yaml/default_config.yaml index 1cbd0fb23..70fa52de3 100644 --- a/scripts/GinanUI/app/resources/Yaml/default_config.yaml +++ b/scripts/GinanUI/app/resources/Yaml/default_config.yaml @@ -10,8 +10,12 @@ inputs: gpt2grid_files: [tables/gpt_25.grd] #AUTO tides: - ocean_tide_loading_blq_files: [tables/OLOAD_GO.BLQ] #AUTO # Required if ocean loading is applied - atmos_tide_loading_blq_files: [tables/ALOAD_GO.BLQ] #AUTO # Required if atmospheric tide loading is applied + ocean_tide_loading_blq_files: #AUTO # Required if ocean loading is applied + - tables/OLOAD_GO.BLQ + - "*_ocean.BLQ" + atmos_tide_loading_blq_files: #AUTO # Required if atmospheric tide loading is applied + - tables/ALOAD_GO.BLQ + - "*_atmos.BLQ" ocean_pole_tide_loading_files: [tables/opoleloadcoefcmcor.txt] #AUTO # Required if ocean pole tide loading is applied snx_files: @@ -57,12 +61,15 @@ outputs: trace: level: 2 output_receivers: false - output_network: true #USER_SET + output_network: false #USER_SET receiver_filename: __.TRACE network_filename: __.TRACE output_residuals: true output_residual_chain: true output_config: true + sinex: + output: false #USER_SET + filename: __.SNX @@ -226,7 +233,6 @@ processing_options: enable: true lag: -1 # interval: 86400 - inverter: LDLT # Inverter to be used within the rts processor, which may provide different performance outcomes in terms of processing time and accuracy and stability filename: _.rts periodic_reset: # enable: true diff --git a/scripts/GinanUI/app/resources/assets/icons.qrc b/scripts/GinanUI/app/resources/assets/icons.qrc index ed16b18e8..063cc8f57 100644 --- a/scripts/GinanUI/app/resources/assets/icons.qrc +++ b/scripts/GinanUI/app/resources/assets/icons.qrc @@ -12,5 +12,11 @@ checkbox_unselected_hover.png checkbox_unselected_pressed.png checkbox_disabled.png + enlarge.png + enlarge_hover.png + enlarge_selected.png + open_in_browser.png + open_in_browser_hover.png + open_in_browser_selected.png diff --git a/scripts/GinanUI/app/resources/assets/icons_rc.py b/scripts/GinanUI/app/resources/assets/icons_rc.py index 0fe27ea1e..10e9dc951 100644 --- a/scripts/GinanUI/app/resources/assets/icons_rc.py +++ b/scripts/GinanUI/app/resources/assets/icons_rc.py @@ -6,6 +6,55 @@ from PySide6 import QtCore qt_resource_data = b"\ +\x00\x00\x02\xe6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x000\x00\x00\x000\x08\x06\x00\x00\x00W\x02\xf9\x87\ +\x00\x00\x02\x9bIDATx\x01\xec\x99mN\xdb@\ +\x10\x86g9E\xd9K\xb4R+\xb5q\xfa\xa3\x95Z\ +\xa9j\xd6\x17\xe9%\x9a\x1e\xa2\xbdH\x9c\xb6\x12H\xf0\ +\x83\x10\x90@\x82K\x04N\xc12\x83\xd9\xe0\xc4\x9ee\ +&\xde\x10>le\xb5\xde\x8f\x99}\x9f\x1d\xc7k\xaf\ +\xb7\xe0\x89\x1f\x1d\xc0\xa6\x03\xd8E\xa0\x8b@\xcb\x19`\ +/!\x9b\xe5^\x9e\xdcP\xab\xc3fnh\x15cp\ +\xfeY\x00\xce\xe0\xb1\xd5\xbf4\x00?\x9e\x1d\x8eL=\ +\x15\xeaKhvX\x0c\xeb~F\x06\xc0\x8f5QV\ +F\xc0\x0cl\xe6\x0a\xcd\x00\x9a\xbe\xa5o3\xd0\xd8\x08\ +\x01\xaa\xb3\xb2\x1e\x88\xba\xf8\xea\x98<\x92\x08\x00\xc3\xed\ +\x16C\x9b\x16\xa2I|9&/<\xb4\x88\x00\xa8s\ +\xe9\xb0:+i \xda\x88']b\x00\xea\x9c\x1a\xa2\ +\xadx\xd2\xa4\x02 \x83T\x10)\xc4\x93\x1e5\x00\x19\ +5A\xbc\xea\xbb\xef\xd4&Ie\xdf\xea\xdd\x86n\xcf\ +\x05\xfe\xcf$\xd6\x8b}\x22\x00\xfe\x17\xfeqo\xd3\xa2\ +\x11\x95\xaa\x10W\xc6\x0f.'\xc5_\xaa\x97$\xeaK\ +6e\xdf\x98\xf8\xb8\x06\xb2g\x01P .4\xb4\xd8\ +\x14\xec\x22\x85}\x1c\x09!A\xe4L\x93\xc8\x86l\xc9\ +\x07g\x87m\xf7j`\x018\xa7\xcb\xf5$d\xb9N\ +Znc\x1b\xc6h\x0d\x10\x1cm*W\x00lJb\ +|\xdc\x0e >?\xebo\xed\x22\xb0\xfe9\x8e\x8f\xd0\ +E >?\xebo\xed\x22`\xb3\xfc\xc7\xaa\xf3\x5c>\ +\xd4\xadj]\xda\xb1\x11\xb07\xfb6\xb4w\xe3\xd8g\ +!\x9b\xe5g\xe8\xe67\xe6j\x08\x12\xbf\xe5\xcd\xd8F\ +\xde\xb1\xb1\x0d\xf7\x8e\xe2\x1aX\x00\x00\xf3\xf3.A\xed\ +\xb0\xbd\xfc\x04+_c\xa2\x9f\x0a\x22\x88'C\x80\xd8\ +\x9b]\x5c\x03\xe0\x11\x01\xc0V\xe6\xb7\xddsG`\xe0\ +m\xa5\xf9\x1c\xb7H\xfeT\xca\xd1\xd3\xf2!.\xcd\xeb\ +\xa9\x1a\x00g\xfe\xc0\x18\xf3~\xae\xd0\xc3)\x8a\x7f3\ +/\x0bO\xf0Q\xd9\xc1\xc2\x1eP,\x12\xbcS\x15\xc0\ +v?\xdf\xc3\x99\xef\x07w\xde\xfb\xe3\xd9t\xf4.\x94\ +\xb5y\x0a\x081\x80\xed\xb9\x1d\xe3\xe1\x13\x84\xc3\xc3\xe4\ +bZ|\x08\xc5U\xf3\xb6\x10\x22\x00\xdb\xcb\xff\x811\ +_\x82Ho`\x1fg\xfec(\xb7\xcd\xdb@\x88\x00\ +\xc0\xc0\xb7\xb9H\xefw/&\xa3\xcf\xf3r\xa2\x93&\ +\x08\x89k\x19@\xf0\xe4\xe1\xfflZ|\x0d\xc5\xd4y\ +\x1d\xe2\xfe\x11t\x00\x18\x09\xdb\xf8Q\xc2\xb1\x8b\x1d'\ +\xc1\xde,\x94\xf5\x8f(\x80\xeb\x02(\x0e\x1d\x80\xc2\xf1\ +Cu}\xbe\x00\xb885|\xc8h\xfa\xb8AuK\ +{G\x82\xe9\xc7\xeb\x1d\xf7|\xc8V\x968\x97\xcf7\ +\x02\x1c\xf1c\xab\xef\x22\xb0\xe9\x88<\xf9\x08\x5c\x03\x00\ +\x00\xff\xff\x22\xdf\xa1\x85\ +\x06\xb8\xb8\xca~\x1b&\x80~u\x0b\x97\xc5r\xa0\xde\ +V*\xc6\xc1\xc1d\x01p\x16\x8a\x15\x93\x00bU\x1e\ +\xf7M\x0e`%b\xf5\xc9\x81X\x95\xc7}o\xc9\x01\ +d\x1eW/\xea@6_\x94\xf6\x90D\x16\x03p\xc2\ +U\x01\xa0\x0a7\x06\x91&\x02\xe0\x04[\xf1\xa8Y\x0e\ +\x228\xc0\xb1xY\x88\xa0\x00\xb4x9\x88`\x00~\ +\xf12\x10A\x00\x8e\xc5\xeb=\x80^\xa2d7\xb6s\ +8\x13\xee;1\x18\xe0\x94\xf8z\xbb~D\xa9\xd8\xbb\ +\xb9\xf0\x10\x83\x01&\x93\xef7\x14i*\xbdwB\xdb\ +\x99\xc3\x91;\xd7B\xfc\xcf=\x8c\xe4\x8f\x07\x03|n\ +6?\xf5\xf4\xe1\xde'\x1e%!\x84\xcd\xb1\xb98\xdf\ +\xb7\x1f\x0c\xd0l\xbcZ\xfd:a\xcd'\xef[\x13k\ +r\xbc\x81\x8c\x800\x00\x8c\x8d\xa4B\x12\x80Te\xb9\ +\xeb&\x07\xb8\x95\x92\x8a\xbb\x0d\x072\xf6C\x8eE\xe9\ +\xabt\xd6\xfc\xc9\xc9Y\x0fM|k\xd9\xf3\xa4\x03J\ +\xc3\x07\x8c\xa4ui!\x01\xbev\xd53hx\x8d\xce\ +`44Z\x08!$\x80\x8d\xafwUq\xde]\xe9\ +\xb5\xbb\x84L\xb2\xf9\xb5-\xdb\x5cj\x9eq\xd7\xdbh\ +0\xcb\x91\xafN\x002kD'\x12@l3\x92\x03\ +\xb1\x1d\xf8\x03\x00\x00\xff\xff\xc1\x07\xa4\xe8\x00\x00\x00\x06\ +IDAT\x03\x00m%LpMa\xb1\x8e\x00\x00\ +\x00\x00IEND\xaeB`\x82\ \x00\x00\x01w\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -131,6 +214,97 @@ Z\x1f\xf9\x92\x88\xae\xeb\x1ey\x9e_\xb9\x05\x00X\x14\ \xa5>:\xd9HN\x8c\x13l%\x00\x00\x00\x00IE\ ND\xaeB`\x82\ +\x00\x00\x035\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x000\x00\x00\x000\x08\x06\x00\x00\x00W\x02\xf9\x87\ +\x00\x00\x02\xeaIDATx\x01\xecYm\x8a\xd4@\ +\x10\xedx\x9d\x89(((\xe8\x0f\x05\x05q=\xc2\x8c\ +\x07\xf0\x0aF\x1cq<\x82\x1e\xc0\xd9#\xb8*(\xe8\ +\x0f\x05\x05\x05\xc5\xd9\xf3\xb4\xaf\xde\xa6\x9bL:\x9dT\ +'3d?\xb2L\xa7\xab\xba\xbb\xde\xab\xd7\x95\x9dd\ +\x92K\xe6\x8c\xffM\x02\xc6.\xe0T\x81\xa9\x02\x03w\ + z\x0a\xe5\x8b\xc2\xaa\xdb\xbcX\xa6\xe6\x91#F\x8d\ +\x8f\x5cb\xf8Q\x01\xb1\x80\xd36~\xc1\x04\xd8\xech\ +\xb3^eA;\x5c%\x9fB\x1b\xc4\x048\xc06\xe0\ +H\xa9rZ\x052{\x90\xcf\x9f\xbdK!HYK\ +lp\xa4\xc4\xe8\x04Tw\x05\x04$JaQ\xac%\ +&\xb0\xfd\xd2*\xa7\x1f\x0c\x0d\x95\x80\xcd\xe1\xcbG[\ +\xa5\x05\x11\x09C\xbc^#\xc4\x02\xa6\x0fF\xf2\xe4\xf4\ +\x03qC%@\xc2\x09\x08`\xb1\xd9@Hb:\xfd\ +\x0f\xc4\x00\x96G\x00\x07\xb9\xfc@\xbb\xa1\x16 0\x04\ +\x06\x81\xd8l f\x02t\xd2\x0f\x8c\x05\x86\x8f\x046\ +9\xfc@\xb7\x91$@\xe0H\x00\x22\xb1\xd9\x90\x00\x13\ +\xa1\xa3?0\x06\xb1>\x02\x98\xc4\xf6\x03:#Y\x80\ +\xc0\x92\x08\x84b\xb3!\x91\xcb\x8b\xe2!m\xc5\x81k\ +\x11\xe3\x97\x02\x8b\x98~@o\xc4\x05X\xf3\xc2\xb8\xd6\ +\x80GB\x10\xcbTf\xcc\xc1\xbf\xf5\xea\xbd\xd8\x9a&\ +k%\x86k\x81A,:\xb5\x83\xe3\x97\xbe6\xe5\xdc\ +\xa8\x00^h\xe4b\x83\xe6\x16\xd7{!\x96D$\xa1\ +\xfa\x5c\x97/1\x12+\x18\xb1\xb5\x9a\x1c\xa2\x02b\xa0\ +\xf5qI\xa4>\xa6\xf5\x87\xc4:\x8e\xc1\x02\x1c\xd0X\ +}\x82\x80\xb1Rl\xe7\x9d\x04\xb4\xef\xcf\xfeg\xa7\x0a\ +\xec\x7f\x8f\xdb\x19\xa6\x0a\xb4\xef\xcf\xfeg\xa7\x0a\xcc\x1e\ +?}\xd2w\x9fyS\xd77\xb8\x8c\x8bV \x97\xe7\ +6e+\xd7\x06\x1d\x9e\xeb\xfc\xc9l\xf6\xba\x8f\x08I\ +\xde\x1as\x94\xb7\xfc\xc6\xceK~\xe9\x03\xf2r *\ +\xc0d\xe6\xb9o\xe5\xe2j\x07\xd0_\xf0\xaf\xa0\x99T\ +\x11.y\x89Ep\xfcAAG\x0e\x12\x1f\x17 \xb3\ +\x916[\x14? \xeeZe\xfa\xef\xf1\xdbWo*\ +~\xab\xc9\x9b8\xdcF\xfbE\xf8m\x90\xb7T\xc2\xaf\ +k0\x92\x05\xe4\xf3\xe2\x1bn\x83ox,k~\xe3\ +\xf9\xceU\xef+\x0d\xdeF\xef@D\x92\x00\x9c\xf3_\ +\xb0\xf3\xb7\x5c\x8e8\x87\x7f\xe2\x9e\xfd\xba\xf3S\xfb]\ +\x88P\x0b@\xf2\x9f\x90\xe0\x1d\xb4\x93\x8f5\xdf\x8f\xd7\ +\xab\x9b'N\xff\xe3P\x11*\x01\xb3y\xf1\x01)\xde\ +Cs\x9f\xaf\xd8\xf9\xdb\xce\x19\xda\x0f\x11\xa1\x12\x90e\ +\xe6A%\xc9\xcf8\xe7\xefV\xfc\x9d\x98M\x224\xc0\ +*\x01\x0e\xc8Z\xf3\x11\xc9\xdfw\xfe\xae\xfb@\x84\x82\ + I\x80T\x02\xff\x0b\xe1\x8b\x0f\x5cp\x14\x5c[K\ +\xf0m\xb6l\xc2\x92\xeb\xc2\xd6\xc2\x0e'I@\x07\xd6\ +(\xd3\xe7W\x00\xce\xf5\xf0E\x06^@4\x8e\xd7\x9f\ +\x1d)j\x81o\xb1e#V\x84#\x06y~+\x10\ +S|\xda\xc6\xa7\x0a\x8c]\x913_\x81\xff\x00\x00\x00\ +\xff\xff\x01\xe0\xe1\xd2\x00\x00\x00\x06IDAT\x03\x00\ +;2\x94p\xf3\x8f\x1b\x12\x00\x00\x00\x00IEND\ +\xaeB`\x82\ +\x00\x00\x02*\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x000\x00\x00\x000\x08\x06\x00\x00\x00W\x02\xf9\x87\ +\x00\x00\x01\xdfIDATx\x01\xec\x99\xcdR\x830\ +\x10\xc7\x83\xc33\xf8\x12V\x1f\xc7\x93\xbc\x83\x9e\xad\x96\ +j\x9d\xf1\xa6\x8f\xe0\x0c\xde|\x1c\xf1\xee\x9d\xab\x87^\ +\x1c\xdc]\x0a\x83\xad\x9blIBp\x9a\x0e\x09a?\ +\x92\xff/\xdb\x19\x068R\xff\xfc\x17\x01B\x170V\ + V\xc0r\x07\xb4\x7f\xa1\xd3\xec\xfai\x96\xcd\xeb\x90\ +\x0d5\xe8\x18Y\x80\x93\xecfY\xab\xe4R\x97<\x86\ +\x0f5\xa0\x16n-\x16 Q\xf5-\x974\xb6=\xd1\ +ha\x01\xc6\x169t\xbd\x080t\xe7\x5c\xe5\x89*\ +P\x16\xab$D\x93@\x8a\x00$\x13\x85\x8a\x89\x00\xa1\ +v\xbe]7V\xa0\xdd\x89P\xe7X\x81P;\xdf\xae\ +{H\x15h\x99\xa7u\xf6Z\x81\xd9\xc5<\xc7\xe6\x13\ +\xd9\x1b\x00\x09O\xd4BA\xa3\xb1'\x0a/\x00$\x18\ +\x84w\x9aaL\xb6\xce\xe0n\xe0\x1c\x80\x84\x82\xe0\x1d\ +\x89`#\xdf\x8e\xc3\xce\xe0\x14\x80\x04\x82PV\x12\xf8\ +(\x86\x0d\xd8\xdf\xe1\x0c\x80\x84\x81@\xa3\x04\x88\xa1X\ +c\xa0,\xc0\x09\x00\x09\x02a\xbd%+U\xabew\ +\xdd\x8c\xab\xee\x1ab)\xa73\x0c\x1fX\x03\x90\x10\x10\ +\xd4\x93P\xc1\xc3\xcfq\xef\x9a\x86\x1b\x9bs\x08k\x80\ +\xaf4}$\x85M\xf7\xa7\xf8\xc6\xa5\xd46\xc4Vn\ +\x1b\xb6\xd7\xd9\x1a\xe0\xf3%_\x97\xeb\xb3\x14V\xd5\x8a\ +\x07?\x1d-\x04\xe6`.\x19-:k\x00Z\xfb\xed\ +\xfc{#\x8c.M\x1d\xc5B\x8e)N\xe2w\x03 \ +Y\xc9SL\x04\xf0\xb4\xb1\xe2ic\x05\xc4[\xe5)\ +\xf00* \xfe\xc0\x01\x0f0\xa6\x8d\xc6;\xb7t>\ +\xd3\x5c\xe8g+\x00\xef\xe4\x9f1`\x0aM\xa7\x85\x05\ +x/\x1e\xae\xe0\xeb\xc8]h\x00\xd4\x80Z8\x1d,\ +\x00&|\x14\xf7\x0b\xb8k\xca\xdfL\xbf\xaer\xccS\ +\xd0\x950\xeera\x0c&:~\xd9\x05o\xbdQ\x03\ +%2\x9d\x16\x80\xc9\x99\x949\x02\x84.G\xac@\xe8\ +\x0a\xfc\x00\x00\x00\xff\xff\x03Z\xfc\xa4\x00\x00\x00\x06I\ +DAT\x03\x00u\xf6Hp3o\x80\x93\x00\x00\x00\ +\x00IEND\xaeB`\x82\ \x00\x00\x02\xf5\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -245,6 +419,42 @@ \xa7H\xe1\x94\x11\xb8\xc5\x8e\xaa\xa5\xe2>\xb6\x04A\x10\ }\x05c\x9f\xc3]\xf0\xc1*\x5c\xab\x9f\x00\x00\x00\x00\ IEND\xaeB`\x82\ +\x00\x00\x02\x1c\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x000\x00\x00\x000\x08\x06\x00\x00\x00W\x02\xf9\x87\ +\x00\x00\x01\xd1IDATx\x01\xec\x99MN\xc30\ +\x10\x85\x9d\x8a\x1d{\x0e\x00\x5c\x86\x1e\x82^\x01.@\ +[V\xec\xe0\x08\x94m\xf7e\xcbA(b\xdf}\xc5\ +\x06\x82\x9f\x9b\x91\xa2\x88\xb1\x87\xd8\xaeSu\xa2\xc4?\ +\xe3\x19\xfb{3\x95\xa2\xb6#s\xe0\x97\x0a(]@\ +\xad\x80V 2\x03\xde\x8f\xd0\xe5\xd5\xe4\xf1b<\xa9\ +K>`\xf0id\x05\xd8\xc0y]\x99\x1bS\xf8\x02\ +\x03X8\x0cV\x80\x0d\xbc\xe3\x82\xf6m\xf7\xb1\xb0\x02\ +\xf6\x0d\xd9\xf7<\x15\xd07s\xa9\xe2D\x15X\xaf\x16\ +U\x89G\x22R$@\xb2Q)\x1f\x15P*\xf3t\ +\xaeV\x802Q\xaa\xd7\x0a\x94\xca<\x9d{L\x15 \ +\xcd\xc3\xea\xb3V\xe0||=\xc3\x93Sr6\x01\x00\ +\xafL5\xc5\x83q.\x11Y\x04\x00\x18\xe0\x04\x8d1\ +l4O\xd9'\x17\x00P\x00w!a\xc3Z\xd7\x1e\ +;O*\x00\x80\x00\xe5\xa0\xb0\x06\x1fn\xbd\x8f=\x99\ +\x00\x80\x010\x04\x01\x1f\xf8\x86\xfc\xa4\xebI\x04\x00\x08\ +`\xadC7\xb5\xa9\xe74o\xc6\x1b\x9a\xc3\x1714\ +\x8f\xe9\xa3\x05\x00\x04@-\x88\x8d\xfd\xf2s\xd6\x9a\xbb\ +acK.\x22Z\xc0\xcfv\xf4\xe0\x08w\xcd\x9f\xf0\ +\xbb%c\xba\x22:\xb1\xe4\xf6\xaf>Z\xc0\xe7\xdb\xf3\ +\xd7\xfat{bO\xf5\xc2\xdbuw\x93\x08\xc4 \xd6\ +\x19#\x9ah\x01\xee\xec\xe5\xf2\xbb\x01s\xd3P\xe3|\ +mL\xc8O\xb2\x9eF\x80\xe4\xa4L>* Sb\ +\xc5\xdbj\x05\xc4\xa9\xca\xe4x\x1c\x15\x90\xfe\xc1\x81\xb7\ +r(\xd1\xf0\x91\xee\x17\xda\x0b\xebl\x05\xaa\xda<\x99\ +\x81\x5c>\x16V\xc0\xfb\xeb\xe2\xd6\x06\xde\x97\xd6\x00\x06\ +\xb0p\x1c\xac\x00\x04\xd8\xc0\xa9}k\x8a\x7f\x99\xfeX\ +\xbd\xcc\x10gl\x831\xc5blM\xee\xc6\x98\xec\x92\ +\x1e\x0c.\x90i\xbc\x02\x98\x98A\x99U@\xe9rh\ +\x05JW\xe0\x17\x00\x00\xff\xff>\xfcgK\x00\x00\x00\ +\x06IDAT\x03\x003\x0dLpP\xed\xdeG\x00\ +\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x01[\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -393,6 +603,55 @@ \x9b\xa5{\xbd\x19\xb9$::\x820V:[w\xd4\ \xd4#\x11\xd1W\x119\x03\xa9\xde<9{\x14\x92\x1c\ \x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x02\xe3\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x000\x00\x00\x000\x08\x06\x00\x00\x00W\x02\xf9\x87\ +\x00\x00\x02\x98IDATx\x01\xecY\xddN\xdb0\ +\x14vx\x94\xb2\xfbM\xda\xa4M\xda.6i\x93\xba\ +v\xcf0z\xbd\x97\x18{\x88\xed\xba\xec\x19\xd6\x824\ +$\xb8\x00\x09$\x90\xe0\x1e\xfa(\x98\xef\x0b\xb6H\x13\ +\x9f\xf48N\x15~R\xf9\xc49\xf6\xf9\xf9>\x1f\x13\ +\xe2v\xc3<\xf2OO\xa0\xeb\x02\xf6\x15\xe8+\x90\xb8\ +\x02\xe2\x16\xda\x1cmY\xad\x0cF\xdf\xb7cq\xd0G\ +\x1b\x9fvR|\x91\x80\xe4\xf0\xd0\xc6\x9f\x1d\x81\xd9\xf5\ +|'+\xcbb\xfe7z\x0b\xd1\xa7\x1c\x87:*<\ +\x83\xa8[l\x05\xc6\xd8\x8f\xff\xd4\xd1#\x0d]\xecq\ +\x8c\x9b\x96@qU\xd6B\x22\x00\xbe\x98S\xe4\xa4\x22\ +\x80\xd2~C\x84b\xc0VI\x84\xc0\xbb\x9cH[\xdf\ +T\x04\x18\xc2\x05l\x9dD\x0ax\xe2R\x13\xa0q\xdb\ +$R\xc1\x13S\x14\x01:\xb4E\xa2\x0d\xf0\xc4\x13M\ +\x80N!\x12\x83\xe1d\xc49\x8d8\xdb\xe2\xd3\x86\x8f\ +g\xfe\x9di\xdc\x97lD\x02\xd6\xd8_^\x96<\x9c\ +R$ao\xb2\xf1bo:wS+;\xda\xd2\xc7\ +\x19\x8a\xe0}~\xf6\xce\xb6\xd2\x89\x04\xf8\x8f\xc6K\xc5\ +\xcb\x0d\x90\x04\x81\x10\x90\x1bRw\xf4\xa1/cHN\ +>?{\xc9F$ 9\x94\xc7\x09\xa4<\xa6\xd5S\ +|}\x8ed\x02>PW}\x04\x81\xae \xd6\xe7\xed\ +\x09\xd4\xaf\xcf\xfag\xfb\x0a\xac\x7f\x8d\xeb3\xf4\x15\xa8\ +_\x9f\xf5\xcf\xf6\x15x1\x9e\xfch\xba\xce\xee\xa5\xae\ +\xa9{\xee'V\x80\xdf\xdbx\xc9-\x03\x97\xc1hr\ +a\xad\xfd\xdd\x84\x04\xc1g\x1bv\xe6^\xab\x03\xd1\x8d\ +\xf1\xf9\xd9\x07\x0d0(\x12\xc8L\xf6\xd3\x0b\xec*\x0d\ +\x89\xcf2c_r\x22\x96\x84\x07O_\x88x<\xf5\ +\xf9\xd9\xc3.\xd8D\x02Ak7\x88\x159\xc1\xedk\ +H\xde\xac\xc9.\xaff\xd3?\xb9\xa2\xb8\xb8\x97\xb8V\ +\x8e\xa7\xd1\x04\xb0\xf2GX\x91\xb7\x05\x9c\xe7\x8b\xf9\xf4\ +UAW\xdd\xba\xd7\xe8d\x12Q\x046\xbfn\x1d\x00\ +\xdd{H\xdep\xd08\x05\x907\xb9\xd2\xe0\x02_\x9e\ +\xc2\x92H\xa8\x09`\xe5\xff\x9b\xcc|4\xf7\x9fc\x1c\ +4\xde\xdd\xab\xcd\xeeRI\xa8\x08\x00\xfc.\xe0}\x86\ +\xdc5k\x0e\x91\xf8\xc3\x9d\x92~E\xac\xc6\x95P\x11\ +\x00\xc4!\xc4\xb7\xfd\xeb\xdd\x9dO^i\xab\x0f\x91\xd0\ +\xc4\xd6\x12\xf0\xb1\xf6\x90\xe8\x8bW\xda\xee\x11\xbb\x5c\x89\ +\x95)b\x09\x0c\xb1\x9d*?|\xe0\xb1\xba\xbd2S\ +\xc9\x80>\xa1X0+~\xdd\x02\xb5\xbe\xc5\x12\xa8\x8f\ +\xd6\xc1\xec\xd3%\x80\xfdX\xf9!C\x1a\xc3\xe3ty\ +\x0b)*A\x1f)^h\x5c\x0a\xf9t+ 1~\ +h\xe3}\x05\xba\xae\xc8\xa3\xaf\xc0-\x00\x00\x00\xff\xff\ +qvn\x8a\x00\x00\x00\x06IDAT\x03\x00\x0f\x8f\ +yp\xaa\x90\x94\x06\x00\x00\x00\x00IEND\xaeB\ +`\x82\ \x00\x00\x01p\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -425,6 +684,11 @@ \x00\x06\xfa^\ \x00i\ \x00c\x00o\x00n\ +\x00\x14\ +\x03-\x00\xe7\ +\x00e\ +\x00n\x00l\x00a\x00r\x00g\x00e\x00_\x00s\x00e\x00l\x00e\x00c\x00t\x00e\x00d\x00.\ +\x00p\x00n\x00g\ \x00\x11\ \x06$\x0d\x87\ \x00h\ @@ -434,11 +698,25 @@ \x04\xa9\xf2'\ \x00h\ \x00e\x00l\x00p\x00_\x00h\x00o\x00v\x00e\x00r\x00.\x00p\x00n\x00g\ +\x00\x1c\ +\x00\xad1\x87\ +\x00o\ +\x00p\x00e\x00n\x00_\x00i\x00n\x00_\x00b\x00r\x00o\x00w\x00s\x00e\x00r\x00_\x00s\ +\x00e\x00l\x00e\x00c\x00t\x00e\x00d\x00.\x00p\x00n\x00g\ \x00\x15\ \x07Sl\xa7\ \x00c\ \x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\ \x00.\x00p\x00n\x00g\ +\x00\x0b\ +\x08\x10\xf2G\ +\x00e\ +\x00n\x00l\x00a\x00r\x00g\x00e\x00.\x00p\x00n\x00g\ +\x00\x13\ +\x08\xc7nG\ +\x00o\ +\x00p\x00e\x00n\x00_\x00i\x00n\x00_\x00b\x00r\x00o\x00w\x00s\x00e\x00r\x00.\x00p\ +\x00n\x00g\ \x00\x08\ \x0c3Z\x87\ \x00h\ @@ -453,6 +731,11 @@ \x00c\ \x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00s\x00e\x00l\x00e\x00c\x00t\x00e\x00d\ \x00_\x00p\x00r\x00e\x00s\x00s\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x19\ +\x0dI\x9b\xa7\ +\x00o\ +\x00p\x00e\x00n\x00_\x00i\x00n\x00_\x00b\x00r\x00o\x00w\x00s\x00e\x00r\x00_\x00h\ +\x00o\x00v\x00e\x00r\x00.\x00p\x00n\x00g\ \x00\x1d\ \x05\xf8\xd7\x07\ \x00c\ @@ -478,6 +761,11 @@ \x00c\ \x00h\x00e\x00c\x00k\x00b\x00o\x00x\x00_\x00u\x00n\x00s\x00e\x00l\x00e\x00c\x00t\ \x00e\x00d\x00_\x00p\x00r\x00e\x00s\x00s\x00e\x00d\x00.\x00p\x00n\x00g\ +\x00\x11\ +\x0c\xc2\xa3\xa7\ +\x00e\ +\x00n\x00l\x00a\x00r\x00g\x00e\x00_\x00h\x00o\x00v\x00e\x00r\x00.\x00p\x00n\x00g\ +\ \x00\x17\ \x04\x93\xc8\x07\ \x00c\ @@ -488,31 +776,43 @@ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x12\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x9e\x00\x00\x00\x00\x00\x01\x00\x00\x0a_\ +\x00\x00\x00\x86\x00\x00\x00\x00\x00\x01\x00\x00\x08\xd5\ +\x00\x00\x01\x9c\xbc\x1d\xae\x13\ +\x00\x00\x01R\x00\x00\x00\x00\x00\x01\x00\x00\x14\xaf\ \x00\x00\x01\x9b\x8b|\xd9\x1d\ -\x00\x00\x02\x12\x00\x00\x00\x00\x00\x01\x00\x00\x15E\ +\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x9c\xbc\x1d\xdcr\ +\x00\x00\x02\xfe\x00\x00\x00\x00\x00\x01\x00\x00!\xb5\ \x00\x00\x01\x9b\x8b\x7f\x84`\ -\x00\x00\x02V\x00\x00\x00\x00\x00\x01\x00\x00\x16\xa6\ +\x00\x00\x03j\x00\x00\x00\x00\x00\x01\x00\x00%\xfd\ \x00\x00\x01\x9b\x8b|\xb8I\ -\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x02\xf4\ +\x00\x00\x00d\x00\x00\x00\x00\x00\x01\x00\x00\x05\xde\ \x00\x00\x01\x9c/\xcb\xb2\x98\ -\x00\x00\x01 \x00\x00\x00\x00\x00\x01\x00\x00\x0e\x17\ +\x00\x00\x02\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x1a\x87\ \x00\x00\x01\x9b\x8b}Y\x86\ -\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00<\x00\x00\x00\x00\x00\x01\x00\x00\x02\xea\ \x00\x00\x01\x9c/\xcb\xa4\x8b\ -\x00\x00\x00X\x00\x00\x00\x00\x00\x01\x00\x00\x05\xeb\ +\x00\x00\x00\xc4\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xd4\ \x00\x00\x01\x9b\x8b|\xd9\x1d\ -\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xda\ +\x00\x00\x01\x94\x00\x00\x00\x00\x00\x01\x00\x00\x16*\ \x00\x00\x01\x9b\x8b\x7f\xec\x90\ -\x00\x00\x01`\x00\x00\x00\x00\x00\x01\x00\x00\x0fv\ +\x00\x00\x00\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x0cO\ +\x00\x00\x01\x9c\xbc\x10\xa6\xa7\ +\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00\x0f\x88\ +\x00\x00\x01\x9c\xbc\x1b#\x8f\ +\x00\x00\x02L\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xe6\ \x00\x00\x01\x9b\x8b|\x97\x0f\ -\x00\x00\x01\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x13\xc5\ +\x00\x00\x02\xb8\x00\x00\x00\x00\x00\x01\x00\x00 5\ \x00\x00\x01\x9b\xc3\xf0\x1b\xf5\ -\x00\x00\x00\x88\x00\x00\x00\x00\x00\x01\x00\x00\x07f\ +\x00\x00\x01<\x00\x00\x00\x00\x00\x01\x00\x00\x11\xb6\ \x00\x00\x01\x9c/\xcb\xbe\x9e\ -\x00\x00\x01\x90\x00\x00\x00\x00\x00\x01\x00\x00\x11\xbb\ +\x00\x00\x03B\x00\x00\x00\x00\x00\x01\x00\x00#\x16\ +\x00\x00\x01\x9c\xbc\x10\xd2\xeb\ +\x00\x00\x01\xd4\x00\x00\x00\x00\x00\x01\x00\x00\x18g\ +\x00\x00\x01\x9c\xbc\x1b?\x03\ +\x00\x00\x02|\x00\x00\x00\x00\x00\x01\x00\x00\x1e+\ \x00\x00\x01\x9b\x8b}\x1f{\ " diff --git a/scripts/GinanUI/app/utils/cddis_email.py b/scripts/GinanUI/app/utils/cddis_connection.py similarity index 90% rename from scripts/GinanUI/app/utils/cddis_email.py rename to scripts/GinanUI/app/utils/cddis_connection.py index 3408e8ac4..d5da3fdc8 100644 --- a/scripts/GinanUI/app/utils/cddis_email.py +++ b/scripts/GinanUI/app/utils/cddis_connection.py @@ -1,15 +1,9 @@ -# app/utils/cddis_email.py """ -Utilities for managing the EMAIL used by the CDDIS flow and for quick connectivity/auth checks. +Utilities for managing the EMAIL used by the CDDIS flow and for quick connectivity / authentication checks. -This module is used by the UI credential flow to: - • Read/Write the EMAIL value (env var first, then a local CDDIS.env file). - • Derive the email/username from `.netrc/_netrc` when the user only saved Earthdata credentials. - • Test connectivity to cddis.nasa.gov and verify Earthdata authentication via requests. - -Notes: - - This module does not present UI; it is called by UI dialogs/controllers. - - File locations are platform-aware and compatible with Windows/macOS/Linux. +Handles reading and writing the EMAIL credential (from env var or CDDIS.env), deriving +the username from .netrc / _netrc, and testing connectivity and Earthdata authentication +against cddis.nasa.gov """ from __future__ import annotations @@ -24,9 +18,8 @@ ENV_FILE = Path(__file__).resolve().parent / "CDDIS.env" EMAIL_KEY = "EMAIL" -# ------------------------------ -# Select the .netrc/_netrc path (for compatibility with different implementations) -# ------------------------------ +#region .NETRC / _NETRC Handling + def _pick_netrc() -> Path: """ Select a `.netrc`-style credential file path to use. @@ -59,6 +52,32 @@ def _pick_netrc() -> Path: return Path(os.environ.get("USERPROFILE", str(Path.home()))) / ".netrc" return Path.home() / ".netrc" +def get_netrc_auth() -> tuple[str, str] | None: + """ + Retrieve (username, password) from `.netrc/_netrc` for Earthdata auth. + + Returns: + tuple[str, str] | None: (username, password) if found; otherwise None. + + Example: + >>> creds = get_netrc_auth() # doctest: +SKIP + >>> creds is None or isinstance(creds, tuple) + True + """ + p = _pick_netrc() + if not p.exists(): + return None + n = netrc.netrc(p) + for host in ("cddis.nasa.gov", "urs.earthdata.nasa.gov"): + auth = n.authenticators(host) + if auth and auth[0] and auth[2]: + return (auth[0], auth[2]) + return None + +#endregion + +#region .NETRC / _NETRC Account Information Handling + def read_email() -> str | None: """ Read the EMAIL used by CDDIS utilities. @@ -159,27 +178,9 @@ def ensure_email_from_netrc(prefer_host: str = "urs.earthdata.nasa.gov") -> Tupl write_email(user) return True, user -def get_netrc_auth() -> tuple[str, str] | None: - """ - Retrieve (username, password) from `.netrc/_netrc` for Earthdata auth. +#endregion - Returns: - tuple[str, str] | None: (username, password) if found; otherwise None. - - Example: - >>> creds = get_netrc_auth() # doctest: +SKIP - >>> creds is None or isinstance(creds, tuple) - True - """ - p = _pick_netrc() - if not p.exists(): - return None - n = netrc.netrc(p) - for host in ("cddis.nasa.gov", "urs.earthdata.nasa.gov"): - auth = n.authenticators(host) - if auth and auth[0] and auth[2]: - return (auth[0], auth[2]) - return None +#region CDDIS Connectivity def test_cddis_connection(timeout: int = 15) -> tuple[bool, str]: """ @@ -216,5 +217,8 @@ def test_cddis_connection(timeout: int = 15) -> tuple[bool, str]: return True, f"AUTH OK, took {time.perf_counter() - start_time:.3f} seconds" return False, f"HTTP {resp.status_code} or login page returned" +#endregion + +# Test CDDIS connection if __name__ == "__main__": print(test_cddis_connection()) \ No newline at end of file diff --git a/scripts/GinanUI/app/utils/cddis_credentials.py b/scripts/GinanUI/app/utils/cddis_credentials.py index 056050472..c9de4b99f 100644 --- a/scripts/GinanUI/app/utils/cddis_credentials.py +++ b/scripts/GinanUI/app/utils/cddis_credentials.py @@ -1,24 +1,33 @@ -# app/utils/cddis_credentials.py +""" +Utilities for saving and validating Earthdata credentials in .netrc/_netrc. + +Writes login credentials for urs.earthdata.nasa.gov and cddis.nasa.gov to the +appropriate credential file for the current platform, and validates that those +credentials are present and well-formed +""" + from __future__ import annotations import os, platform, stat, shutil from pathlib import Path import netrc +# Constants URS = "urs.earthdata.nasa.gov" CDDIS = "cddis.nasa.gov" -def _win_user_home() -> Path: +def _write_text_secure(p: Path, content: str) -> None: """ - Return the Windows user home path. - - Returns: - Path: Path to the current user's home directory on Windows; falls back to Path.home() if env var is missing. + Write text to a file, applying secure permissions on non-Windows. - Example: - >>> isinstance(_win_user_home(), Path) - True + Arguments: + p (Path): Target file path. + content (str): File content to write (UTF-8). """ - return Path(os.environ.get("USERPROFILE", str(Path.home()))) + p.write_text(content, encoding="utf-8") + if not platform.system().lower().startswith("win"): + os.chmod(p, stat.S_IRUSR | stat.S_IWUSR) # 0600 + +#region .NETRC / _NETRC Handling def netrc_candidates() -> tuple[Path, ...]: """ @@ -32,21 +41,9 @@ def netrc_candidates() -> tuple[Path, ...]: ('...netrc',) or ('...netrc', '_netrc') """ if platform.system().lower().startswith("win"): - return (_win_user_home() / ".netrc", _win_user_home() / "_netrc") + return (_win_user_home() / ".netrc", _win_user_home() / "_netrc") # Windows is not consistent return (Path.home() / ".netrc",) -def _write_text_secure(p: Path, content: str) -> None: - """ - Write text to a file, applying secure permissions on non-Windows. - - Arguments: - p (Path): Target file path. - content (str): File content to write (UTF-8). - """ - p.write_text(content, encoding="utf-8") - if not platform.system().lower().startswith("win"): - os.chmod(p, stat.S_IRUSR | stat.S_IWUSR) # 0600 - def save_earthdata_credentials(username: str, password: str) -> tuple[Path, ...]: """ Save Earthdata credentials for both URS and CDDIS hosts. @@ -74,19 +71,6 @@ def save_earthdata_credentials(username: str, password: str) -> tuple[Path, ...] os.environ["NETRC"] = str(written[0]) return tuple(written) -def _ensure_windows_mirror() -> None: - """ - Ensure .netrc exists by mirroring _netrc on Windows if necessary. - """ - if not platform.system().lower().startswith("win"): - return - dot, under = _win_user_home() / ".netrc", _win_user_home() / "_netrc" - if under.exists() and not dot.exists(): - try: - shutil.copyfile(under, dot) - except Exception: - pass - def validate_netrc(required=(URS, CDDIS)) -> tuple[bool, str]: """ Validate presence and completeness of Earthdata credentials. @@ -116,4 +100,36 @@ def validate_netrc(required=(URS, CDDIS)) -> tuple[bool, str]: os.environ["NETRC"] = str(p) return True, str(p) except Exception as e: - return False, f"invalid netrc {p}: {e}" \ No newline at end of file + return False, f"invalid netrc {p}: {e}" + +#endregion + +#region Windows OS Helper Functions + +def _ensure_windows_mirror() -> None: + """ + Ensure .netrc exists by mirroring _netrc on Windows if necessary. + """ + if not platform.system().lower().startswith("win"): + return + dot, under = _win_user_home() / ".netrc", _win_user_home() / "_netrc" + if under.exists() and not dot.exists(): + try: + shutil.copyfile(under, dot) + except Exception: + pass + +def _win_user_home() -> Path: + """ + Return the Windows user home path. + + Returns: + Path: Path to the current user's home directory on Windows; falls back to Path.home() if env var is missing. + + Example: + >>> isinstance(_win_user_home(), Path) + True + """ + return Path(os.environ.get("USERPROFILE", str(Path.home()))) + +#endregion \ No newline at end of file diff --git a/scripts/GinanUI/app/utils/common_dirs.py b/scripts/GinanUI/app/utils/common_dirs.py index b3601370d..8fed946a2 100644 --- a/scripts/GinanUI/app/utils/common_dirs.py +++ b/scripts/GinanUI/app/utils/common_dirs.py @@ -1,3 +1,11 @@ +""" +Defines common directory and file path constants used throughout Ginan-UI. + +Resolves paths correctly for both development mode and PyInstaller-bundled +distributions, exposing constants for the template YAML, generated YAML, +input products directory, and user manual +""" + import sys from pathlib import Path @@ -20,8 +28,12 @@ def get_user_manual_path(): # Running in development mode - __file__ is in app/utils/ return Path(__file__).parent.parent.parent / "docs" / "USER_MANUAL.md" +# Project filepath constants +# Used to build relative file paths on the user's system BASE_PATH = get_base_path() TEMPLATE_PATH = BASE_PATH / "resources" / "Yaml" / "default_config.yaml" GENERATED_YAML = BASE_PATH / "resources" / "ppp_generated.yaml" INPUT_PRODUCTS_PATH = BASE_PATH / "resources" / "inputData" / "products" -USER_MANUAL_PATH = get_user_manual_path() \ No newline at end of file +TABLES_PRODUCTS_PATH = INPUT_PRODUCTS_PATH / "tables" +USER_MANUAL_PATH = get_user_manual_path() +INSPECTOR_HTML_PATH = BASE_PATH / "resources" / "Yaml" / "GinanYAMLInspector.html" \ No newline at end of file diff --git a/scripts/GinanUI/app/utils/gn_functions.py b/scripts/GinanUI/app/utils/gn_functions.py index 49e7595f9..f47b45e8c 100644 --- a/scripts/GinanUI/app/utils/gn_functions.py +++ b/scripts/GinanUI/app/utils/gn_functions.py @@ -1,4 +1,12 @@ -"""Base time conversion functions""" +""" +GNSS utility functions for time conversion, file handling, and downloading. + +Provides GPS week/day conversions, the GPSDate helper class, file decompression +(gz, tar, Z, Hatanaka), and URL-based download utilities with retry logic. + +These have been ripped from the base Ginan code and minimally adapted +later on this file should be removed as these functions are migrated into the gnssanalysis pip package +""" from datetime import datetime as _datetime import logging @@ -7,12 +15,10 @@ import time as _time import gzip as _gzip import tarfile as _tarfile - from pathlib import Path as _Path from typing import Optional as _Optional, Union as _Union from urllib import request as _request from urllib.error import HTTPError as _HTTPError - import numpy as _np import hatanaka as _hatanaka diff --git a/scripts/GinanUI/app/utils/logger.py b/scripts/GinanUI/app/utils/logger.py index e15afb268..8add86de7 100644 --- a/scripts/GinanUI/app/utils/logger.py +++ b/scripts/GinanUI/app/utils/logger.py @@ -2,14 +2,16 @@ Unified logging system for Ginan-UI This module provides a thread-safe logging interface that can passes -messages to different UI channels ("terminal" or "console" at the moment) via Qt signals. +messages to different UI channels ("Workflow" or "Console" at the moment) via Qt signals. +Must be initialised with a MainWindow instance +before use; falls back to stdout if uninitialised. Usage: # In main_window.py initialisation: Logger.initialise(main_window_instance) - + # Anywhere in your code: - Logger.terminal("Message for terminal") + Logger.workflow("Message for workflow") Logger.console("Message for console") Logger.both("Message for both channels") """ @@ -17,13 +19,11 @@ from PySide6.QtCore import QObject, Signal from typing import Optional - class LoggerSignals(QObject): """Signal container for thread-safe logging""" - terminal_signal = Signal(str) + workflow_signal = Signal(str) console_signal = Signal(str) - class Logger: """ Static logger class for easy logging throughout the application. @@ -44,32 +44,32 @@ def initialise(cls, main_window): cls._signals = LoggerSignals() # Connect signals to main window's log_message method - cls._signals.terminal_signal.connect( - lambda msg: main_window.log_message(msg, channel = "terminal") + cls._signals.workflow_signal.connect( + lambda msg: main_window.log_message(msg, channel = "workflow") ) cls._signals.console_signal.connect( lambda msg: main_window.log_message(msg, channel = "console") ) @classmethod - def terminal(cls, message: str): + def workflow(cls, message: str): """ - Log a message to the terminal widget. + Log a message to the "Workflow" widget. Thread-safe. :param message: Message to log """ if cls._signals is None: - print(f"[Logger not initialised - terminal] {message}") + print(f"[Logger not initialised - workflow] {message}") return # Simply emit the signal - Qt handles thread safety automatically - cls._signals.terminal_signal.emit(message) + cls._signals.workflow_signal.emit(message) @classmethod def console(cls, message: str): """ - Log a message to the console widget. + Log a message to the "Console" widget. Thread-safe. :param message: Message to log @@ -84,12 +84,12 @@ def console(cls, message: str): @classmethod def both(cls, message: str): """ - Log a message to both terminal and console widgets. + Log a message to both "Workflow" and "Console" widgets. Thread-safe. :param message: Message to log """ - cls.terminal(message) + cls.workflow(message) cls.console(message) @classmethod diff --git a/scripts/GinanUI/app/utils/toast.py b/scripts/GinanUI/app/utils/toast.py index 421c1d993..b41ef9c86 100644 --- a/scripts/GinanUI/app/utils/toast.py +++ b/scripts/GinanUI/app/utils/toast.py @@ -1,14 +1,15 @@ +""" +Non-blocking toast notification widget for Ginan-UI. + +Provides the Toast widget (a QLabel that fades in/out at the bottom of the window) +and the show_toast() convenience function for displaying short user-feedback messages. +""" + from PySide6.QtWidgets import QLabel, QGraphicsOpacityEffect, QPushButton from PySide6.QtCore import QTimer, QPropertyAnimation, QEasingCurve, Qt, QEvent from PySide6.QtGui import QFont - class Toast(QLabel): - """ - A non-blocking toast notification that appears at the bottom of the window, - fades in, stays visible, then fades out automatically. - """ - def __init__(self, parent=None): super().__init__(parent) diff --git a/scripts/GinanUI/app/utils/ui_compilation.py b/scripts/GinanUI/app/utils/ui_compilation.py index 090c17afd..b7c072997 100644 --- a/scripts/GinanUI/app/utils/ui_compilation.py +++ b/scripts/GinanUI/app/utils/ui_compilation.py @@ -1,22 +1,15 @@ +""" +Compiles the Qt .ui file into a Python module for use by Ginan-UI. + +Runs pyside6-uic on main_window.ui to produce main_window_ui.py, then patches +the generated resource import lines to match the project's package structure. +Intended to be run as a script or called during development setup. +""" + import subprocess, shutil from pathlib import Path - def compile_ui(): - """ - Compile the Qt `.ui` file into a Python module and fix its resource import. - - Converts `main_window.ui` into `main_window_ui.py` using `pyside6-uic`, - then updates the logo import line for correct resource loading. - - Raises: - ImportError: If `pyside6-uic` is not found. - - Example: - >>> compile_ui() - UI compiled successfully. - """ - # File paths ui_file = Path(__file__).parent.parent / "views" / "main_window.ui" output_file = Path(__file__).parent.parent / "views" / "main_window_ui.py" @@ -45,5 +38,6 @@ def compile_ui(): with open(output_file, 'w') as f: f.writelines(lines) +# Run this to compile the the user interface without running Ginan-UI if __name__ == "__main__": compile_ui() \ No newline at end of file diff --git a/scripts/GinanUI/app/utils/workers.py b/scripts/GinanUI/app/utils/workers.py index e6bef87c8..910e3b8d2 100644 --- a/scripts/GinanUI/app/utils/workers.py +++ b/scripts/GinanUI/app/utils/workers.py @@ -1,12 +1,21 @@ -# app/utils/workers.py +""" +QObject worker classes for running background tasks in Ginan-UI. + +Provides thread-safe workers for: + - PeaExecutionWorker: runs a PEA processing execution + - DownloadWorker: downloads PPP / BRDC products or retrieves valid analysis centres + - BiasProductWorker: fetches and parses BIA code priorities for a given provider + - SinexValidationWorker: downloads and validates an IGS CRD SINEX file against RINEX metadata + +All workers communicate results back to the UI via Qt signals. +""" + import traceback from datetime import datetime from pathlib import Path from typing import Optional, List - import pandas as pd from PySide6.QtCore import QObject, Signal, Slot - from scripts.GinanUI.app.models.dl_products import ( get_product_dataframe_with_repro3_fallback, download_products, @@ -15,10 +24,9 @@ get_provider_constellations, get_bia_code_priorities_for_selection, download_and_validate_sinex, - log_sinex_validation_results + log_sinex_validation_results, ) from scripts.GinanUI.app.utils.common_dirs import INPUT_PRODUCTS_PATH - from scripts.GinanUI.app.utils.logger import Logger class PeaExecutionWorker(QObject): @@ -37,14 +45,14 @@ def __init__(self, execution): @Slot() def stop(self): try: - Logger.terminal("🛑 Stop requested - terminating PEA...") + Logger.workflow("🛑 Stop requested - terminating PEA...") # recommended to implement stop_all() in Execution to terminate child processes if hasattr(self.execution, "stop_all"): self.execution.stop_all() - Logger.terminal("🛑 Stopped") + Logger.workflow("🛑 Stopped") except Exception: tb = traceback.format_exc() - Logger.terminal(f"⚠️ Exception during stop:\n{tb}") + Logger.workflow(f"⚠️ Exception during stop:\n{tb}") @Slot() def run(self): @@ -53,8 +61,7 @@ def run(self): self.finished.emit("✅ Execution finished successfully.") except Exception: tb = traceback.format_exc() - Logger.terminal(f"⚠️ Error launching Execution! Exception:\n{tb}") - + Logger.workflow(f"⚠️ Error launching Execution! Exception:\n{tb}") class DownloadWorker(QObject): """ @@ -92,14 +99,14 @@ def run(self): # 1. Get valid products if self.analysis_centers: if not self.start_epoch and not self.end_epoch: - Logger.terminal(f"📦 No start and/or end date, can't check valid analysis centers") + Logger.workflow(f"📦 No start and/or end date, can't check valid analysis centers") self.cancelled.emit() return - Logger.terminal(f"📦 Retrieving valid products") + Logger.workflow(f"📦 Retrieving valid products") try: # Check if stop was requested before starting if self._stop: - Logger.terminal(f"📦 Analysis centres retrieval cancelled") + Logger.workflow(f"📦 Analysis centres retrieval cancelled") self.cancelled.emit() return # Use the repro3 fallback function which automatically checks repro3 @@ -107,13 +114,13 @@ def run(self): valid_products = get_product_dataframe_with_repro3_fallback(self.start_epoch, self.end_epoch) # Check again before emitting result - don't emit finished if cancelled if self._stop: - Logger.terminal(f"📦 Analysis centres retrieval cancelled") + Logger.workflow(f"📦 Analysis centres retrieval cancelled") self.cancelled.emit() return # Fetch constellation information for each provider if not valid_products.empty: - Logger.terminal(f"📡 Fetching constellation information from SP3 headers...") + Logger.workflow(f"📡 Fetching constellation information from SP3 headers...") def check_stop(): return self._stop @@ -125,7 +132,7 @@ def check_stop(): ) if self._stop: - Logger.terminal(f"📦 Analysis centres retrieval cancelled") + Logger.workflow(f"📦 Analysis centres retrieval cancelled") self.cancelled.emit() return @@ -136,12 +143,12 @@ def check_stop(): self.finished.emit(valid_products) except Exception as e: if self._stop: - Logger.terminal(f"📦 Analysis centres retrieval cancelled") + Logger.workflow(f"📦 Analysis centres retrieval cancelled") self.cancelled.emit() return tb = traceback.format_exc() - Logger.terminal(f"⚠️ Error whilst retrieving valid products:\n{tb}") - Logger.terminal(f"⚠️ {e}") + Logger.workflow(f"⚠️ Error whilst retrieving valid products:\n{tb}") + Logger.workflow(f"⚠️ {e}") self.cancelled.emit() return @@ -151,8 +158,8 @@ def check_stop(): download_metadata(self.download_dir, self.progress.emit, self.atx_downloaded.emit) except Exception as e: tb = traceback.format_exc() - Logger.terminal(f"⚠️ Error whilst downloading metadata:\n{tb}") - Logger.terminal(f"⚠️ {e}") + Logger.workflow(f"⚠️ Error whilst downloading metadata:\n{tb}") + Logger.workflow(f"⚠️ {e}") self.cancelled.emit() return @@ -170,13 +177,13 @@ def check_stop(): progress_callback=self.progress.emit, stop_requested=check_stop): pass except RuntimeError as e: - Logger.terminal(f"⚠️ {e}") + Logger.workflow(f"⚠️ {e}") self.cancelled.emit() return except Exception as e: tb = traceback.format_exc() - Logger.terminal(f"⚠️ Error whilst downloading products:\n{tb}") - Logger.terminal(f"⚠️ {e}") + Logger.workflow(f"⚠️ Error whilst downloading products:\n{tb}") + Logger.workflow(f"⚠️ {e}") self.cancelled.emit() return @@ -294,7 +301,7 @@ def run(self): try: # Check if stop was requested before starting if self._stop: - Logger.terminal(f"📦 SINEX validation cancelled") + Logger.workflow(f"📦 SINEX validation cancelled") self.error.emit("SINEX validation cancelled") return @@ -316,12 +323,16 @@ def check_stop(): # Check again after download if self._stop: - Logger.terminal(f"📦 SINEX validation cancelled") + Logger.workflow(f"📦 SINEX validation cancelled") self.error.emit("SINEX validation cancelled") return if sinex_path is None: - self.error.emit("Failed to download SINEX file") + # The SINEX file could not be downloaded - it most likely does not + # exist for this date (common for recent ultra-rapid / rapid products). + # Skip validation gracefully rather than reporting a hard failure. + Logger.workflow("ℹ️ SINEX file unavailable for this date - skipping SINEX validation") + self.error.emit("SINEX validation skipped: file unavailable") return # Log the validation results @@ -335,5 +346,69 @@ def check_stop(): self.error.emit("SINEX validation cancelled") return tb = traceback.format_exc() - Logger.terminal(f"⚠️ Error during SINEX validation:\n{tb}") - self.error.emit(f"Error during SINEX validation: {e}") \ No newline at end of file + Logger.workflow(f"⚠️ Error during SINEX validation:\n{tb}") + self.error.emit(f"Error during SINEX validation: {e}") + +class LoadingWorker(QObject): + """ + Ensures ocean and atmospheric tide loading BLQ files exist for a given station. + + Downloads the loading grid netCDF files if needed, checks whether the station + already has entries in the existing BLQ files, and runs interpolate_loading + to generate station-specific BLQ files when required. + + :param execution: The Execution instance (used for ensure_loading_blq) + :param marker_name: 4-character station marker name (e.g. 'ALIC') + :param marker_number: DOMES marker number (e.g. '50137M0014') or None + :param apriori_position: [X, Y, Z] ECEF coordinates in metres + """ + finished = Signal() # Emitted when loading BLQ processing completes + error = Signal(str) # Emits error message string + progress = Signal(str, int) # Emits (description, percent) for progress updates + + def __init__(self, execution, marker_name: str, marker_number: Optional[str], + apriori_position: List[float]): + super().__init__() + self.execution = execution + self.marker_name = marker_name + self.marker_number = marker_number + self.apriori_position = apriori_position + self._stop = False + + @Slot() + def stop(self): + self._stop = True + + @Slot() + def run(self): + try: + if self._stop: + Logger.workflow("📦 Loading BLQ generation cancelled") + self.error.emit("Loading BLQ generation cancelled") + return + + def check_stop(): + return self._stop + + self.execution.ensure_loading_blq( + marker_name=self.marker_name, + marker_number=self.marker_number, + apriori_position=self.apriori_position, + progress_callback=self.progress.emit, + stop_requested=check_stop, + ) + + if self._stop: + Logger.workflow("📦 Loading BLQ generation cancelled") + self.error.emit("Loading BLQ generation cancelled") + return + + self.finished.emit() + + except Exception as e: + if self._stop: + self.error.emit("Loading BLQ generation cancelled") + return + tb = traceback.format_exc() + Logger.workflow(f"⚠️ Error during loading BLQ generation:\n{tb}") + self.error.emit(f"Error during loading BLQ generation: {e}") \ No newline at end of file diff --git a/scripts/GinanUI/app/utils/yaml.py b/scripts/GinanUI/app/utils/yaml.py index 52b94bf19..cdb893c8a 100644 --- a/scripts/GinanUI/app/utils/yaml.py +++ b/scripts/GinanUI/app/utils/yaml.py @@ -1,18 +1,9 @@ -from ruamel.yaml import YAML -from ruamel.yaml.comments import CommentedSeq, CommentedMap -from ruamel.yaml.scalarstring import PlainScalarString -from pathlib import Path -import tempfile -import os - -from scripts.GinanUI.app.utils.logger import Logger - """ YAML utilities for the Ginan-UI application. -This module provides safe wrappers around ruamel.yaml to ensure that Python -objects (e.g., pathlib.Path, lists, strings) are always serialised and -deserialised in a consistent way. +Provides safe wrappers around ruamel.yaml for loading, writing, and updating YAML +config files while preserving comments and formatting. Normalises Python objects +(Path, list, str) to types that ruamel.yaml can serialise reliably. Key functions: - load_yaml(file_path): Load YAML into memory, converting path-like strings @@ -24,35 +15,25 @@ list → CommentedSeq, str → PlainScalarString). - _normalise_inplace(): Internal helper to recursively normalise an entire config tree in-place. Used as a safety net in write_yaml(). - -Conventions: -- Leading underscore (_) marks helpers intended for internal use only. -- Public functions (no underscore) are part of the module’s stable API and - should be used by other parts of the application. """ +import os +import tempfile +from pathlib import Path +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedSeq, CommentedMap +from ruamel.yaml.scalarstring import PlainScalarString +from scripts.GinanUI.app.utils.logger import Logger + # Configure YAML parser +# These values work, don't need to change yaml = YAML() yaml.preserve_quotes = True yaml.indent(mapping=4, sequence=4, offset=4) yaml.width = 4096 # Avoid line wrapping yaml.default_flow_style = False # Use block-style lists - -def _convert_paths(obj): - """Recursively convert plain strings that look like filesystem paths into Path objects.""" - if isinstance(obj, dict): - return {k: _convert_paths(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [_convert_paths(v) for v in obj] - elif isinstance(obj, (PlainScalarString, str)): - s = str(obj) - # heuristic: treat as path if it looks like one - if "/" in s or s.startswith(".") or s.startswith("~") or os.path.isabs(s): - return Path(s).expanduser() - return s - else: - return obj +#region YAML Manipulation def load_yaml(file_path: Path) -> CommentedMap: """ @@ -110,6 +91,9 @@ def update_yaml_values(file_path: Path, updates: list[tuple[str, str]]): with file_path.open("w", encoding='utf-8') as f: yaml.dump(data, f) +#endregion + +#region Helper Functions def normalise_yaml_value(val): """ @@ -128,6 +112,21 @@ def normalise_yaml_value(val): return seq return val +def _convert_paths(obj): + """Recursively convert plain strings that look like filesystem paths into Path objects.""" + if isinstance(obj, dict): + return {k: _convert_paths(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [_convert_paths(v) for v in obj] + elif isinstance(obj, (PlainScalarString, str)): + s = str(obj) + # heuristic: treat as path if it looks like one + if "/" in s or s.startswith(".") or s.startswith("~") or os.path.isabs(s): + return Path(s).expanduser() + return s + else: + return obj + def _normalise_inplace(obj): """ Recursively normalise values in-place using normalise_yaml_value(). @@ -142,3 +141,5 @@ def _normalise_inplace(obj): obj[i] = normalise_yaml_value(v) _normalise_inplace(obj[i]) return obj + +#endregion \ No newline at end of file diff --git a/scripts/GinanUI/app/views/main_window.ui b/scripts/GinanUI/app/views/main_window.ui index bf176d930..0f13a58b6 100644 --- a/scripts/GinanUI/app/views/main_window.ui +++ b/scripts/GinanUI/app/views/main_window.ui @@ -7,7 +7,7 @@ 0 0 1200 - 800 + 850 @@ -88,7 +88,7 @@ text-align: right; - Ginan-UI v4.1.1 + Ginan-UI v4.1.2 Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter @@ -295,6 +295,12 @@ QPushButton:disabled { Qt::TextElideMode::ElideNone + + true + + + false + false @@ -794,40 +800,6 @@ QComboBox:disabled { - - - - - 0 - 0 - - - - PointingHandCursor - - - QPushButton { - background-color: #2c5d7c; - color: white; - padding: 2px 8px; - font: 13pt "Segoe UI"; - text-align: center; -} -QPushButton:hover { - background-color: #214861; -} -QPushButton:pressed { - background-color: #1a3649; -} -QPushButton:disabled { - background-color: rgb(120, 120, 120); -} - - - Reset Config - - - @@ -862,40 +834,6 @@ QPushButton:disabled { - - - - - 0 - 0 - - - - PointingHandCursor - - - QPushButton { - background-color: #2c5d7c; - color: white; - padding: 2px 8px; - font: 13pt "Segoe UI"; - text-align: center; -} -QPushButton:hover { - background-color: #214861; -} -QPushButton:pressed { - background-color: #1a3649; -} -QPushButton:disabled { - background-color: rgb(120, 120, 120); -} - - - Show Config - - - @@ -1387,8 +1325,61 @@ QPushButton:disabled { 30 - - + + + + true + + + + 0 + 0 + + + + PointingHandCursor + + + QCheckBox::indicator { + width: 36px; + height: 36px; +} + +QCheckBox::indicator:unchecked { + image: url(:/icon/checkbox_unselected.png); +} + +QCheckBox::indicator:unchecked:hover { + + image: url(:/icon/checkbox_unselected_pressed.png); +} + +QCheckBox::indicator:checked { + image: url(:/icon/checkbox_selected.png); +} + +QCheckBox::indicator:checked:hover { + image: url(:/icon/checkbox_selected_hover.png); +} + +QCheckBox::indicator:checked:disabled { + image: url(:/icon/checkbox_selected_disabled.png); +} + +QCheckBox::indicator:unchecked:disabled { + image: url(:/icon/checkbox_unselected_disabled.png); +} + + + + + + true + + + + + 0 @@ -1401,7 +1392,7 @@ QPushButton:disabled { - .TRACE + .POS @@ -1455,8 +1446,58 @@ QCheckBox::indicator:unchecked:disabled { - - + + + + true + + + + 0 + 0 + + + + PointingHandCursor + + + QCheckBox::indicator { + width: 36px; + height: 36px; +} + +QCheckBox::indicator:unchecked { + image: url(:/icon/checkbox_unselected.png); +} + +QCheckBox::indicator:unchecked:hover { + + image: url(:/icon/checkbox_unselected_pressed.png); +} + +QCheckBox::indicator:checked { + image: url(:/icon/checkbox_selected.png); +} + +QCheckBox::indicator:checked:hover { + image: url(:/icon/checkbox_selected_hover.png); +} + +QCheckBox::indicator:checked:disabled { + image: url(:/icon/checkbox_selected_disabled.png); +} + +QCheckBox::indicator:unchecked:disabled { + image: url(:/icon/checkbox_unselected_disabled.png); +} + + + + + + + + 0 @@ -1469,7 +1510,25 @@ QCheckBox::indicator:unchecked:disabled { - .POS + .SNX + + + + + + + + 0 + 0 + + + + + 12 + + + + .TRACE @@ -1491,8 +1550,8 @@ QCheckBox::indicator:unchecked:disabled { - - + + true @@ -1541,8 +1600,119 @@ QCheckBox::indicator:unchecked:disabled { + + + + + + true + + + + 0 + 0 + + + + + 12 + + + + Output File Generation + + + Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft + + + + + + + + background-color: rgb(24, 24, 24); color: rgb(255, 255, 255); + + + YAML + + + + QLayout::SizeConstraint::SetDefaultConstraint + + + 5 + + + 5 + + + 5 + + + 5 + + + 8 + + + 10 + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Expanding + + + + 20 + 120 + + + + + + + + 5 + + + 5 + + + 5 + + + 5 + + + 24 + + + 30 + + + + + + 0 + 0 + + + + + 12 + + + + Overwrite Config with UI Values + + + - + true @@ -1596,8 +1766,117 @@ QCheckBox::indicator:unchecked:disabled { + + + + true + + + + 0 + 0 + + + + + 13 + true + + + + color: #bfbfbf; font-size: 13pt; + + + Specify how the YAML config file that PEA uses during execution will be generated + + + Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft + + + true + + + + + + + 6 + + + 0 + + + + + + 0 + 0 + + + + PointingHandCursor + + + QPushButton { + background-color: #2c5d7c; + color: white; + padding: 2px 8px; + font: 13pt "Segoe UI"; + text-align: center; +} +QPushButton:hover { + background-color: #214861; +} +QPushButton:pressed { + background-color: #1a3649; +} +QPushButton:disabled { + background-color: rgb(120, 120, 120); +} + + + Reset Config + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + QPushButton { + background-color: #2c5d7c; + color: white; + padding: 2px 8px; + font: 13pt "Segoe UI"; + text-align: center; +} +QPushButton:hover { + background-color: #214861; +} +QPushButton:pressed { + background-color: #1a3649; +} +QPushButton:disabled { + background-color: rgb(120, 120, 120); +} + + + Show Config + + + + + - + true @@ -1613,13 +1892,47 @@ QCheckBox::indicator:unchecked:disabled { - Output File Generation + YAML Config File Generation Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft + + + + + 0 + 0 + + + + PointingHandCursor + + + QPushButton { + background-color: #2c5d7c; + color: white; + padding: 2px 8px; + font: 13pt "Segoe UI"; + text-align: center; +} +QPushButton:hover { + background-color: #214861; +} +QPushButton:pressed { + background-color: #1a3649; +} +QPushButton:disabled { + background-color: rgb(120, 120, 120); +} + + + Open Advanced Config Inspector + + + @@ -1928,7 +2241,7 @@ QPushButton:disabled { 0 - + 0 @@ -2064,38 +2377,110 @@ li.checked::marker { content: "\2612"; } - - - - 0 - 0 - + + + 0 - - PointingHandCursor + + 0 - - QPushButton { - background-color: #2c5d7c; - color: white; - padding: 2px 8px; - font: 11pt "Segoe UI"; - text-align: center; + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Expanding + + + + 40 + 20 + + + + + + + + PointingHandCursor + + + false + + + QPushButton { + width: 40px; + height: 40px; + border: none; + background: transparent; + image: url(:/icon/enlarge.png); } + QPushButton:hover { - background-color: #214861; + image: url(:/icon/enlarge_hover.png); } + QPushButton:pressed { - background-color: #1a3649; + image: url(:/icon/enlarge_selected.png); } -QPushButton:disabled { - background-color: rgb(120, 120, 120); -} - - - Open in Browser - - + + + + + + + + 32 + 32 + + + + false + + + + + + + PointingHandCursor + + + false + + + QPushButton { + width: 40px; + height: 40px; + border: none; + background: transparent; + image: url(:/icon/open_in_browser.png); +} + +QPushButton:hover { + image: url(:/icon/open_in_browser_hover.png); +} + +QPushButton:pressed { + image: url(:/icon/open_in_browser_selected.png); +} + + + + + + + + 32 + 32 + + + + false + + + + @@ -2153,7 +2538,7 @@ QComboBox QAbstractItemView { cddisCredentialsButton observationsButton outputButton - terminalTextEdit + workflowTextEdit consoleTextEdit modeCombo constellationsCombo diff --git a/scripts/GinanUI/docs/APPLICATION_ARCHITECTURE.md b/scripts/GinanUI/docs/APPLICATION_ARCHITECTURE.md new file mode 100644 index 000000000..f01acf58c --- /dev/null +++ b/scripts/GinanUI/docs/APPLICATION_ARCHITECTURE.md @@ -0,0 +1,488 @@ +# Ginan-UI +## Application Architecture +### This document describes the software design choices and architecture framework of Ginan-UI +### Version: Release v4.1.2 +#### Written by: Sam Greenwood +#### Last Updated: 16th June 2026 + +## 0. Table of Contents +- [1. Overview & Purpose](#1-overview--purpose) +- [2. System Context](#2-system-context) +- [3. Tech Stack](#3-tech-stack) +- [4. Application Structure](#4-application-structure) +- [5. Key Components & Modules](#5-key-components--modules) +- [6. Data Flow](#6-data-flow) +- [7. Authentication & Authorisation](#7-authentication--authorisation) +- [8. Configuration & Environment](#8-configuration--environment) +- [9. Build, Run & Deployment](#9-build-run--deployment) +- [10. Testing](#10-testing) +- [11. Known Issues & Technical Debt](#11-known-issues--technical-debt) +- [12. Decision Log](#12-decision-log) +- [13. Glossary](#13-glossary) + +--- +## 1. Overview & Purpose + +Ginan-UI is a graphical user interface for the Ginan software developed by Geoscience Australia. It aims to lower the barrier of entry for users trying to use Ginan by simplifying the user's interaction with the software away from a command-line interface. On top of this, it automatically populates a `.yaml` configuration file based on a user-provided `.rnx` RINEX observation file, automatically downloads all static and dynamic GNSS products required for processing from NASA's CDDIS Earthdata archives, executes Ginan's Parameter Estimation Algorithm (PEA), and then visualises its output in an interactive HTML format embedded directly within the UI. + +Ginan-UI lives inside the broader Ginan repository at `ginan/scripts/GinanUI/` and is designed as a companion tool - it does not replace or modify Ginan itself, but rather wraps around it to make Ginan as easy as drag-and-drop. It was developed as part of the ANU TechLauncher program in collaboration with Geoscience Australia. + +--- +## 2. System Context + +Ginan-UI interacts with the following external systems: + +**NASA CDDIS Earthdata Archives (`cddis.nasa.gov`):** The primary external dependency. Ginan-UI downloads all GNSS products (CLK, BIA, SP3, BRDC, SNX, and various other static metadata files) directly from the CDDIS HTTP archive. Authentication is via NASA Earthdata credentials stored in the user's `.netrc` / `_netrc` file. The `requests` library is used for all HTTP communication. Connectivity and credential validity are tested at startup. + +**The Ginan PEA binary (`ginan/bin/pea`):** The core processing engine that Ginan-UI wraps. Ginan-UI locates the PEA binary (either bundled in a PyInstaller release or found on the system PATH), instantiates it as a subprocess, and then streams its `stdout` / `stderr` into the Console log panel in real time. + +**`scripts/plot_pos.py` and `scripts/plot_trace_res.py`:** External plotting scripts located outside of Ginan-UI and within the broader Ginan repository. These are called as Python functions after PEA finishes processing and generates interactive HTML visualisations from the resultant `.POS` and `.TRACE` output files. + +**The Local Filesystem:** Ginan-UI reads user-supplied RINEX observation files, reads and writes the generated `ppp_generated.yaml` config, manages product and output directories, and archives files from previous processing runs. + +Ginan-UI does not expose any ports, APIs, or services. It is a standalone desktop application with no inbound network communication. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ User's Machine │ +│ │ +│ ┌──────────────┐ subprocess ┌──────────────────────┐ │ +│ │ Ginan-UI │ ─────────────────── │ Ginan PEA Binary │ │ +│ │ (PySide6) │ │ (ginan/bin/pea) │ │ +│ └──────┬───────┘ └──────────────────────┘ │ +│ │ calls │ +│ ▼ │ +│ ┌──────────────────────────────────┐ │ +│ │ plot_pos.py / plot_trace_res.py │ │ +│ │ (from ginan/scripts/) │ │ +│ └──────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ + │ HTTPS (requests + .netrc auth) + ▼ +┌─────────────────────────┐ +│ NASA CDDIS Earthdata │ +│ cddis.nasa.gov │ +│ (SP3, CLK, BIA, etc.) │ +└─────────────────────────┘ +``` + +--- +## 3. Tech Stack + +**Python 3.9+:** The implementation language. The minimum version of 3.9 was chosen for compatibility with the broader Ginan repository's Python tooling. + +**PySide6 ~6.10.0:** Qt6 Python bindings, used for the entire GUI (widgets, layouts, signals / slots, threading, web engine). PySide6 was chosen over PyQt6 for its more permissive LGPL licensing, which is compatible with Ginan being open-source. Qt's signal / slot mechanism is the primary pattern for cross-thread communication (e.g., logging from worker threads back to the UI). The `.ui` file (`main_window.ui`) is designed in Qt Designer and compiled to Python via `pyside6-uic`. + +**ruamel.yaml ~0.18.15:** Used for all YAML reading and writing. Chosen over PyYAML because it preserves comments and formatting in the config file, which is important so that manual user edits to `ppp_generated.yaml` are not silently destroyed when Ginan-UI writes back specific fields. Breaking changes in ruamel.yaml's API are common across minor versions, and the pinned version in `requirements.txt` should be respected. + +**pandas ~2.3.3:** Used for managing the product availability dataframe returned by the CDDIS product query logic in `dl_products.py`. Provides a convenient structure for filtering available PPP providers, series, and projects by date range. + +**plotly ~6.3.1:** Used by the external `plot_pos.py` and `plot_trace_res.py` scripts to generate the interactive HTML visualisations. + +**numpy ~2.3.3:** Used internally by the plotting scripts and data processing pipeline. + +**statsmodels ~0.14.5:** Used by the plotting scripts for statistical analysis of position solutions. + +**requests ~2.32.5:** Used for all HTTP communication with the CDDIS archive. + +**hatanaka ~2.8.1:** Used to decompress Hatanaka-compressed RINEX files (`.crx` / `.rnx.gz`). + +**unlzw3 ~0.2.3:** Used to decompress Unix `.Z` compressed product files (a legacy format still common in the CDDIS archive). + +**beautifulsoup4 ~4.14.2:** Used for parsing CDDIS HTML directory listings when scanning for available product files. + +> **Note on requirements.txt accuracy:** `requirements.txt` lists the direct Python dependencies used by Ginan-UI. `netrc` is part of the Python standard library, and runtime dependencies such as `numpy` are listed explicitly even when another package also requires them. If `pip install -r requirements.txt` fails, check whether an upstream dependency has changed its supported Python or platform constraints. + +--- +## 4. Application Structure + +All Ginan-UI code lives within `ginan/scripts/GinanUI/`. The structure follows a loose Model-View-Controller (MVC) architecture pattern with three clearly separated layers. + +``` +GinanUI/ +├── main.py # Entry point - instantiates QApplication and MainWindow +├── README.md +├── requirements.txt +├── app/ +│ ├── main_window.py # Top-level controller / application shell +│ ├── controllers/ # UI controllers (C in MVC) +│ │ ├── input_controller.py # Parent controller - owns shared state & top-level buttons +│ │ ├── general_config_controller.py +│ │ ├── constellation_config_controller.py +│ │ ├── output_config_controller.py +│ │ ├── yaml_config_controller.py +│ │ └── visualisation_controller.py +│ ├── models/ # Core logic & data (M in MVC) +│ │ ├── execution.py # PEA subprocess lifecycle & YAML config management +│ │ ├── dl_products.py # CDDIS product discovery & downloading +│ │ ├── rinex_extractor.py # RINEX file parsing +│ │ ├── archive_manager.py # Product and output file archival +│ │ └── inspector.py # GinanYAMLInspector integration & YAML merging +│ ├── views/ # UI definitions (V in MVC) +│ │ ├── main_window.ui # Qt Designer file - edit this, not main_window_ui.py +│ │ └── main_window_ui.py # Auto-generated - DO NOT EDIT MANUALLY +│ ├── resources/ +│ │ ├── assets/ # Icons, logos, Qt resource files (.qrc, _rc.py) +│ │ ├── Yaml/ +│ │ │ ├── default_config.yaml # Template YAML that is used to generate ppp_generated.yaml +│ │ │ └── GinanYAMLInspector.html # Inspector HTML (auto-generated by "pea -Y 4") +│ │ ├── ppp_generated.yaml # Generated at runtime from default_config.yaml +│ │ └── inputData/products/ # Downloaded GNSS products live here at runtime +│ └── utils/ +│ ├── logger.py # Centralised thread-safe logging via Qt signals +│ ├── toast.py # Non-blocking toast notification widget +│ ├── common_dirs.py # Centralised path constants (dev & PyInstaller aware) +│ ├── yaml.py # ruamel.yaml wrappers (load, write, update in-place) +│ ├── workers.py # QObject workers for background threads +│ ├── cddis_credentials.py # .netrc credential save / validate utilities +│ ├── cddis_connection.py # CDDIS connectivity & authentication testing +│ └── ui_compilation.py # Compiles main_window.ui -> main_window_ui.py +├── docs/ +│ ├── USER_MANUAL.md +│ ├── APPLICATION_ARCHITECTURE.md +│ └── images/ +└── tests/ + ├── test_checksum.py + ├── test_executable.py + ├── test_execution.py + └── test_ui_compilation.py +``` + +**Key conventions:** +- Many complex code files use ``#region`` blocks to segment and organise sections of code. These drastically improve readability and make it easier to find the relevant code when debugging. +- Controllers are instantiated by `main_window.py` and do not instantiate each other except through the parent `InputController`, which owns and instantiates its four config tab sub-controllers (`GeneralConfigController`, `ConstellationConfigController`, `OutputConfigController`, `YAMLConfigController`). +- All background work (i.e. product downloads, PEA execution, and loading BLQ generation) is done in `QThread` workers defined in `workers.py`. Qt signals are used exclusively for communicating results and progress back to the UI thread - no direct cross-thread widget access. +- All filepath constants are defined in `common_dirs.py` and resolve correctly in both development mode and PyInstaller bundle mode. **DO NOT** hardcode paths elsewhere. +- Logging throughout the app is done exclusively via the static `Logger` class (`utils/logger.py`). **DO NOT** use `print()` in production code, only temporary testing. +- The `main_window.ui` file is the single source of truth for the UI layout. `main_window_ui.py` is auto-generated every time ``main.py`` is run (it calls ``utils/ui_compilation.py`` automatically) and should never be edited directly - regenerate it using `ui_compilation.py` after any `.ui` changes. + +--- +## 5. Key Components & Modules + +### `main_window.py` - Application Shell +The top-level class that initialises and owns all controllers. It creates the `Execution` model instance, instantiates `InputController` and `VisualisationController`, and wires together the high-level workflow signals (e.g., `pea_ready` -> start download + PEA). It also contains the `log_message()` method that the `Logger` signals connect to, routing messages to either the "Workflow" or "Console" `QTextEdit` widgets. `MainWindow` is also responsible for spinning up and tearing down `QThread` / worker pairs and connecting their signals. + +### `input_controller.py` - Parent UI Controller +Owns all shared UI state. This includes the selected RINEX file path (`rnx_file`), the output directory (`output_dir`), the products dataframe (`products_df`), and the `Execution` instance. It wires the top-level action buttons ("Observations", "Output", "Process", "Stop", "CDDIS Credentials", "Open User Manual") and delegates configuration tab-specific behaviour to four sub-controllers (`GeneralConfigController`, `ConstellationConfigController`, `OutputConfigController`, `YAMLConfigController`). The `ExtractedInputs` dataclass, defined inside this module, is the data transfer object that packages all UI values before they are passed to `Execution.apply_ui_config()` for config generation. + +### `execution.py` - PEA Lifecycle & Config Manager +The primary model class. On instantiation it locates the PEA binary (checking the PyInstaller bundle, then system PATH, then relative path from the source tree), and loads the `ppp_generated.yaml` config using ruamel.yaml. Its key responsibilities include: `edit_config()` applies individual YAML field changes in-place without destroying comments; `apply_ui_config()` orchestrates writing all UI-derived values into the YAML before a run; `ensure_loading_blq()` verifies and generates ocean / atmospheric tide loading BLQ files for the station; `execute_config()` spawns PEA as a subprocess and streams its output; `build_pos_plots()` and `build_trace_plots()` call the external plotting scripts post-run. The `stop_all()` method uses `os.killpg` (process group kill) to reliably terminate PEA and any child processes on Unix. + +**Ocean and Atmospheric Loading:** The `ensure_loading_blq()` method checks whether the station has entries in the configured BLQ files. If not, it downloads loading grid netCDF files (`oceantide.nc` and `atmtide.nc`) and runs the `interpolate_loading` binary to compute station-specific loading coefficients. The generated BLQ files are automatically added to the YAML configuration. This workflow is triggered before PEA execution and operates even when the "Overwrite Config with UI Values" toggle is disabled. + +### `dl_products.py` - Product Discovery & Downloading +Handles all interaction with the CDDIS archive for downloading GNSS products. Scans the CDDIS HTTP directory listing (using `beautifulsoup4`) to find available SP3, CLK, BIA, and BRDC files for a given date range and PPP provider selection. Includes a REPRO3 fallback for older RINEX data where standard products are no longer available. Also responsible for downloading static metadata products (ATX, ALOAD, OLOAD, etc.) on first launch and every seven (7) days after that. The `download_loading_grids()` function handles downloading the ocean and atmospheric tide loading grid files (`oceantide.nc` and `atmtide.nc`) required for BLQ generation. + +### `rinex_extractor.py` - RINEX Parser +Parses RINEX v2 and v3 / v4 observation file headers to extract: marker name, receiver type, antenna type, antenna offset (ENU), approximate position (referred to as "apriori position"), time window (first / last epoch), data interval, and per-constellation observation codes. The extracted data is used to pre-populate the UI fields and to construct the `ExtractedInputs` object. A non-obvious behaviour: for RINEX v3 / v4 files, extracted observation codes are culled to carrier phase (L) codes only, then reordered against the template config's default priority list. For v2 files, codes are converted and already in priority order so this reordering step is skipped. + +### `archive_manager.py` - File Archival +Manages moving old product and output files into timestamped archive subdirectories. Archival is triggered in three scenarios: (1) on app startup for static products older than seven (7) days; (2) when the RINEX file changes, to prevent products from mismatched time windows being mixed; (3) when the PPP provider / series / project selection changes. Also provides `restore_from_archive()` which checks the archive directory before downloading a product from CDDIS, avoiding redundant network requests. + +### `workers.py` - Background Thread Workers +Defines five `QObject`-based workers intended to run in `QThread`: `PeaExecutionWorker`, `DownloadWorker`, `BiasProductWorker`, `SinexValidationWorker`, and `LoadingBlqWorker`. All workers expose a `stop()` slot and check a `_stop` flag at regular intervals to ensure they do not cause a segmentation fault on cancellation. Results are communicated back via Qt signals (`finished`, `error`, `progress`). **A non-obvious behaviour:** `DownloadWorker` handles three distinct modes depending on which constructor arguments are provided - analysis centre discovery, metadata installation, or product downloading - and branches its `run()` logic accordingly. The `LoadingBlqWorker` delegates to `Execution.ensure_loading_blq()` and runs in a background thread to prevent UI blocking during BLQ generation. + +### `logger.py` - Centralised Logging +A static class that must be initialised with the `MainWindow` instance before use. Exposes `Logger.workflow()`, `Logger.console()`, and `Logger.both()`. Uses Qt signals internally so that log calls from background worker threads are safely passed to the UI thread before updating the `QTextEdit` widgets. Falls back to `print()` if called before initialisation. + +### `utils/yaml.py` - YAML Utilities +Wraps ruamel.yaml with safe read / write helpers that preserve comments and formatting. **The key non-obvious behaviour** is that all values written to the YAML are passed through `normalise_yaml_value()` first, which converts Python `Path` objects and plain strings to `PlainScalarString` and lists to block-style `CommentedSeq`. This is necessary because ruamel.yaml will raise `RepresenterError` on bare Python types in certain contexts. + +### `utils/common_dirs.py` - Path Constants +Defines all important path constants (`TEMPLATE_PATH`, `GENERATED_YAML`, `INPUT_PRODUCTS_PATH`, `USER_MANUAL_PATH`) in a way that resolves correctly both in development mode and when running from a release build that is bundled with PyInstaller (where `sys._MEIPASS` is set). Any new path constant should be added here rather than hardcoded in individual modules. + +### `views/main_window.ui` - Qt Designer UI Definition +The XML file that defines all UI widget geometry, layout, and properties. It is highly recommended that you edit this file in Qt Designer by running the ``pyside6-designer`` file from within your Python virtual environment (however you can still edit it by hand), then regenerate `main_window_ui.py` by running ``utils/ui_compilation.py``. The generated file patches two import lines for Qt resource files - this patching is done automatically by `ui_compilation.py`. + +### `controllers/constellation_config_controller.py` +Manages the Constellations tab, which displays drag-and-drop lists of observation code priorities for each enabled GNSS constellation. Triggers a `BiasProductWorker` background download whenever the PPP provider / series / project selection changes, to fetch and parse the corresponding `.BIA` file and update the available code priorities. Cross-validates RINEX constellations against the SP3 file and highlights any unsupported constellations in red with strikethrough styling. + +### `controllers/yaml_config_controller.py` +Manages the YAML Config tab, which provides controls for configuration file management and advanced editing. Wires the "Overwrite Config with UI Values" toggle, "Show Config" button, "Reset Config" button, and "Edit Config in Inspector" button. The inspector integration uses `QWebEngineView` to embed the GinanYAMLInspector HTML within a Qt dialog which automatically imports the current configuration and intercepts the save action to merge changes back via `QWebChannel`. Delegates all inspector I/O and YAML merging logic to the `Inspector` model. + +### `models/inspector.py` +The model layer for GinanYAMLInspector integration. Handles: (1) ensuring the inspector HTML exists by auto-generating it via `pea -Y 4` when missing; (2) building the JavaScript that wires auto-import and save interception when the inspector is opened from Ginan-UI; (3) sanitising the inspector's YAML output (quoting wildcard patterns, stripping trailing whitespace); (4) deep-merging the inspector export onto the existing `ppp_generated.yaml` so keys the inspector doesn't know about are preserved; (5) repairing known ruamel.yaml output edge cases. The `merge_and_save()` method includes fallback validation - if the comment-preserving write produces unparseable YAML, it automatically falls back to a clean write without comment preservation. + +### `controllers/visualisation_controller.py` +Manages the visualisation panel and plot display. Embeds generated HTML plots into a `QWebEngineView` widget, maintains an indexed list of available visualisations, and provides controls for plot selection and external viewing. The `bind_enlarge_button()` method wires the "Enlarge" button to open the current visualisation in a separate resizable pop-out window (implemented as a `QDialog` containing a `QWebEngineView`). The `bind_open_button()` method wires the "Open in Browser" button to launch the system's default browser. + +--- +## 6. Data Flow + +The primary processing workflow follows this sequence: + +``` +User selects RINEX file + │ + ▼ +RinexExtractor.extract_rinex_data() + → Extracts metadata (marker name, time window, constellations, codes, antenna info, etc.) + → UI fields pre-populated via GeneralConfigController / ConstellationConfigController + │ + ▼ +DownloadWorker (QThread) - analysis centre discovery + → Scans CDDIS for valid PPP providers for the RINEX time window + → Populates PPP Provider / Series / Project dropdowns in UI + │ + ▼ +User sets Mode, and reviews / adjusts fields before clicking "Process" + │ + ▼ +InputController.on_run_pea() + → Calls archive_old_outputs() to move previous run outputs + → Calls archive_products_if_selection_changed() if PPP selection changed + → Calls InputController.extract_ui_values() → produces ExtractedInputs dataclass + → Calls Execution.apply_ui_config(ExtractedInputs) → writes all values into ppp_generated.yaml + │ + ▼ +DownloadWorker (QThread) - product downloading + → Downloads missing dynamic products (SP3, CLK, BIA, BRDC) from CDDIS + → Checks archive first via restore_from_archive() before downloading + → Progress streamed to "Workflow" log via Qt signals + │ + ▼ +LoadingBlqWorker (QThread) - ocean and atmospheric loading generation + → Checks configured BLQ files for station entry + → Downloads loading grid files (oceantide.nc, atmtide.nc) if needed + → Runs interpolate_loading binary to generate station-specific BLQ files + → Updates YAML config with new BLQ file paths + → Progress streamed to "Workflow" log via Qt signals + │ + ▼ +PeaExecutionWorker (QThread) + → Spawns PEA binary as subprocess with ppp_generated.yaml + → Streams stdout / stderr to "Console" log via Qt signals + │ + ▼ +Execution.build_pos_plots() / build_trace_plots() + → Calls plot_pos_files() / plot_trace_res_files() from ginan/scripts/ + → Generates HTML files in output_dir/visual/ + │ + ▼ +VisualisationController + → Registers HTML files, embeds first plot in QWebEngineView + → Enables plot selector ComboBox, "Enlarge" button, and "Open in Browser" button +``` + +--- +## 7. Authentication & Authorisation + +Ginan-UI has a single authentication concern: NASA Earthdata credentials for accessing the CDDIS archive. There is no user login system, sessions, or role-based access control within the application itself. + +**Credential Storage:** NASA Earthdata credentials (username and password) are saved to the user's `.netrc` file (Unix / MacOS: `~/.netrc`; Windows: `%USERPROFILE%\.netrc` and `%USERPROFILE%\_netrc`). Both `urs.earthdata.nasa.gov` and `cddis.nasa.gov` entries are written simultaneously. The credential file is managed by `cddis_credentials.py`. + +**Credential Entry:** On first launch, if valid credentials are not detected in `.netrc`, a separate dialog window `CredentialsDialog` is displayed prompting the user for their username and password. This dialog can also be opened at any time via the "CDDIS Credentials" button in the top-right. Credentials are saved via `save_earthdata_credentials()`. + +**Credential Usage:** The `requests` library uses the `.netrc` file automatically for HTTP Basic authentication when downloading from CDDIS. The `cddis_connection.py` module provides `test_cddis_connection()` for verifying both connectivity and authentication validity, and `get_netrc_auth()` for explicitly reading credentials when needed. + +**Email / Username Persistence:** A secondary `CDDIS.env` file (`app/utils/CDDIS.env`) stores the email / username separately. This is read by `read_email()` and used in some CDDIS API contexts. If not present in the env file, it is derived from the `.netrc` username. + +--- +## 8. Configuration & Environment + +Configuration is handled through two mechanisms: a template YAML for PEA configuration, and path resolution logic for development versus bundled distribution. + +**YAML Configuration:** +- `app/resources/Yaml/default_config.yaml` - The template config. This is the committed, default PEA configuration that ships with Ginan-UI. It contains default values for all PEA processing parameters. +- `app/resources/ppp_generated.yaml` - The generated config, created at runtime by copying the template and then overwriting specific fields with user-supplied values. This file is ignored by git and should not be committed. It persists between sessions and preserves manual edits made via the "Show Config" button. + +**Path Resolution:** +The `common_dirs.py` module detects whether Ginan-UI is running in development mode or a PyInstaller bundle by checking `sys.frozen`. In bundle mode, `sys._MEIPASS` points to the `_internal/` directory and paths are constructed accordingly. All path constants (`TEMPLATE_PATH`, `GENERATED_YAML`, `INPUT_PRODUCTS_PATH`, `USER_MANUAL_PATH`) are derived from `get_base_path()` and `get_user_manual_path()` in this module. + +**Product Storage:** +Downloaded products are stored in `app/resources/inputData/products/` (or `_internal/scripts/GinanUI/app/resources/inputData/products/` in the bundled distribution). Archived products live in subdirectories under `products/archive/`. + +--- +## 9. Build, Run & Deployment + +### Running from Source + +Ginan-UI runs as a Python module from within the Ginan repository. The following assumes you have Python 3.9+ and have cloned the Ginan repository. + +```bash +# Navigate to the Ginan repository root +cd /path/to/ginan + +# Install dependencies +pip install -r scripts/GinanUI/requirements.txt + +# Run Ginan-UI +python -m scripts.GinanUI.main +``` + +You will also need a built `pea` binary available. The binary is expected at `ginan/bin/pea`. If it is not present, `execution.py` will also check the system PATH for a `pea` executable. + +### Running Qt Designer + +Ensure that you have a python virtual environment (sometimes referred to as ``venv``) activated, and run the ``pyside6-designer`` file like so: + +```bash +pyside6-designer +``` + +If the command is not on your `PATH`, run the executable from your virtual environment's `bin/` directory on Linux/macOS or `Scripts\` directory on Windows. This will open a GUI editing software window that makes modifying the front-end of Ginan-UI much easier than editing the XML file ``main_window.ui`` directly. Do this by selecting to open the `app/views/main_window.ui` file. + +### Adding Images / Icons to the UI - Qt Resource Files + +Qt has its own asset pipeline for bundling images and icons into the application. Raw image files cannot simply be referenced by filesystem path in a `.ui` file - they must be compiled into a Qt resource module first. The workflow is: + +1. **Create a `.qrc` file** - this is an XML file that lists the image assets to include. For example, `ginan_logo.qrc` references `ginan-logo.png`. These files live in `app/resources/assets/`. + +2. **Compile the `.qrc` file into a Python module** using `pyside6-rcc`: +```bash + pyside6-rcc app/resources/assets/ginan_logo.qrc -o app/resources/assets/ginan_logo_rc.py +``` + This produces `ginan_logo_rc.py` - a Python module containing the image data as base64-encoded bytes. The `_rc.py` suffix is the convention. + +3. **Import the `_rc.py` module** somewhere in the application before the `.ui` file is loaded. The import registers the resources with Qt's internal resource system. In Ginan-UI this is handled by the auto-generated `main_window_ui.py`, which imports `ginan_logo_rc` and `icons_rc`. When `ui_compilation.py` regenerates this file, it patches the raw `import ginan_logo_rc` lines to the correct package path (`from scripts.GinanUI.app.resources.assets import ginan_logo_rc`). **However this must be updated for all new images added to the UI**. + +4. **Reference the asset in the `.ui` file** using the Qt resource path syntax: `:/prefix/filename` (e.g., `:/icons/help.png`). Qt Designer uses this same syntax when you add images through its built-in resource browser. + +The short version: if you add a new image to the UI, add it to the relevant `.qrc` file, recompile with `pyside6-rcc`, and ensure the resulting `_rc.py` is imported before the UI loads. Do not reference image files by raw filesystem path in the `.ui` file. + +### Recompiling the UI After Qt Designer Changes + +If you modify `app/views/main_window.ui` in Qt Designer, you must regenerate the Python file using ``utils/ui_compilation.py``, or command-line: + +```bash +python -m scripts.GinanUI.app.utils.ui_compilation +``` + +This will recompile `main_window_ui.py` and automatically patch the resource import lines. + +### Building a Distributable Executable + +Ginan-UI is distributed as a standalone executable via PyInstaller. + +Releases are published to the [Ginan GitHub Releases page](https://github.com/GeoscienceAustralia/ginan/releases) for Windows, MacOS, and Linux. See the User Manual (Section 2.2) for platform-specific installation instructions for end users. + +--- +## 10. Testing + +Tests live in `GinanUI/tests/` and are run with pytest. + +```bash +cd /path/to/ginan +pytest scripts/GinanUI/tests/ +``` + +Four test modules exist: + +- `test_ui_compilation.py` - Verifies that `pyside6-uic` is available and that `main_window.ui` compiles without errors. +- `test_executable.py` - Checks that the PEA binary can be located from the development directory structure. +- `test_execution.py` - Tests the `Execution` model class, including config loading, YAML editing, and the `apply_ui_config()` workflow. +- `test_checksum.py` - Validates the SHA512 checksum verification logic used during product downloading. + +Test resources (a sample RINEX file, example YAML config, product lists) live within ``ginan/exampleConfig/`` and ``ginan/inputData/``. The shell scripts `getFiles.sh` and `setFiles.sh` are used to fetch or set up the test input data. + +**Known Gaps:** There is no automated UI testing (no widget interaction tests). The product downloading logic in `dl_products.py` is not unit tested due to its dependency on live CDDIS connectivity. The `RinexExtractor` parsing logic has no dedicated test coverage. + +--- +## 11. Known Issues & Technical Debt + +**Race Condition on First Launch (Qt Segmentation Fault):** On rare occasions, Ginan-UI will launch to a black screen and then crash with a segmentation fault. This is a known Qt initialisation race condition and resolves itself on a second launch attempt. The User Manual documents this for end users. + +**Stop Button Race Condition:** If the user clicks "Stop" before the first download thread has started, and then clicks "Process" again immediately, a core dump can occur because the thread has not yet had a chance to exit. The workaround is to wait for the "stopped thread" message in the "Console" terminal before clicking "Process" again. This is documented in the troubleshooting table of the User Manual. + +**GinanYAMLInspector Wildcard Pattern Handling:** The inspector copies values directly from HTML form fields without adding YAML quotes. Wildcard patterns (e.g., `*.CLK`, `*_ocean.BLQ`) are treated as YAML aliases by ruamel.yaml unless quoted. The `Inspector.fix_inspector_yaml()` method detects and quotes these patterns before parsing, but edge cases with unusual patterns may still cause issues. If you encounter "found undefined alias" errors when saving from the inspector, check for unquoted wildcards in the generated YAML. + +**YAML Artifacts Persisting Between Sessions:** When the RINEX file is changed, Ginan-UI updates specific YAML fields but does not regenerate the entire config from scratch. This means some stale values (e.g., old marker names listed under `receiver_options` ) can persist across sessions. This is usually harmless but can occasionally cause unexpected PEA behaviour. The "Reset Config" button is the remedy as it deletes and fully regenerates the config file from ``default_config.yaml``. + +**CDDIS Server Reliability:** The CDDIS servers have experienced significant reliability issues (notably during the 2025 US government shutdown). The application handles connection timeouts with retry logic, but extended outages will result in failed downloads. This is an external dependency that cannot be resolved within the codebase. + +--- +## 12. Decision Log + +### UI Framework - PySide6 + +Several Python GUI frameworks were evaluated: Tkinter / CustomTkinter, PyQt6, PySide6, and Kivy. + +Tkinter and CustomTkinter were ruled out for lacking the complex UI elements the project required - particularly interactive graph plotting and an embedded web view for HTML visualisations. While third-party themes can modernise Tkinter's appearance, that introduces additional dependencies without solving the underlying capability gap. + +PyQt6 provides the same feature set as PySide6 and is based on the same underlying Qt C++ library, but is distributed under a commercial licence that would require a paid licence for non-personal use. PySide6 is functionally near-identical to PyQt6 but is officially maintained by The Qt Company under an LGPL licence, making it compatible with Ginan being open-source. + +**PySide6 was chosen** for the following reasons: it provides the full Qt feature set (modern widgets, Qt Designer drag-and-drop layout, signals / slots, multi-threading, `QWebEngineView` for embedded HTML, Matplotlib / Plotly integration); it has a modern look that will hold up over time; and its LGPL licence is appropriate for an open-source project. The steeper learning curve compared to Tkinter was considered an acceptable trade-off given that the features requiring that curve - graph plotting, complex layouts, thread-safe UI updates - were hard requirements of the product. + +> **Note for future maintainers:** PyQt6 and PySide6 APIs are nearly identical. If there is ever a reason to switch, the `qtpy` abstraction library (`pip install qtpy`) provides compatibility that allows both to be used interchangeably from a single codebase. + +--- + +### Architecture Pattern - MVC + +A Model-View-Controller pattern was chosen to structure the codebase. This provided a clear separation between the UI layer (`views/`, `controllers/`), the central logic and data layer (`models/`), and supporting utilities (`utils/`). MVC was a natural fit for a desktop application with a clear front-end (Qt widgets) and back-end (PEA execution, file downloading, YAML management), and the original TechLauncher team had prior familiarity with the pattern. + +--- + +### YAML Library - ruamel.yaml over PyYAML + +PyYAML is the more commonly known YAML library, but it does not preserve comments or formatting when writing back to a file. Because Ginan-UI writes specific fields into `ppp_generated.yaml` while leaving the rest untouched - and because advanced users are expected to make manual edits to this file - silently destroying comments and formatting on every write would be a poor experience. `ruamel.yaml` was chosen specifically for its ability to perform targeted in-place updates while preserving all existing comments, indentation, and structure. + +--- +## 13. Glossary + +**ATX** - Antenna Exchange Format. A file format describing antenna phase centre corrections for GNSS receivers and satellites. + +**BDS** - BeiDou Navigation Satellite System. China's GNSS constellation. + +**BIA** - Bias product file. Contains code and phase biases for multi-GNSS processing. Used to populate the Constellations tab code priorities. + +**BLQ** - Ocean and atmospheric tide loading coefficient file. Contains corrections that account for the deformation of the Earth's crust due to ocean and atmospheric tides. Ginan-UI automatically generates station-specific BLQ files using the `interpolate_loading` tool when they are not present in the configured files. + +**BRDC / NAV** - Broadcast navigation message. The navigation data broadcast by GNSS satellites, used as a fallback or supplement to precise ephemeris products. + +**CDDIS** - Crustal Dynamics Data Information System. NASA's geodetic data archive, from which Ginan-UI downloads all GNSS products. Hosted at `cddis.nasa.gov`. + +**CLK** - Clock product file. Contains precise satellite and station clock corrections. + +**ECEF** - Earth-Centred, Earth-Fixed coordinate system. The Cartesian coordinate system used for apriori position values (X, Y, Z in metres). + +**ENU** - East-North-Up coordinate system. Used for antenna offset values. + +**GAL** - Galileo. The European GNSS constellation. + +**GLO** - GLONASS. Russia's GNSS constellation. + +**GNSS** - Global Navigation Satellite System. The generic term for satellite navigation systems (GPS, GLONASS, Galileo, BeiDou, etc.). + +**GPS** - Global Positioning System. The United States' GNSS constellation. + +**GPT2** - Global Pressure and Temperature 2. A tropospheric model used in GNSS processing. + +**IGS** - International GNSS Service. A global collaborative network of GNSS tracking stations and analysis centres that produces the reference products (SP3, CLK, BIA, etc.) used by Ginan. + +**interpolate_loading** - A Ginan binary tool that computes station-specific ocean and atmospheric tide loading coefficients from grid files. Ginan-UI automatically runs this tool to generate BLQ files when a station is not found in the configured loading files. + +**ION** - Ionospheric product file. Contains ionospheric delay corrections. + +**MVC** - Model-View-Controller. The broad architectural pattern used to organise Ginan-UI's codebase: Models (`models/`), Views (`views/`), Controllers (`controllers/`). + +**.netrc / _netrc** - A plain text file storing credentials for network services. Used by Ginan-UI and the `requests` library for authenticating with the CDDIS archive. Located at `~/.netrc` on Unix / MacOS, or `%USERPROFILE%\.netrc` / `%USERPROFILE%\_netrc` on Windows. + +**PEA** - Parameter Estimation Algorithm. Ginan's core GNSS processing engine, compiled as a native binary (`ginan/bin/pea`). Ginan-UI wraps and automates the use of PEA. + +**PPP** - Precise Point Positioning. A GNSS processing technique that uses precise satellite orbit and clock products (as opposed to differential positioning) to compute highly accurate positions from a single receiver. + +**PPP Provider / AC** - Analysis Centre. An organisation that produces and publishes precise GNSS products (e.g., IGS, COD, GFZ, JPL). Referred to interchangeably as "PPP Provider" in the UI and "analysis centre" in the code. + +**QWebChannel** - Qt's JavaScript integration mechanism for bidirectional communication between C++/Python code and web content loaded in `QWebEngineView`. Used by the GinanYAMLInspector integration to intercept the save action and route the generated YAML back to Python for merging. + +**QZS / QZSS** - Quasi-Zenith Satellite System. Japan's regional GNSS constellation. + +**REPRO3** - The third IGS reprocessing campaign. A set of reprocessed historical GNSS products available on CDDIS for older observation data where standard products may no longer be published. REPRO3 is prioritised between the GPS weeks 730 to 2138. Outside of this, the regular products are prioritised. + +**RINEX** - Receiver INdependent EXchange format. The standard file format for raw GNSS observation data. Ginan-UI supports RINEX v2, v3, and v4. + +**RNX** - Common file extension for RINEX observation files. + +**SNX / SINEX** - Solution INdependent EXchange format. Used for station coordinate and metadata exchange. Ginan-UI downloads and validates IGS CRD SINEX files to cross-check receiver metadata extracted from the RINEX file. + +**SP3** - Standard Product 3, also known as the "Orbit" file. A file format containing precise satellite orbit positions (ephemeris). One of the primary dynamic products downloaded for each processing run. + +**TRO** - Tropospheric product file. Contains tropospheric delay corrections. + +**URS** - Earthdata User Registration System. NASA's authentication system for CDDIS access. Credentials registered at `urs.earthdata.nasa.gov`. + +**YAML** - YAML Ain't Markup Language (Yes that is the acronym). The configuration file format used by Ginan's PEA. Ginan-UI reads and writes `ppp_generated.yaml` using `ruamel.yaml` to preserve comments and formatting. + +**GinanYAMLInspector** - An HTML-based configuration editor generated by Ginan's PEA executable (`pea -Y 4`). Provides a comprehensive form interface for editing nearly all Ginan configuration options. When opened from Ginan-UI, the inspector auto-imports the current configuration and intelligently merges saved changes back to `ppp_generated.yaml` whilst preserving keys the inspector doesn't manage. diff --git a/scripts/GinanUI/docs/USER_MANUAL.md b/scripts/GinanUI/docs/USER_MANUAL.md index 3b048e817..68dbff0e8 100644 --- a/scripts/GinanUI/docs/USER_MANUAL.md +++ b/scripts/GinanUI/docs/USER_MANUAL.md @@ -1,8 +1,9 @@ # Ginan-UI ## User Manual ### This guide is written to aid those using the Ginan-UI extension software. -### Version: Release v4.1.1 -### Last Updated: 13th February 2026 +### Version: Release v4.1.2 +#### Written by: Sam Greenwood +#### Last Updated: 16th June 2026 ## 1. Introduction @@ -50,15 +51,13 @@ Ginan-UI is safe to run - the complete source code is available in this reposito 3. Remove the file from macOS quarantine: -``` -bash +```bash xattr -dr com.apple.quarantine /path/to/ginan-ui ``` 4. Run the startup script, which configures environment variables and launches Ginan-UI: -``` -bash +```bash ./run.sh ``` @@ -68,34 +67,30 @@ bash 2. Extract the archive: -``` -bash +```bash tar -xf ginan-ui-linux-x64.tar.gz cd ginan-ui ``` 3. Make the executable runnable (if needed): -``` -bash +```bash chmod +x ginan-ui ``` 4. Run Ginan-UI: -``` -bash +```bash ./ginan-ui ``` **Note:** On some Linux distributions, you may need to install additional Qt dependencies. If you encounter missing library errors, refer to Section 7.1 (Troubleshooting). #### Installation from Source -Follow the below commands, tested with python 3.9+ +Follow the commands below, tested with Python 3.9+: -``` -Install and navigate to the root of the Ginan repository: -cd /ginan +```bash +cd /path/to/ginan pip install -r scripts/GinanUI/requirements.txt python -m scripts.GinanUI.main ``` @@ -108,7 +103,6 @@ When you open Ginan-UI for the first time, you will be taken to the main dashboa ![Dashboard of Ginan-UI](./images/ginan_ui_dashboard.jpg) - To use Ginan-UI, you will require an account with NASA's CDDIS EarthData archives. Once you have created an account here, you can log in by clicking the "CDDIS Credentials" button in the top-right of Ginan-UI:

CDDIS Credentials button in the top-right

@@ -167,7 +161,7 @@ The Ginan-UI interface is divided into two main panels: the left panel for input ### 4.1 Input Configuration (Left Panel) -The left-hand panel contains all the configuration options required to set up Ginan to commence processing. These options are organised into three tabs: **General**, **Constellations**, and **Output**. +The left-hand panel contains all the configuration options required to set up Ginan to commence processing. These options are organised into four tabs: **General**, **Constellations**, **Output**, and **YAML**. #### 4.1.1 General Tab @@ -218,6 +212,12 @@ The General tab contains the primary configuration options for setting up your G - View / edit ENU (East-North-Up) offset values. This allows manual adjustment if your antenna has a known offset position from its reference point. +##### Apriori Position + +- View / edit the approximate ECEF (X, Y, Z) position of the receiver in metres, automatically extracted from the RINEX file header. + +- This is used by PEA as the initial position estimate for the PPP filter. If no apriori position is present in the RINEX file, this field will remain empty and PEA will use its own initialisation strategy. + ##### PPP Provider / Project / Series - Three drop-downs that filter available products based on the provided time window @@ -232,26 +232,6 @@ The General tab contains the primary configuration options for setting up your G - These fields are populated after a valid observation file has been loaded. -##### "Reset Config" Button - -- Resets both the UI and the configuration file back to their default states. - -- The `ppp_generated.yaml` configuration file will be regenerated from the default template. - -- All UI fields will be cleared and returned to their initial placeholder values. - -- This is useful if you have made configuration changes you want to undo, or if you want to start fresh with a new RINEX file without lingering settings from a previous session. - -- A confirmation dialog will appear before the reset proceeds. - -##### "Show Config" Button - -- A button that opens the generated `ppp_generated.yaml` file in your system's default text editor. - -- Allows advanced users to manually edit PEA configuration parameters - -- See Section 6.1 for more details on manual config editing. - #### 4.1.2 Constellations Tab The Constellations tab allows you to manage the observation code priorities for each enabled GNSS constellation on the "General" tab. Code priorities determine which signal types PEA will prefer when processing your data. @@ -276,6 +256,8 @@ Ginan-UI performs automatic validation to ensure compatibility between the provi - **Code priority detection:** The supported code priorities are automatically detected from the PPP provider's `.BIA` file, ensuring that only valid codes are configured. +- **SINEX station validation:** Ginan-UI downloads the IGS CRD SINEX file for your observation date and validates the receiver metadata extracted from your RINEX file (marker name, receiver type, antenna type, antenna offset, and apriori position) against the IGS station database. Any discrepancies are reported in the Workflow log. This helps catch misconfigured or non-standard station metadata before processing begins. + #### 4.1.3 Output Tab The Output tab allows you to specify which output files PEA should generate during processing. @@ -288,10 +270,66 @@ The Output tab allows you to specify which output files PEA should generate duri - **TRACE** (Trace file): Produces detailed debugging output from PEA processing. Disabled by default. +- **SNX** (SINEX file): Generates a Solution Independent Exchange Format file containing station coordinates, velocities, and other geodetic parameters. Disabled by default. + ##### Visualisation Dependency The plot visualisation feature in the right panel depends on the output files being generated. If you disable the POS output, the corresponding position plots will not be available in the Visualisation section after processing completes. +#### 4.1.4 YAML Config Tab + +The YAML Config tab provides controls for managing the YAML configuration file and accessing advanced editing tools. + +##### "Overwrite Config with UI Values" Toggle + +- Enabled by default. Controls whether Ginan-UI should automatically update the YAML configuration file with values from the user interface. + +- **When enabled:** Clicking the "Show Config" or "Process" buttons will write all UI-configured values to fields marked with `#AUTO` comments in the YAML file. + +- **When disabled:** Your manual edits to the YAML file are preserved. Ginan-UI will not overwrite any fields which allows for complete manual control over the configuration. This is useful when you want to make advanced changes to config parameters that are not exposed in the UI. + +- **Note:** Ocean and atmospheric loading BLQ file generation will still occur even when this toggle is disabled, as these are required for proper tide loading corrections. + +##### "Show Config" Button + +- Opens the generated `ppp_generated.yaml` file in your system's default text editor. + +- If "Overwrite Config with UI Values" is enabled, the file will be updated with current UI values before opening. + +- If disabled, the file will be opened as-is without any modifications, preserving your manual edits. + +- Allows advanced users to manually edit PEA configuration parameters that are not exposed in the UI. + +- See Section 6.1 for more details on manual config editing. + +##### "Reset Config" Button + +- Resets both the UI and the configuration file back to their default states. + +- The `ppp_generated.yaml` configuration file will be deleted and regenerated from the default template. + +- All UI fields will be cleared and returned to their initial placeholder values. + +- This is useful if you have made configuration changes you want to undo, or if you want to start fresh with a new RINEX file without lingering settings from a previous session. + +- A confirmation dialog will appear before the reset proceeds. + +##### "Edit Config in Inspector" Button + +- Opens the GinanYAMLInspector tool in an embedded browser window. + +- The inspector provides a graphical interface for editing the YAML configuration with more advanced options than the main Ginan-UI interface exposes. + +- **Auto-import:** The current `ppp_generated.yaml` configuration is automatically loaded into the inspector when opened. + +- **Save integration:** When you click "Save file" in the inspector, the changes are automatically merged back into `ppp_generated.yaml`. Keys that the inspector doesn't know about are preserved which ensures manual edits to unsupported fields will remain intact. + +- **Inspector generation:** If the GinanYAMLInspector HTML file does not exist, Ginan-UI will automatically attempt to generate it using the PEA executable (`pea -Y 4`). + +- This feature is particularly useful for bulk configuration changes or when working with configuration options that require the full flexibility of the inspector's interface. + +- See Section 6.2 for more details on using the GinanYAMLInspector. + ### 4.2 Monitoring & Output (Right Panel) The right-hand panel contains all the monitoring tools for Ginan-UI's functionality and Ginan's processing, as well as managing your CDDIS credentials. @@ -320,7 +358,13 @@ The right-hand panel contains all the monitoring tools for Ginan-UI's functional - The visualisation panel displays an interactive HTML plot that is generated using the `plot_pos.py` script after PEA completes its processing. It allows the user to view, pan, zoom, hover over tooltips, and toggle legends. -- Below the visualisation panel, the user can choose to open the plot in their system's default web-browser, or switch between the other generated plots. +- Below the visualisation panel are controls for managing how you view the plots: + + - **Visualisation selector dropdown:** Switch between different generated plots (e.g., position plots, smoothed position plots). This dropdown is automatically populated based on the HTML files generated during processing. + + - **"Enlarge" button:** Opens the current visualisation in a separate resizable pop-out window. This provides a larger viewing area while keeping Ginan-UI accessible in the background. The pop-out window uses an embedded browser (QWebEngineView) and can be freely resized, minimized, or maximized. + + - **"Open in Browser" button:** Opens the currently enabled visualisation plot in your system's default web browser for full-screen viewing or external analysis. - **Note:** Plot visualisation is only available when the corresponding output file type is enabled in the Output tab. For example, position plots require the POS output to be enabled. @@ -344,9 +388,32 @@ The right-hand panel contains all the monitoring tools for Ginan-UI's functional ### 5.1 What Happens When You Click "Process" -Once all required parameters within the UI are filled and the "Process" button is clicked, Ginan-UI will begin downloading the required dynamic products from the CDDIS EarthData servers. These primarily include the `.bia`, `.clk`, `.nav (BRDC)` and `.sp3` files. +Once all required parameters within the UI are filled and the "Process" button is clicked, Ginan-UI will begin downloading the required dynamic products from the CDDIS EarthData servers. These primarily include the `.bia`, `.clk`, `.nav (BRDC)` and `.sp3` files. Each downloaded file is verified against its SHA512 checksum published on the CDDIS server to ensure the download is complete and uncorrupted. If a previously downloaded product is found in the archive from a prior processing run, it will be restored from the archive rather than re-downloaded. + +Ginan-UI will also download and validate the IGS CRD SINEX file for your observation date, cross-checking the receiver metadata from your RINEX file against the IGS station database. Any discrepancies are reported in the Workflow log. + +#### Ocean and Atmospheric Loading Support + +Before PEA processing begins, Ginan-UI automatically verifies that ocean and atmospheric tide loading corrections are available for your station: + +- **BLQ file verification:** Ginan-UI checks the configured `.BLQ` files (referenced in `inputs.tides.ocean_tide_loading_blq_files` and `inputs.tides.atmos_tide_loading_blq_files`) to determine if your station already has loading coefficients. -Once these have successfully downloaded, Ginan's PEA tool will be automatically executed with the generated `.yaml` configuration file. This processing can be observed within the "Console" log tab which should look similar to PEA's command-line interface output. +- **Automatic generation:** If your station is not found in the existing BLQ files, Ginan-UI will automatically generate station-specific loading corrections using the `interpolate_loading` tool: + + 1. Downloads loading grid files (`oceantide.nc` and `atmtide.nc`) if not already present + 2. Runs `interpolate_loading` to compute ocean tide loading coefficients from your station's ECEF coordinates and writes them to a `{STATION}_ocean.BLQ` file + 3. Runs `interpolate_loading` again to compute atmospheric tide loading coefficients and writes them to a `{STATION}_atmos.BLQ` file + 4. Updates the YAML configuration to include these new BLQ files alongside the global reference files + +- **Progress reporting:** The loading generation process is reported in the "Workflow" log with progress indicators showing download and computation status. + +- **Configuration preservation:** When "Overwrite Config with UI Values" is disabled in the YAML Config tab, the loading BLQ generation still occurs and updates the configuration file. This ensures proper tide loading corrections are applied even when you are manually managing the YAML configuration. + +This verification confirms that tide loading corrections are properly configured without requiring the user to manually interpolate or manage BLQ files. + +#### PEA Execution + +Once all products and loading corrections have been prepared, Ginan's PEA tool will be automatically executed with the generated `.yaml` configuration file. This processing can be observed within the "Console" log tab which should look similar to PEA's command-line interface output. Once it finishes processing, the `plot_pos.py` script will be called automatically to plot the resulting `.pos` and `_smoothed.pos` files generated during processing, and the plots will appear within the UI under the "Visualisation" heading. @@ -356,7 +423,7 @@ Ginan-UI automatically downloads all required products for GNSS processing from #### Static Products (Metadata) -Static products are reference files that rarely change and are downloaded once when Ginan-UI is launchd for the first time. These include: +Static products are reference files that rarely change and are downloaded once when Ginan-UI is launched for the first time. These include: - **ATX** (Antenna exchange format) - Antenna phase centre corrections @@ -376,7 +443,7 @@ Static products are reference files that rarely change and are downloaded once w - **GPT2** (Global Pressure and Temperature 2) - Tropospheric models -These fies are stored in `scripts/GinanUI/app/resources/inputData/products/` and are automatically archived when they become outdated (typically after one week). Fresh copies are then downloaded on the next program launch. +These files are stored in `scripts/GinanUI/app/resources/inputData/products/` and are automatically archived when they become outdated (typically after one week). Fresh copies are then downloaded on the next program launch. #### Dynamic Products (Observation-Specific) @@ -412,9 +479,13 @@ When you click "Process", Ginan-UI will: 2. Query for the available products for the provided time window from the CDDIS servers -3. Download any missing dynamic products with progress indicators shown in the "Workflow" log tab +3. Check the local archive for any previously downloaded products that can be restored, to avoid re-downloading + +4. Download any remaining missing dynamic products with progress indicators shown in the "Workflow" log tab -4. Verify all required products are present before launching PEA +5. Verify each downloaded file against its SHA512 checksum published on the CDDIS server to confirm integrity + +6. Verify all required products are present before launching PEA If a product cannot be found (which is common for either very old or very new RINEX observation files), Ginan-UI will inform you that the selected provider does not have the products available for your time window yet. Different PPP providers publish their products with varying latencies. Ultra-rapid (ULT) are available within hours, Rapid (RAP) are available within about one day, and Final (FIN) may take one or two weeks. @@ -426,7 +497,7 @@ Ginan-UI will automatically archive both products and output files to prevent co #### Product Archival -Product files are automatically archived in the follow situations: +Product files are automatically archived in the following situations: - **On Application Startup:** Static products older than seven days are moved to timestamped archive folders within `scripts/GinanUI/app/resources/inputData/products/archived/`. Fresh versions are then downloaded to replace them. @@ -483,19 +554,29 @@ Ginan-UI has several important directories for its operation. All paths are rela ### 5.5 How the YAML Config is Generated -The `.yaml` configuration file that is generated for Ginan's PEA processing originates from the template config file located within `scripts/GinanUI/app/resources/Yaml/default_config.yaml`. This template file is copied if no config file exists within `scripts/GinanUI/app/resources/ppp_generated.yaml`, or if one does exist already, the `ppp_generated.yaml` file is instead overwritten. Keep in mind however that this may maintain some artifacts from previous config generations, which can be useful in some use cases. +The `.yaml` configuration file that is generated for Ginan's PEA processing originates from the template config file located within `scripts/GinanUI/app/resources/Yaml/default_config.yaml`. This template file is copied if no config file exists at `scripts/GinanUI/app/resources/ppp_generated.yaml`. If `ppp_generated.yaml` already exists, Ginan-UI reuses it and updates only the fields it manages so manual edits and unsupported keys are preserved where possible. -If you would like to instead generate a fresh `ppp_generated.yaml` file, simply delete `ppp_generated.yaml` and on the next processing run, a new config file will be generated from the `default_config.yaml` template file. +If you would like to generate a fresh `ppp_generated.yaml` file, use the "Reset Config" button or delete `ppp_generated.yaml`; on the next processing run, a new config file will be generated from the `default_config.yaml` template file. ## 6. Advanced Usage ### 6.1 Manual YAML Editing -For experienced users of Ginan who need fine-grained control over Ginan's processing, the `.yaml` configuration file can be manually edited via clicking the "Show Config" button.This will open `ppp_generated.yaml` in your system's default text editor. +For experienced users of Ginan who need fine-grained control over Ginan's processing, the `.yaml` configuration file can be manually edited by clicking the "Show Config" button in the YAML Config tab. This will open `ppp_generated.yaml` in your system's default text editor. + +#### Controlling Automatic Overwrites + +The "Overwrite Config with UI Values" toggle in the YAML Config tab controls how Ginan-UI interacts with your YAML file: + +- **When enabled (default):** Ginan-UI will automatically update fields marked with `#AUTO` comments in the YAML file whenever you click the "Show Config" or "Process" buttons. This ensures the config values remain synchronised with the UI selections. + +- **When disabled:** Your manual edits are fully preserved. Ginan-UI will not overwrite any fields in the YAML file which gives you complete control. This is useful when you need to configure advanced options that are not exposed in the UI. + +**Important:** Even with automatic overwrites disabled, ocean and atmospheric loading BLQ file generation will still update the configuration file to ensure proper tide loading corrections are applied. #### Persistence of Manual Changes -Manual user edits are preserved across most operations as Ginan-UI will only update specific fields when necessary: +When "Overwrite Config with UI Values" is enabled, Ginan-UI will only update specific fields marked with `#AUTO`: - RINEX metadata (time windows, constellations, receiver / antenna information) @@ -503,9 +584,11 @@ Manual user edits are preserved across most operations as Ginan-UI will only upd - Output directory paths -All other parameters like processing strategies, filter settings, quality control thresholds, satellite-specific options will all remain untouched. +- Ocean and atmospheric loading BLQ file paths + +All other parameters like processing strategies, filter settings, quality control thresholds, and satellite-specific options will remain untouched, preserving your manual customisations. -**Note:** YAML artifacts may persist between sessions. For example, marker names within `receiver_options` may remain if not explicitly overwritten, though this rarely causes issues. +**Note:** YAML artefacts may persist between sessions. For example, marker names within `receiver_options` may remain if not explicitly overwritten, though this rarely causes issues. #### Resetting to Default @@ -513,7 +596,7 @@ If you experience any configuration errors and want to start fresh, you have two **Option 1: Use the Reset Config Button** -Click the "Reset Config" button in the General tab. This will regenerate the configuration file from the default template and reset all UI fields to their initial state. A confirmation dialog will appear before the reset proceeds. +Click the "Reset Config" button in the YAML Config tab. This will delete the configuration file and regenerate it from the default template, and reset all UI fields to their initial state. A confirmation dialog will appear before the reset proceeds. **Option 2: Manual Reset** @@ -525,6 +608,61 @@ Click the "Reset Config" button in the General tab. This will regenerate the con **Warning:** Invalid YAML syntax (like incorrect indentation, mismatched quotes, and malformed lists) will cause PEA to fail. Please verify your formatting if you encounter configuration-related errors in the logs. +### 6.2 Using the GinanYAMLInspector + +The GinanYAMLInspector is a browser-based configuration tool that provides a more comprehensive interface for editing Ginan's YAML configuration than the main Ginan-UI panels expose. It can be accessed by clicking the "Edit Config in Inspector" button in the YAML Config tab. + +#### What is the GinanYAMLInspector? + +The GinanYAMLInspector is an HTML-based interactive form generated by Ginan's PEA executable. It provides structured input fields for nearly all configuration options available in Ginan, organised by category. This tool is particularly useful for: + +- Making bulk configuration changes across multiple parameters +- Accessing advanced configuration options not exposed in Ginan-UI's main interface +- Reviewing the full range of available Ginan configuration options +- Fine-tuning processing parameters for specialised use cases + +#### How to Use the Inspector + +1. **Opening the Inspector:** Click the "Edit Config in Inspector" button in the YAML Config tab. Ginan-UI will: + - Ensure the inspector HTML file exists (should be auto-generated via `pea -Y 4`, do manually if needed and place the HTML file in `scripts/GinanUI/app/resources/Yaml/`) + - Open the inspector in a new browser window + - Load your current `ppp_generated.yaml` configuration + - Open the inspector in an embedded browser window + +2. **Auto-Import:** When the inspector opens, your current configuration is automatically loaded into all the form fields. You don't need to manually import the file. + +3. **Making Changes:** Navigate through the inspector's sections and modify any parameters you wish to change. The inspector organises configuration options into logical categories (inputs, processing options, outputs, etc.). + +4. **Generating YAML:** After making your changes, click the "Generate YAML" button in the inspector. This converts your form inputs into YAML format and displays it in a text area. + +5. **Saving Changes:** Click the "Save file" button in the inspector. Instead of downloading a file, Ginan-UI intercepts this action and: + - Validates and sanitises the generated YAML (e.g., properly quotes wildcard patterns like `*.CLK`) + - Deep-merges the inspector's output onto your existing `ppp_generated.yaml` file + - Preserves any configuration keys that the inspector doesn't know about + - Updates the UI fields to reflect the saved changes + - Displays a confirmation message + +#### Key Features + +**Intelligent Merging:** Unlike manual file editing, the inspector save process preserves configuration keys that weren't included in the inspector's output. For example, if you only edited processing parameters in the inspector, your custom output settings and constellation-specific configurations remain untouched. + +**Wildcard Handling:** The inspector automatically handles special YAML characters. Wildcard patterns (e.g., `*.CLK`, `*_ocean.BLQ`) are properly quoted to avoid errors in parsing the YAML. + +**Fallback Validation:** If the merged configuration produces invalid YAML, Ginan-UI automatically falls back to a clean write without comment preservation, ensuring the save always succeeds. + +**Respects Overwrite Toggle:** The inspector works independently of the "Overwrite Config with UI Values" toggle. You can use the inspector to make changes even when automatic UI overwrites are disabled. + +#### When to Use the Inspector + +Using GinanYAMLInspector is not required, however it can be useful for more advanced users. Use it when you need to: + +- Configure many parameters not available in Ginan-UI's main interface (e.g., satellite-specific quality control settings, advanced filter parameters) +- Make coordinated changes across multiple related configuration sections +- Review the full scope of Ginan's configuration options to understand what's available +- Quickly enable / disable features by checking or unchecking inspector form fields + +For simple changes like adjusting the time window, mode, or output formats, the main Ginan-UI interface is more convenient. + ## 7. Troubleshooting ### 7.1 Common Issues @@ -543,6 +681,10 @@ Click the "Reset Config" button in the General tab. This will regenerate the con | Disk space errors during processing | Insufficient disk space for downloading products or writing PEA outputs. | Free up disk space. Products can consume several GB depending on time window and number of constellations. Check available space in both the products directory and your selected output directory. | | Constellation mismatch warning | The constellations in the provided RINEX file do not match those available in the selected PPP provider's SP3 file. | Select a different PPP provider that supports the constellations in your RINEX file, or disable the unsupported constellations in the "General" config tab's "Constellations" field. | | No valid PPP providers found for older data | The RINEX file is from a time period where standard PPP products are no longer available in the main CDDIS directory. | Ginan-UI will automatically attempt to use REPRO3 products for older data. If no providers are found, the data may be too old for available PPP products. | +| Ocean / atmospheric loading BLQ generation failed | The `interpolate_loading` tool failed to generate loading corrections, or the loading grid files could not be downloaded. | Check the Workflow log for specific error messages. Ensure you have a valid apriori position for your station. If the loading grid files are missing, check your network connection and CDDIS credentials. Processing may continue without loading corrections but with reduced accuracy. | +| GinanYAMLInspector not opening or HTML generation failed | The inspector HTML file doesn't exist and couldn't be auto-generated, or the PEA executable is not available. | Manually generate the inspector by running `pea -Y 4` from the command line and place the output `GinanYamlInspector.html` in `scripts/GinanUI/app/resources/Yaml/`. Ensure the PEA executable is accessible in your PATH or in the expected location. | +| Inspector save produces invalid YAML | The inspector generated YAML with syntax errors or the merge process failed. | Check the Workflow log for specific errors. Ginan-UI has automatic fallback handling, but if issues persist, try using "Show Config" to manually edit the YAML instead. Report persistent issues with the inspector output. | +| Manual YAML edits being overwritten | The "Overwrite Config with UI Values" toggle is enabled and UI values are being written to `#AUTO` fields. | Disable the "Overwrite Config with UI Values" toggle in the YAML Config tab before making manual edits. This prevents Ginan-UI from overwriting your changes when you click "Show Config" or "Process". | ### 7.2 Log Message Interpretation @@ -568,7 +710,7 @@ Common issues you may see in the logs: - **YAML configuration errors:** Syntax errors may cause PEA to fail on startup if you have manually edited the `.yaml` config file. -- **Disk space issues:** Ginan-UI has encountered problems when disk space is very limited. Please ensure you have at least a 2 - 3 Gb of disk space free. +- **Disk space issues:** Ginan-UI has encountered problems when disk space is very limited. Please ensure you have at least 2 - 3 GB of disk space free. #### Tips @@ -604,7 +746,7 @@ When reporting issues, please include: - Any error messages from the Workflow / Console logs - Screenshots if relevant -**Note:** Ginan-UI was developed as part of the ANU TechLauncher program in collaboration with Geoscience Australia. For general enquiries about Geoscience Australia's GNSS analysis capabilities, visit [www.ga.gov.au](www.ga.gov.au) +**Note:** Ginan-UI was developed as part of the ANU TechLauncher program in collaboration with Geoscience Australia. For general enquiries about Geoscience Australia's GNSS analysis capabilities, visit [www.ga.gov.au](https://www.ga.gov.au) ## 8. FAQ @@ -620,16 +762,32 @@ Here are some answers to the frequently asked questions: **Q:** *"Why is PEA giving me a configuration error?"* -**A:** This could be due to a product file being deleted erroneously, which would resolve on the next click of the "Process" button, or due to manual changes to the `.yaml` config file. The app **does not overwrite** the `ppp_generated.yaml` file when the `.rnx` file is changed or when the app is restarted. If you wish to reset to the default config, click the "Reset Config" button in the General tab, or delete the file in `ginan/scripts/GinanUI/app/resources/ppp_generated.yaml` and then run the app again. +**A:** This could be due to a product file being deleted erroneously, which would resolve on the next click of the "Process" button, or due to manual changes to the `.yaml` config file containing invalid YAML syntax. If you wish to reset to the default config, click the "Reset Config" button in the "YAML" tab, or delete the file in `ginan/scripts/GinanUI/app/resources/ppp_generated.yaml` and then run the app again. **Q:** *"How do I reset the configuration to default?"* -**A:** Click the "Reset Config" button in the General tab. This will regenerate the configuration file from the default template and clear all UI fields back to their initial state. Alternatively, you can manually delete the `ppp_generated.yaml` file. +**A:** Click the "Reset Config" button in the "YAML" tab. This will delete and regenerate the configuration file from the default template and clear all UI fields back to their initial state. Alternatively, you can manually delete the `ppp_generated.yaml` file. + +**Q:** *"What does the 'Overwrite Config with UI Values' toggle do?"* + +**A:** This toggle in the YAML Config tab controls whether Ginan-UI automatically updates the YAML configuration file with your UI selections. When enabled (default), clicking either the "Show Config" or "Process" buttons will write UI values to fields marked `#AUTO` in the YAML. When disabled, your manual edits are fully preserved and Ginan-UI won't overwrite any fields. Note that ocean and atmospheric loading BLQ file generation still occurs even when disabled. + +**Q:** *"How do I use the GinanYAMLInspector?"* + +**A:** Click the "Edit Config in Inspector" button in the YAML Config tab. Your current configuration will be automatically loaded. Make changes in the inspector's form fields, click "Generate yaml", then click "Save file". The changes will be intelligently merged back into your configuration while preserving keys the inspector doesn't manage. See Section 6.2 for detailed instructions. + +**Q:** *"What are ocean and atmospheric loading BLQ files?"* + +**A:** BLQ files contain tide loading corrections that account for the deformation of the Earth's crust due to ocean and atmospheric tides. Ginan-UI automatically checks if your station has these corrections and generates them if needed using the `interpolate_loading` tool. This happens automatically before PEA processing begins, ensuring accurate positioning results. **Q:** *"Why is the plot visualisation disabled or not showing?"* **A:** Plot visualisation depends on the corresponding output file being enabled in the "Output" tab. If you have disabled the POS output, the position plots will not be available. Enable the required output type and re-run processing. +**Q:** *"How do I view the visualisation plots in a larger window?"* + +**A:** Use the "Enlarge" button below the visualisation panel to open the current plot in a separate resizable pop-out window. This provides a larger viewing area while keeping Ginan-UI accessible in the background. Alternatively, use the "Open in Browser" button to view the plot in your system's default web browser. + **Q:** *"Can I process older RINEX files?"* **A:** Yes. Ginan-UI supports RINEX v2, v3, and v4 files. For older data (typically more than three years old), Ginan-UI will automatically search the REPRO3 directory for reprocessed products if standard products are not available. @@ -649,4 +807,4 @@ This project was designed during the Australian National University's TechLaunch - Fan Jin - Songxuan He -Special thanks to Simon McClusky at Geoscience Australia for their continuous support and guidance throughout the project's development. \ No newline at end of file +Special thanks to Simon McClusky at Geoscience Australia for their continuous support and guidance throughout the project's development. diff --git a/scripts/GinanUI/docs/images/cddis_credentials_button.jpg b/scripts/GinanUI/docs/images/cddis_credentials_button.jpg index 7e670f196..a5e97ae6e 100644 Binary files a/scripts/GinanUI/docs/images/cddis_credentials_button.jpg and b/scripts/GinanUI/docs/images/cddis_credentials_button.jpg differ diff --git a/scripts/GinanUI/docs/images/cddis_credentials_screen.jpg b/scripts/GinanUI/docs/images/cddis_credentials_screen.jpg index a4dcd4e01..bfb55695f 100644 Binary files a/scripts/GinanUI/docs/images/cddis_credentials_screen.jpg and b/scripts/GinanUI/docs/images/cddis_credentials_screen.jpg differ diff --git a/scripts/GinanUI/docs/images/ginan_ui_dashboard.jpg b/scripts/GinanUI/docs/images/ginan_ui_dashboard.jpg index c13c37754..642c29765 100644 Binary files a/scripts/GinanUI/docs/images/ginan_ui_dashboard.jpg and b/scripts/GinanUI/docs/images/ginan_ui_dashboard.jpg differ diff --git a/scripts/GinanUI/docs/images/mode_dropdown.jpg b/scripts/GinanUI/docs/images/mode_dropdown.jpg index ad1b7efd7..a4e967046 100644 Binary files a/scripts/GinanUI/docs/images/mode_dropdown.jpg and b/scripts/GinanUI/docs/images/mode_dropdown.jpg differ diff --git a/scripts/GinanUI/docs/images/observations_output_buttons.jpg b/scripts/GinanUI/docs/images/observations_output_buttons.jpg index 07119ac3c..34523484f 100644 Binary files a/scripts/GinanUI/docs/images/observations_output_buttons.jpg and b/scripts/GinanUI/docs/images/observations_output_buttons.jpg differ diff --git a/scripts/GinanUI/docs/images/pea_processing.jpg b/scripts/GinanUI/docs/images/pea_processing.jpg index 013bdf76c..ce5b2b85a 100644 Binary files a/scripts/GinanUI/docs/images/pea_processing.jpg and b/scripts/GinanUI/docs/images/pea_processing.jpg differ diff --git a/scripts/GinanUI/docs/images/plot_visualisation.jpg b/scripts/GinanUI/docs/images/plot_visualisation.jpg index 19a935cf7..378bbac62 100644 Binary files a/scripts/GinanUI/docs/images/plot_visualisation.jpg and b/scripts/GinanUI/docs/images/plot_visualisation.jpg differ diff --git a/scripts/GinanUI/docs/images/plot_visualisation_web.jpg b/scripts/GinanUI/docs/images/plot_visualisation_web.jpg index 62786a58c..e86323bf5 100644 Binary files a/scripts/GinanUI/docs/images/plot_visualisation_web.jpg and b/scripts/GinanUI/docs/images/plot_visualisation_web.jpg differ diff --git a/scripts/GinanUI/docs/images/process_button.jpg b/scripts/GinanUI/docs/images/process_button.jpg index 118a6e154..c1a42db8e 100644 Binary files a/scripts/GinanUI/docs/images/process_button.jpg and b/scripts/GinanUI/docs/images/process_button.jpg differ diff --git a/scripts/GinanUI/docs/images/product_downloading.jpg b/scripts/GinanUI/docs/images/product_downloading.jpg index 42adcaa93..98706bbc6 100644 Binary files a/scripts/GinanUI/docs/images/product_downloading.jpg and b/scripts/GinanUI/docs/images/product_downloading.jpg differ diff --git a/scripts/GinanUI/main.py b/scripts/GinanUI/main.py index 74543cb4a..6b2940e45 100644 --- a/scripts/GinanUI/main.py +++ b/scripts/GinanUI/main.py @@ -1,7 +1,14 @@ +""" +Entry point for Ginan-UI. + +Initialises the Qt application, sets the window icon, and launches the MainWindow. +GPU acceleration is disabled at startup to prevent segmentation faults on some +platforms when QWebEngineView is in use. +""" + import os import sys import logging - from PySide6.QtGui import QIcon from PySide6.QtWidgets import QApplication from scripts.GinanUI.app.main_window import MainWindow @@ -11,6 +18,7 @@ format='%(asctime)s - %(levelname)s - %(message)s' ) +# Run this to run Ginan-UI if __name__ == "__main__": # Disable GPU acceleration (can cause segmentation faults on launch if enabled) os.environ['QTWEBENGINE_CHROMIUM_FLAGS'] = '--disable-gpu --disable-software-rasterizer --no-sandbox' diff --git a/scripts/auto_download_PPP.py b/scripts/auto_download_PPP.py index 42c928d4c..f62ed10b1 100644 --- a/scripts/auto_download_PPP.py +++ b/scripts/auto_download_PPP.py @@ -403,9 +403,9 @@ def download_most_recent_cddis_file( long_filename=long_filename, file_type="SNX", analysis_center=analysis_center, - timespan=timedelta(days=1), + timespan=timedelta(days=7), solution_type="SNX", - sampling_rate="01D", + sampling_rate="07D", content_type="CRD", ) # Download recent file: diff --git a/scripts/ci/compile_vcpkg_eigen_matrix.sh b/scripts/ci/compile_vcpkg_eigen_matrix.sh new file mode 100644 index 000000000..0a6f7d572 --- /dev/null +++ b/scripts/ci/compile_vcpkg_eigen_matrix.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 <3.4|5>" >&2 + exit 2 +fi + +target_platform="$1" +eigen_lane="$2" + +case "$target_platform" in + linux) + triplet="x64-linux" + preset="release" + build_dir="build/linux-Release-eigen${eigen_lane}" + apt_packages="build-essential cmake ninja-build curl zip unzip tar pkg-config git gfortran jq" + unsupported_args=() + ;; + windows-cross) + triplet="x64-mingw-static" + preset="windows-cross-release" + build_dir="build/windows-cross-Release-eigen${eigen_lane}" + apt_packages="build-essential cmake ninja-build curl zip unzip tar pkg-config git jq mingw-w64 g++-mingw-w64-x86-64 gcc-mingw-w64-x86-64" + unsupported_args=(--allow-unsupported) + ;; + *) + echo "Unknown target platform: $target_platform" >&2 + exit 2 + ;; +esac + +case "$eigen_lane" in + 3.4) + baseline="5bf0c55239da398b8c6f450818c9e28d36bf9966" + eigen_dependency='{"name":"eigen3"}' + eigen_override='{"name":"eigen3","version":"3.4.1","port-version":1}' + ;; + 5) + baseline="6b07d2d37301e9e7c6fcf771536d2ff6585c5912" + eigen_dependency='{"name":"eigen3","version>=":"5.0.1"}' + eigen_override='{"name":"eigen3","version":"5.0.1"}' + ;; + *) + echo "Unknown Eigen lane: $eigen_lane" >&2 + exit 2 + ;; +esac + +apt-get update +apt-get install -y ${apt_packages} + +export VCPKG_ROOT="${BITBUCKET_CLONE_DIR}/vcpkg" +export VCPKG_BUILD_TYPE=release +export VCPKG_OVERLAY_PORTS="${BITBUCKET_CLONE_DIR}/scripts/ci/vcpkg-overlay-ports" + +lane_name="${target_platform}-eigen${eigen_lane}" +manifest_dir="${BITBUCKET_CLONE_DIR}/.ci-vcpkg-manifests/${lane_name}" +install_root="${BITBUCKET_CLONE_DIR}/vcpkg_installed/${lane_name}" +binary_cache="${BITBUCKET_CLONE_DIR}/.vcpkg-cache/${lane_name}" + +export VCPKG_BINARY_SOURCES="clear;files,${binary_cache},readwrite" + +mkdir -p "$manifest_dir" "$install_root" "$binary_cache" + +if [ ! -f "$VCPKG_ROOT/bootstrap-vcpkg.sh" ]; then + rm -rf "$VCPKG_ROOT" + git clone https://github.com/Microsoft/vcpkg.git "$VCPKG_ROOT" +fi + +if [ ! -x "$VCPKG_ROOT/vcpkg" ]; then + "$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics +fi + +jq \ + --arg baseline "$baseline" \ + --argjson eigen_dependency "$eigen_dependency" \ + --argjson eigen_override "$eigen_override" \ + ' + del(."builtin-baseline", .overrides) + | .dependencies |= map( + if . == "eigen3" then $eigen_dependency + elif (type == "object" and .name == "eigen3") then $eigen_dependency + else . + end + ) + | .["builtin-baseline"] = $baseline + | .overrides = [$eigen_override] + ' \ + "${BITBUCKET_CLONE_DIR}/vcpkg.json" > "${manifest_dir}/vcpkg.json" + +"$VCPKG_ROOT/vcpkg" install \ + --triplet "$triplet" \ + --x-manifest-root="$manifest_dir" \ + --x-install-root="$install_root" \ + --overlay-ports="$VCPKG_OVERLAY_PORTS" \ + "${unsupported_args[@]}" \ + --clean-after-build + +cd "${BITBUCKET_CLONE_DIR}/src" +rm -rf "$build_dir" +cmake --preset "$preset" \ + -DVCPKG_MANIFEST_DIR="$manifest_dir" \ + -DVCPKG_INSTALLED_DIR="$install_root" \ + -DVCPKG_OVERLAY_PORTS="$VCPKG_OVERLAY_PORTS" \ + -DEigen3_DIR="${install_root}/${triplet}/share/eigen3" \ + -B "$build_dir" +cmake --build "$build_dir" --parallel 8 diff --git a/scripts/ci/vcpkg-overlay-ports/libaec/cmake-config.patch b/scripts/ci/vcpkg-overlay-ports/libaec/cmake-config.patch new file mode 100644 index 000000000..808e2216f --- /dev/null +++ b/scripts/ci/vcpkg-overlay-ports/libaec/cmake-config.patch @@ -0,0 +1,58 @@ +diff --git a/cmake/libaec-config.cmake.in b/cmake/libaec-config.cmake.in +index 11ac99e..03b96aa 100644 +--- a/cmake/libaec-config.cmake.in ++++ b/cmake/libaec-config.cmake.in +@@ -36,7 +36,7 @@ if (libaec_USE_STATIC_LIBS OR (NOT DEFINED libaec_USE_STATIC_LIBS AND NOT "@BUIL + endif () + else () + find_library(libaec_LIBRARY NAMES aec DOC "AEC library") +- find_library(SZIP_LIBRARY NAMES sz szip DOC "SZIP compatible version of the AEC library") ++ find_library(SZIP_LIBRARY NAMES sz szip NAMES_PER_DIR DOC "SZIP compatible version of the AEC library") + endif () + + # Check version here +@@ -55,6 +55,7 @@ find_package_handle_standard_args(libaec + ) + + if (libaec_FOUND) ++ if(0) + if (libaec_USE_STATIC_LIBS) + add_library(libaec::aec STATIC IMPORTED) + else () +@@ -87,6 +88,8 @@ if (libaec_FOUND) + IMPORTED_LOCATION "${SZIP_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${SZIP_INCLUDE_DIR}" + ) ++ endif() ++ include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@-targets.cmake") + + # Set SZIP variables. + set(SZIP_FOUND TRUE) +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index b2aeb6c..1fb3b6d 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -77,9 +77,22 @@ set_target_properties(aec sz + + if(BUILD_SHARED_LIBS) + set(install_targets aec_shared sz_shared) ++ set_target_properties(aec_shared PROPERTIES EXPORT_NAME aec INTERFACE_COMPILE_DEFINITIONS LIBAEC_SHARED) ++ set_target_properties(sz_shared PROPERTIES EXPORT_NAME sz INTERFACE_COMPILE_DEFINITIONS LIBAEC_SHARED) + set_target_properties(aec_static graec sz_static PROPERTIES EXCLUDE_FROM_ALL 1) + else() + set(install_targets aec_static sz_static) ++ set_target_properties(aec_static PROPERTIES EXPORT_NAME aec PUBLIC_HEADER "${CMAKE_CURRENT_BINARY_DIR}/../include/libaec.h") ++ set_target_properties(sz_static PROPERTIES EXPORT_NAME sz PUBLIC_HEADER ../include/szlib.h) + set_target_properties(aec_shared graec sz_shared PROPERTIES EXCLUDE_FROM_ALL 1) + endif() +-install(TARGETS ${install_targets}) ++set_target_properties(aec PROPERTIES EXPORT_NAME aec_obj) ++set_target_properties(sz PROPERTIES EXPORT_NAME sz_obj) ++install(TARGETS ${install_targets} aec sz ++ EXPORT ${PROJECT_NAME}-targets ++) ++install( ++ EXPORT ${PROJECT_NAME}-targets ++ NAMESPACE ${PROJECT_NAME}:: ++ DESTINATION cmake ++) diff --git a/scripts/ci/vcpkg-overlay-ports/libaec/portfile.cmake b/scripts/ci/vcpkg-overlay-ports/libaec/portfile.cmake new file mode 100644 index 000000000..d8a411646 --- /dev/null +++ b/scripts/ci/vcpkg-overlay-ports/libaec/portfile.cmake @@ -0,0 +1,26 @@ +vcpkg_download_distfile(ARCHIVE + URLS "https://github.com/Deutsches-Klimarechenzentrum/libaec/archive/refs/tags/v1.1.3.tar.gz" + FILENAME "deutsches-klimarechenzentrum-libaec-v1.1.3.tar.gz" + SHA512 b64d10f8dd1f8d4c08dcbb5025550c790b01c9138714131456632e37cb58b60f40a94015644db727489fb0365dfc1e7ef0494f890639c8f306f2c90c09299136 +) + +vcpkg_extract_source_archive(SOURCE_PATH + ARCHIVE "${ARCHIVE}" + PATCHES + static-shared.patch + cmake-config.patch +) + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS + -DBUILD_TESTING=OFF +) +vcpkg_cmake_install() +vcpkg_copy_pdbs() +vcpkg_cmake_config_fixup(CONFIG_PATH "cmake") + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") + +file(INSTALL "${CURRENT_PORT_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt") diff --git a/scripts/ci/vcpkg-overlay-ports/libaec/static-shared.patch b/scripts/ci/vcpkg-overlay-ports/libaec/static-shared.patch new file mode 100644 index 000000000..0454a6fc1 --- /dev/null +++ b/scripts/ci/vcpkg-overlay-ports/libaec/static-shared.patch @@ -0,0 +1,30 @@ +diff --git a/cmake/libaec-config.cmake.in b/cmake/libaec-config.cmake.in +index 6f6c9e9..11ac99e 100644 +--- a/cmake/libaec-config.cmake.in ++++ b/cmake/libaec-config.cmake.in +@@ -26,7 +26,7 @@ + + find_path(libaec_INCLUDE_DIR NAMES libaec.h DOC "AEC include directory") + find_path(SZIP_INCLUDE_DIR NAMES szlib.h DOC "SZIP include directory") +-if (libaec_USE_STATIC_LIBS) ++if (libaec_USE_STATIC_LIBS OR (NOT DEFINED libaec_USE_STATIC_LIBS AND NOT "@BUILD_SHARED_LIBS@")) + if (MSVC) + find_library(libaec_LIBRARY NAMES aec-static.lib DOC "AEC library") + find_library(SZIP_LIBRARY NAMES szip-static.lib DOC "SZIP compatible version of the AEC library") +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index f9c3031..b2aeb6c 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -75,4 +75,11 @@ set_target_properties(aec sz + PROPERTIES + COMPILE_DEFINITIONS "${libaec_COMPILE_DEFINITIONS}") + +-install(TARGETS aec_static aec_shared sz_static sz_shared) ++if(BUILD_SHARED_LIBS) ++ set(install_targets aec_shared sz_shared) ++ set_target_properties(aec_static graec sz_static PROPERTIES EXCLUDE_FROM_ALL 1) ++else() ++ set(install_targets aec_static sz_static) ++ set_target_properties(aec_shared graec sz_shared PROPERTIES EXCLUDE_FROM_ALL 1) ++endif() ++install(TARGETS ${install_targets}) diff --git a/scripts/ci/vcpkg-overlay-ports/libaec/usage b/scripts/ci/vcpkg-overlay-ports/libaec/usage new file mode 100644 index 000000000..a6aeb5f6a --- /dev/null +++ b/scripts/ci/vcpkg-overlay-ports/libaec/usage @@ -0,0 +1,7 @@ +libaec provides CMake targets: + + find_package(libaec CONFIG REQUIRED) + # libaec API + target_link_libraries(main PRIVATE libaec::aec) + # szip compatible API + target_link_libraries(main PRIVATE libaec::sz) diff --git a/scripts/ci/vcpkg-overlay-ports/libaec/vcpkg.json b/scripts/ci/vcpkg-overlay-ports/libaec/vcpkg.json new file mode 100644 index 000000000..575b5265d --- /dev/null +++ b/scripts/ci/vcpkg-overlay-ports/libaec/vcpkg.json @@ -0,0 +1,18 @@ +{ + "name": "libaec", + "version": "1.1.3", + "port-version": 1, + "description": "Adaptive Entropy Coding library", + "homepage": "https://github.com/Deutsches-Klimarechenzentrum/libaec", + "license": "BSD-2-Clause", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ] +} diff --git a/scripts/installation/apple.md b/scripts/installation/apple.md index b9e035fce..54498ea3e 100644 --- a/scripts/installation/apple.md +++ b/scripts/installation/apple.md @@ -1,6 +1,6 @@ # Installation procedure on Apple -Tested on Macbook Pro (Intel) with Somona OSX and Macbook Pro (ARM64) with Sonoma OSX +Tested on MacBook Pro (Intel) with Sonoma macOS and MacBook Pro (ARM64) with Sonoma macOS. ## Install Ginan dependencies @@ -12,7 +12,7 @@ brew install boost cmake eigen netcdf-cxx netcdf mongo-c-driver mongo-cxx-driver *** Follow the instructions here to install the MongoDB application: -https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/` +https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/ ## Install gnssanalysis python module @@ -25,15 +25,26 @@ pip3 install gnssanalysis You can download Ginan source from github using git clone: ``` -#git clone https://github.com/GeoscienceAustralia/ginan.git -git clone -b develop-weekly --depth 1 --single-branch https://github.com/GeoscienceAustralia/ginan.git +git clone https://github.com/GeoscienceAustralia/ginan.git cd ginan +export VCPKG_ROOT="$PWD/vcpkg" +export VCPKG_COMMIT="4c5ae6b55f3e3e39d291679f89822f496cf190ee" + +git clone https://github.com/Microsoft/vcpkg.git "$VCPKG_ROOT" +git -C "$VCPKG_ROOT" fetch --depth 1 origin "$VCPKG_COMMIT" +git -C "$VCPKG_ROOT" checkout --detach "$VCPKG_COMMIT" +"$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics + cd src -mkdir build -cd build -cmake -DCMAKE_TOOLCHAIN_FILE=compile_mac_arm64.cmake .. -make -j4 pea + +# Apple silicon: +cmake --preset macos-arm64-release +cmake --build --preset macos-arm64-release --target pea --parallel 4 + +# Intel Mac: +# cmake --preset macos-x64-release +# cmake --build --preset macos-x64-release --target pea --parallel 4 cd ../.. ./bin/pea --help @@ -49,4 +60,4 @@ cd products getProducts.sh cd ../data getData.sh -``` \ No newline at end of file +``` diff --git a/scripts/installation/generic.md b/scripts/installation/generic.md index 60baeafdc..3809e3ed8 100644 --- a/scripts/installation/generic.md +++ b/scripts/installation/generic.md @@ -6,7 +6,7 @@ If instead you wish to build Ginan from source, there are several software depen * C/C++ and Fortran compiler. We use and recommend [gcc, g++, and gfortran](https://gcc.gnu.org) * BLAS and LAPACK linear algebra libraries. We use and recommend [OpenBlas](https://www.openblas.net/) as this contains both libraries required -* CMAKE > 3.0 +* CMAKE >= 3.22 * YAML > 0.6 * Boost >= 1.74 * MongoDB @@ -14,7 +14,7 @@ If instead you wish to build Ginan from source, there are several software depen * Mongo_cxx >= 3.9.0 * Eigen3 > 3.4 * netCDF4 -* Python >= 3.7 +* Python >= 3.9 *** @@ -29,9 +29,9 @@ sudo apt update sudo apt upgrade -y -sudo apt install -y git gobjc gobjc++ gfortran libopenblas-dev openssl curl net-tools libncurses5-dev openssh-server cmake make libssl1.0-dev wget sudo python3 software-properties-common +sudo apt install -y git gobjc gobjc++ gfortran libopenblas-dev openssl curl net-tools libncurses5-dev openssh-server cmake make libssl-dev wget sudo python3 software-properties-common -sudo -H pip3 install wheel pandas boto3 unlzw tdqm scipy gnssanalysis +sudo -H pip3 install wheel pandas boto3 unlzw tqdm scipy gnssanalysis ``` diff --git a/scripts/qzss_ohi_merge.py b/scripts/qzss_ohi_merge.py new file mode 100644 index 000000000..4e68615ab --- /dev/null +++ b/scripts/qzss_ohi_merge.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Merge QZSS operational history information (OHI) files into a single SINEX file. + +Specifically merges SATELLITE/ATTITUDE MODE blocks into the SATELLITE/ATTITUDE_MODE +SINEX format used by Ginan. + +Individual OHI files are available at: + https://qzss.go.jp/en/technical/qzssinfo/index.html + +SVN assignments follow igs_satellite_metadata.snx. Note that IGS does not assign J006; +the constellation goes J001-J005 then J007. + +Usage: + python qzss_ohi_merge.py + + input_dir - directory containing JAXA OHI files (ohi-qzs*.txt or ohi-qzs*.txt.gz) + output_file - path for the output SINEX file (e.g. qzss_yaw_modes.snx) + +Example: + python qzss_ohi_merge.py inputData/products/tables/qzss_ohi qzss_yaw_modes.snx +""" + +import argparse +import gzip +import sys +from pathlib import Path + + +# Mapping from JAXA OHI filename to IGS SVN identifier. +# Follows igs_satellite_metadata.snx — note IGS skips J006. +OHI_FILE_MAP = [ + ("ohi-qzs1.txt", "J001"), # QZS-1 (MICHIBIKI-1, launched 2010-09-11) + ("ohi-qzs2.txt", "J002"), # QZS-2I (MICHIBIKI-2, launched 2017-06-01) + ("ohi-qzs3.txt", "J003"), # QZS-2G (MICHIBIKI-3, launched 2017-08-19) + ("ohi-qzs4.txt", "J004"), # QZS-2I (MICHIBIKI-4, launched 2017-10-09) + ("ohi-qzs1r.txt", "J005"), # QZS-2A / QZS-1R (launched 2021-10-26) + ("ohi-qzs6.txt", "J007"), # QZS-3G / QZS-6 (launched 2025-02-02) +] + + +def open_ohi(path): + """Open an OHI file, transparently handling plain text and gzip formats.""" + if path.suffix == ".gz": + return gzip.open(path, "rt") + return open(path, "r") + + +def find_ohi_file(input_dir, filename): + """Locate an OHI file in input_dir, accepting plain (.txt) or gzip (.txt.gz) variants.""" + plain = input_dir / filename + gzipped = input_dir / (filename + ".gz") + if plain.exists(): + return plain + if gzipped.exists(): + return gzipped + return None + + +def format_attitude_line(sat_id, line): + """Reformat a single OHI CSV line into SINEX SATELLITE/ATTITUDE_MODE format.""" + if "#+SATELLITE/ATTITUDE MODE" in line: + return "+SATELLITE/ATTITUDE_MODE" + if "#-SATELLITE/ATTITUDE MODE" in line: + return "-SATELLITE/ATTITUDE_MODE\n" + if "#DATE TIME START(UTC),END(UTC),ATTITUDE MODE" in line: + return "*SVN_ DATE_TIME_START(UTC) END(UTC)___________ ATTITUDE_MODE" + + fields = line.split(",") + widths = [20, 19, 9] + formatted = " " + sat_id + " " + for i, field in enumerate(fields): + formatted += ("" if i == 0 else " ") + field[:widths[i]].ljust(widths[i]) + return formatted + + +def extract_attitude_block(path, sat_id): + """Extract and reformat the SATELLITE/ATTITUDE MODE block from an OHI file. + + Returns a list of formatted lines, or an empty list if no attitude block is found. + """ + lines = [] + in_block = False + with open_ohi(path) as f: + for raw in f: + line = raw.strip() + if "#+SATELLITE/ATTITUDE MODE" in line: + in_block = True + if in_block: + lines.append(format_attitude_line(sat_id, line)) + if "#-SATELLITE/" in line: + in_block = False + return lines + + +def merge(input_dir, output_path): + input_dir = Path(input_dir) + output_path = Path(output_path) + + found = [] + missing = [] + for filename, sat_id in OHI_FILE_MAP: + path = find_ohi_file(input_dir, filename) + if path: + found.append((path, sat_id)) + else: + missing.append((filename, sat_id)) + + for filename, sat_id in missing: + print(f"Warning: OHI file not found for {sat_id} ({filename}[.gz]) — skipping.", file=sys.stderr) + + if not found: + print("Error: No OHI files found in the input directory.", file=sys.stderr) + sys.exit(1) + + processed = [] + with open(output_path, "w") as out: + out.write("%=SNX\n") + out.write("*" + "-" * 79 + "\n") + out.write("*This file was created from the following OHI files using 'scripts/qzss_ohi_merge.py':\n") + for path, sat_id in found: + out.write(f"* '{path}' ({sat_id})\n") + out.write("*" + "-" * 79 + "\n\n") + + for path, sat_id in found: + block = extract_attitude_block(path, sat_id) + if not block: + print(f"Warning: No SATELLITE/ATTITUDE MODE section in {path.name} ({sat_id}) — skipping.", file=sys.stderr) + continue + for line in block: + out.write(line + "\n") + processed.append(sat_id) + + out.write("%ENDSNX\n") + + print(f"Written to '{output_path}' ({len(processed)} satellites: {', '.join(processed)}).") + + +def main(): + parser = argparse.ArgumentParser( + description="Merge QZSS OHI files into a Ginan SINEX SATELLITE/ATTITUDE_MODE file.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "input_dir", + help="Directory containing JAXA OHI files (ohi-qzs*.txt or ohi-qzs*.txt.gz)", + ) + parser.add_argument( + "output_file", + help="Output SINEX file path (e.g. qzss_yaw_modes.snx)", + ) + args = parser.parse_args() + merge(args.input_dir, args.output_file) + + +if __name__ == "__main__": + main() diff --git a/scripts/requirements.txt b/scripts/requirements.txt index f60d80ea3..21acb01d4 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -18,5 +18,5 @@ pytest>=7.1.2 scipy>=1.7.3 statsmodels>=0.13.2 hatanaka>=2.8.1 -gnssanalysis>=0.0.57 +gnssanalysis>=0.0.60.dev1 beautifulsoup4 diff --git a/scripts/ssrMonitoring/analyse_orbit_clock.py b/scripts/ssrMonitoring/analyse_orbit_clock.py index 6cc676f49..580c48703 100644 --- a/scripts/ssrMonitoring/analyse_orbit_clock.py +++ b/scripts/ssrMonitoring/analyse_orbit_clock.py @@ -577,7 +577,7 @@ def plot_orb_rms( rms[sat] = {dim: stats_df.loc[(sat, "rms"), dim] for dim in ["Radial", "Along-track", "Cross-track"]} rms_all = { dim: round(stats_df.loc[("All", "rms"), dim], 3) for dim in ["Radial", "Along-track", "Cross-track"] - } # TODO Eugene: RMS/statistics by constellation + } # TODO? RMS/statistics by constellation # Plot Radial, Along-track, and Cross-track RMS of each satellite fig, ax = plt.subplots(dpi=300.0, figsize=(12, 3)) @@ -629,7 +629,7 @@ def plot_clk_rms( rms = {} for sat in svs: rms[sat] = {"Clock": stats_df.loc[(sat, "rms"), "Clock"]} - rms_all = {"Clock": round(stats_df.loc[("All", "rms"), "Clock"], 3)} # TODO Eugene: RMS/statistics by constellation + rms_all = {"Clock": round(stats_df.loc[("All", "rms"), "Clock"], 3)} # TODO? RMS/statistics by constellation # Plot Clock RMS of each satellite fig, ax = plt.subplots(dpi=300.0, figsize=(12, 3)) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 354758528..e923d8d43 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,9 +11,9 @@ include(CheckPIESupported) check_pie_supported() # Use absolute paths for output directories to support nested build directories -set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../lib") -set (CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../lib") -set (CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../bin") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../lib") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../lib") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../bin") set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") set(CMAKE_CXX_VISIBILITY_PRESET hidden) @@ -54,7 +54,9 @@ endif () set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-misleading-indentation") # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-var-tracking-assignments") # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unknown-warning-option") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-extern-c-compat") + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-extern-c-compat") + endif() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-zero-length") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-array-bounds") @@ -210,19 +212,34 @@ if(USE_STATIC_BOOST) message(STATUS "Using static Boost libraries") else() set(Boost_USE_STATIC_LIBS OFF) - # Dynamic Boost requires additional compile definitions - add_compile_definitions(BOOST_LOG_DYN_LINK) message(STATUS "Using dynamic Boost libraries") endif() #set(Boost_NO_SYSTEM_PATHS ON) # Note: Boost system is header-only since 1.67, no longer needed as a component -find_package(Boost 1.75.0 REQUIRED COMPONENTS log log_setup date_time thread program_options serialization timer json) +find_package(Boost 1.75.0 REQUIRED COMPONENTS log log_setup date_time thread program_options serialization timer json unit_test_framework) + +if(TARGET Boost::log) + get_target_property(BOOST_LOG_LIBRARY_TYPE Boost::log TYPE) + if(BOOST_LOG_LIBRARY_TYPE STREQUAL "SHARED_LIBRARY") + add_compile_definitions(BOOST_LOG_DYN_LINK) + elseif(NOT USE_STATIC_BOOST) + message(STATUS "Shared Boost requested, but the available Boost::log target is ${BOOST_LOG_LIBRARY_TYPE}") + endif() +elseif(NOT USE_STATIC_BOOST) + # Legacy FindBoost variables do not expose the selected library type. + add_compile_definitions(BOOST_LOG_DYN_LINK) +endif() -# Try CONFIG mode first (for vcpkg), fall back to module mode (for brew/system) -find_package(Eigen3 3.3.0 CONFIG QUIET) +find_package(Eigen3 CONFIG QUIET) if(NOT Eigen3_FOUND) - find_package(Eigen3 3.3.0) + find_package(Eigen3 REQUIRED) +endif() +if(Eigen3_VERSION VERSION_LESS 3.3 OR Eigen3_VERSION VERSION_GREATER_EQUAL 6) + message(FATAL_ERROR "Found unsupported Eigen version ${Eigen3_VERSION}; expected Eigen >= 3.3 and < 6") +endif() +if(TARGET Eigen3::Eigen AND NOT EIGEN3_INCLUDE_DIRS) + get_target_property(EIGEN3_INCLUDE_DIRS Eigen3::Eigen INTERFACE_INCLUDE_DIRECTORIES) endif() include_directories(${EIGEN3_INCLUDE_DIRS}) @@ -257,34 +274,67 @@ endif() # Using local magic_enum header in 3rdparty directory -set(NETCDF_CXX "YES") -find_package(NetCDF) -if (NOT NetCDF_FOUND) - message(STATUS "NetCDF library not found, skip compiling loading packages.") -endif() - +# Pre-find dependencies for netCDF to ensure proper propagation of vcpkg variables +find_package(ZLIB REQUIRED) +find_package(CURL CONFIG) -# message(STATUS "...NETCDF >>>>>> ${NETCDF_LIBRARIES} ${NETCDF_INCLUDES}" ) -# message(STATUS "...NETCDF_C++ >>>>>> ${NETCDF_LIBRARIES_CXX} ${NETCDF_INCLUDES_CXX}" ) -# find_package(netCDFCxx REQUIRED) +# Find NetCDF libraries using vcpkg's modern CMake config +set(NETCDF_CXX "YES") +find_package(netCDF CONFIG) +find_package(netCDFCxx CONFIG) -# set(OPENBLAS_USE_STATIC_LIBS ON) -# set(BLA_VENDOR open) +if (netCDF_FOUND AND netCDFCxx_FOUND) + set(NetCDF_FOUND TRUE) + message(STATUS "Found NetCDF C library via vcpkg") + message(STATUS "Found NetCDF C++ library via vcpkg") +else() + # Fallback to legacy FindNetCDF.cmake for system installations + find_package(NetCDF) + if (NOT NetCDF_FOUND) + message(STATUS "NetCDF library not found, skip compiling loading packages.") + endif() +endif() -find_package(BLAS REQUIRED) -# Try to find LAPACK - OpenBLAS includes LAPACK so it may be found there -find_package(LAPACK QUIET) +if(BLA_VENDOR STREQUAL "OpenBLAS") + find_package(OpenBLAS CONFIG REQUIRED) + if(TARGET OpenBLAS::OpenBLAS) + set(BLAS_LIBRARIES OpenBLAS::OpenBLAS) + else() + set(BLAS_LIBRARIES ${OpenBLAS_LIBRARIES}) + endif() + set(BLAS_INCLUDE_DIRS ${OpenBLAS_INCLUDE_DIRS}) + + set(_GINAN_OPENBLAS_LIBRARIES ${BLAS_LIBRARIES}) + set(_GINAN_OPENBLAS_INCLUDE_DIRS ${BLAS_INCLUDE_DIRS}) + find_package(LAPACK REQUIRED) + if(APPLE) + find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED) + set(LAPACK_LIBRARIES ${ACCELERATE_FRAMEWORK}) + elseif(TARGET LAPACK::LAPACK) + set(LAPACK_LIBRARIES LAPACK::LAPACK) + elseif(NOT LAPACK_LIBRARIES) + # Some OpenBLAS installs include LAPACK symbols directly. + set(LAPACK_LIBRARIES ${_GINAN_OPENBLAS_LIBRARIES}) + endif() + set(BLAS_LIBRARIES ${_GINAN_OPENBLAS_LIBRARIES}) + set(BLAS_INCLUDE_DIRS ${_GINAN_OPENBLAS_INCLUDE_DIRS}) -if(LAPACK_FOUND) - message(STATUS "Found BLAS library: ${BLAS_LIBRARIES}") - message(STATUS "Found LAPACK library: ${LAPACK_LIBRARIES}") + message(STATUS "Using OpenBLAS for BLAS: ${BLAS_LIBRARIES}") + message(STATUS "Using LAPACK library: ${LAPACK_LIBRARIES}") else() - # OpenBLAS includes LAPACK but FindLAPACK might not detect it - # Use BLAS libraries which include LAPACK functionality - message(STATUS "Found BLAS library: ${BLAS_LIBRARIES}") - message(STATUS "LAPACK not found separately - assuming BLAS includes LAPACK (OpenBLAS)") - set(LAPACK_LIBRARIES ${BLAS_LIBRARIES}) + find_package(BLAS REQUIRED) + find_package(LAPACK QUIET) + + if(LAPACK_FOUND) + message(STATUS "Found BLAS library: ${BLAS_LIBRARIES}") + message(STATUS "Found LAPACK library: ${LAPACK_LIBRARIES}") + else() + # OpenBLAS includes LAPACK but FindLAPACK might not detect it. + message(STATUS "Found BLAS library: ${BLAS_LIBRARIES}") + message(STATUS "LAPACK not found separately - assuming BLAS includes LAPACK") + set(LAPACK_LIBRARIES ${BLAS_LIBRARIES}) + endif() endif() if (YAML_CPP_LIB) @@ -339,6 +389,8 @@ link_directories(/usr/lib64 ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) add_subdirectory(cpp) +enable_testing() +add_subdirectory(tests) message(STATUS "CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}") diff --git a/src/CMakePresets.json b/src/CMakePresets.json index 6f2cf0540..6b1413367 100644 --- a/src/CMakePresets.json +++ b/src/CMakePresets.json @@ -52,6 +52,9 @@ "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" + }, + "cacheVariables": { + "BLA_VENDOR": "OpenBLAS" } }, { @@ -61,6 +64,7 @@ "cacheVariables": { "VCPKG_TARGET_TRIPLET": "arm64-osx", "CMAKE_OSX_ARCHITECTURES": "arm64", + "OpenBLAS_DIR": "/opt/homebrew/opt/openblas/lib/cmake/openblas", "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/toolchain/clang_mac_arm64.cmake", "VCPKG_INSTALLED_DIR": "${sourceDir}/../vcpkg_installed/macos-arm64" } @@ -72,6 +76,7 @@ "cacheVariables": { "VCPKG_TARGET_TRIPLET": "x64-osx", "CMAKE_OSX_ARCHITECTURES": "x86_64", + "OpenBLAS_DIR": "/usr/local/opt/openblas/lib/cmake/openblas", "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/toolchain/clang_mac_x64.cmake", "VCPKG_INSTALLED_DIR": "${sourceDir}/../vcpkg_installed/macos-x64" } @@ -96,6 +101,7 @@ "VCPKG_TARGET_TRIPLET": "x64-mingw-static", "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/toolchain/mingw64.cmake", "VCPKG_INSTALLED_DIR": "${sourceDir}/../vcpkg_installed/mingw", + "OPENSSL_ROOT_DIR": "${sourceDir}/../vcpkg_installed/mingw/x64-mingw-static", "ENABLE_MONGODB": "OFF" } }, @@ -137,6 +143,16 @@ "CMAKE_BUILD_TYPE": "Debug" } }, + { + "name": "relwithdebinfo", + "displayName": "RelWithDebInfo", + "description": "Optimized build with debug symbols for profiling", + "binaryDir": "${sourceDir}/build/linux-RelWithDebInfo", + "inherits": ["linux-base"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, { "name": "windows-release", "displayName": "Windows Release", @@ -251,6 +267,11 @@ "configurePreset": "debug", "displayName": "Debug Build" }, + { + "name": "relwithdebinfo", + "configurePreset": "relwithdebinfo", + "displayName": "RelWithDebInfo Build" + }, { "name": "windows-release", "configurePreset": "windows-release", @@ -300,6 +321,24 @@ "name": "windows-mingw-debug", "configurePreset": "windows-mingw-debug", "displayName": "Windows MinGW Native Debug Build" + }, + { + "name": "make_otl_blq", + "configurePreset": "release", + "displayName": "Build make_otl_blq (NetCDF ocean tide loading)", + "targets": ["make_otl_blq"] + }, + { + "name": "interpolate_loading", + "configurePreset": "release", + "displayName": "Build interpolate_loading (NetCDF loading interpolation)", + "targets": ["interpolate_loading"] + }, + { + "name": "netcdf-programs", + "configurePreset": "release", + "displayName": "Build all NetCDF programs", + "targets": ["make_otl_blq", "interpolate_loading"] } ] } diff --git a/src/cmake/toolchain/mingw64.cmake b/src/cmake/toolchain/mingw64.cmake index 2301bd9dd..9d8a60dbf 100644 --- a/src/cmake/toolchain/mingw64.cmake +++ b/src/cmake/toolchain/mingw64.cmake @@ -16,6 +16,12 @@ set(CMAKE_RANLIB x86_64-w64-mingw32-ranlib) # Where to search for target environment set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) +# Add vcpkg installed directory to find root path if available +if(DEFINED _VCPKG_INSTALLED_DIR AND DEFINED VCPKG_TARGET_TRIPLET) + list(APPEND CMAKE_FIND_ROOT_PATH "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}") + message(STATUS "Added vcpkg install dir to CMAKE_FIND_ROOT_PATH: ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}") +endif() + # Adjust the default behavior of the FIND_XXX() commands: # search programs in the host environment set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) @@ -41,17 +47,20 @@ add_definitions(-D_LARGEFILE64_SOURCE) # Help FindOpenSSL locate libraries in vcpkg for cross-compilation # The vcpkg wrapper uses different variable names for WIN32 -if(DEFINED ENV{VCPKG_ROOT} AND DEFINED VCPKG_TARGET_TRIPLET) - set(OPENSSL_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../vcpkg_installed/${VCPKG_TARGET_TRIPLET}") - set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/include" CACHE PATH "OpenSSL include directory") - set(LIB_EAY "${OPENSSL_ROOT_DIR}/lib/libcrypto.a" CACHE FILEPATH "OpenSSL crypto library") - set(SSL_EAY "${OPENSSL_ROOT_DIR}/lib/libssl.a" CACHE FILEPATH "OpenSSL SSL library") +if(DEFINED _VCPKG_INSTALLED_DIR AND DEFINED VCPKG_TARGET_TRIPLET) + set(OPENSSL_ROOT_DIR "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}" CACHE PATH "OpenSSL root directory" FORCE) + set(OPENSSL_INCLUDE_DIR "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include" CACHE PATH "OpenSSL include directory" FORCE) + set(OPENSSL_CRYPTO_LIBRARY "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/libcrypto.a" CACHE FILEPATH "OpenSSL crypto library" FORCE) + set(OPENSSL_SSL_LIBRARY "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/libssl.a" CACHE FILEPATH "OpenSSL SSL library" FORCE) + set(LIB_EAY "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/libcrypto.a" CACHE FILEPATH "OpenSSL crypto library (legacy)" FORCE) + set(SSL_EAY "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/libssl.a" CACHE FILEPATH "OpenSSL SSL library (legacy)" FORCE) + message(STATUS "Set OpenSSL paths for cross-compilation: ${OPENSSL_ROOT_DIR}") # Help FindBLAS/FindLAPACK locate libraries - set(BLAS_LIBRARIES "${OPENSSL_ROOT_DIR}/lib/libopenblas.a" CACHE FILEPATH "BLAS library") - set(LAPACK_LIBRARIES "${OPENSSL_ROOT_DIR}/lib/liblapack.a;${OPENSSL_ROOT_DIR}/lib/libf2c.a;${OPENSSL_ROOT_DIR}/lib/libopenblas.a" CACHE FILEPATH "LAPACK library") + set(BLAS_LIBRARIES "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/libopenblas.a" CACHE FILEPATH "BLAS library" FORCE) + set(LAPACK_LIBRARIES "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/liblapack.a;${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/libf2c.a;${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/libopenblas.a" CACHE FILEPATH "LAPACK library" FORCE) # Help FindYAML_CPP locate libraries - set(YAML_CPP_LIBRARIES "${OPENSSL_ROOT_DIR}/lib/libyaml-cpp.a" CACHE FILEPATH "YAML-CPP library") - set(YAML_CPP_LIB "${OPENSSL_ROOT_DIR}/lib/libyaml-cpp.a" CACHE FILEPATH "YAML-CPP library (alternate variable)") + set(YAML_CPP_LIBRARIES "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/libyaml-cpp.a" CACHE FILEPATH "YAML-CPP library" FORCE) + set(YAML_CPP_LIB "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/libyaml-cpp.a" CACHE FILEPATH "YAML-CPP library (alternate variable)" FORCE) endif() diff --git a/src/cpp/3rdparty/sofa/CMakeLists.txt b/src/cpp/3rdparty/sofa/CMakeLists.txt index 8a057af22..f7d49b165 100644 --- a/src/cpp/3rdparty/sofa/CMakeLists.txt +++ b/src/cpp/3rdparty/sofa/CMakeLists.txt @@ -254,6 +254,7 @@ set(SOFA_SRC ) add_library(sofa_lib + STATIC ${SOFA_SRC}) target_compile_options(sofa_lib PRIVATE -fpie) diff --git a/src/cpp/CMakeLists.txt b/src/cpp/CMakeLists.txt index 6e27c5c06..6484fc093 100644 --- a/src/cpp/CMakeLists.txt +++ b/src/cpp/CMakeLists.txt @@ -9,6 +9,13 @@ add_executable(pea common/acsConfig.cpp common/acsConfigDocs.cpp + common/sanityCheckers/ConfigSanityManager.cpp + common/sanityCheckers/EphemerisTimeDelayChecker.cpp + common/sanityCheckers/EpochToleranceChecker.cpp + common/sanityCheckers/IonosphericFreeComboChecker.cpp + common/sanityCheckers/IonosphericOutageChecker.cpp + common/sanityCheckers/RequiredSiteEccentricityChecker.cpp + common/sanityCheckers/SbasSanityChecker.cpp 3rdparty/jpl/jpl_eph.cpp @@ -53,7 +60,7 @@ add_executable(pea common/algebra.cpp common/algebra_old.cpp common/algebraTrace.cpp - common/kalmanBlas.cpp + # common/kalmanBlas.cpp common/attitude.cpp common/compare.cpp common/antenna.cpp @@ -84,6 +91,7 @@ add_executable(pea common/ntripTrace.cpp common/orbits.cpp common/receiver.cpp + common/receiverMetadata.cpp common/rinex.cpp common/rtsSmoothing.cpp common/rtcmDecoder.cpp @@ -190,6 +198,8 @@ if(OpenMP_CXX_FOUND) endif() target_compile_definitions(pea PRIVATE + # LapackWrapper calls Fortran BLAS symbols directly, so its declarations + # must remain independent of Eigen's internal BLAS headers across versions. EIGEN_USE_BLAS=1 ) @@ -224,55 +234,101 @@ if (NetCDF_FOUND) loading/interpolate_loading.cpp ) - target_include_directories(otl PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} - ${YAML_INCLUDE_DIRS} - ${Boost_INCLUDE_DIRS} - ${NETCDF_INCLUDES} - ${NETCDF_INCLUDES_CXX} - ) + # Use modern CMake targets if available from vcpkg, otherwise use legacy variables + if (TARGET netCDF::netcdf-cxx4 AND TARGET netCDF::netcdf) + # Using vcpkg's modern CMake targets + target_include_directories(otl PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${YAML_INCLUDE_DIRS} + ${Boost_INCLUDE_DIRS} + ) - target_include_directories(make_otl_blq PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} - ${YAML_INCLUDE_DIRS} - ${Boost_INCLUDE_DIRS} - ${NETCDF_INCLUDES} - ${NETCDF_INCLUDES_CXX} - ) + target_include_directories(make_otl_blq PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${YAML_INCLUDE_DIRS} + ${Boost_INCLUDE_DIRS} + ) - target_include_directories(interpolate_loading PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} - ${YAML_INCLUDE_DIRS} - ${Boost_INCLUDE_DIRS} - ${NETCDF_INCLUDES} - ${NETCDF_INCLUDES_CXX} - ) + target_include_directories(interpolate_loading PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${YAML_INCLUDE_DIRS} + ${Boost_INCLUDE_DIRS} + ) + target_link_libraries(otl PUBLIC + netCDF::netcdf-cxx4 + netCDF::netcdf + ) - target_link_libraries(make_otl_blq PUBLIC - otl - ${NETCDF_LIBRARIES_CXX} - ${NETCDF_LIBRARIES} - Boost::timer - Boost::program_options - Boost::log - Boost::log_setup - ${YAML_CPP_LIBRARIES} - ${YAML_CPP_LIB} - ) + target_link_libraries(make_otl_blq PUBLIC + otl + Boost::timer + Boost::program_options + Boost::log + Boost::log_setup + ${YAML_CPP_LIBRARIES} + ${YAML_CPP_LIB} + ) - target_link_libraries(interpolate_loading PUBLIC - otl - ${NETCDF_LIBRARIES_CXX} - ${NETCDF_LIBRARIES} - Boost::timer - Boost::program_options - Boost::log - Boost::log_setup - ${YAML_CPP_LIBRARIES} - ${YAML_CPP_LIB} - ) + target_link_libraries(interpolate_loading PUBLIC + otl + Boost::timer + Boost::program_options + Boost::log + Boost::log_setup + ${YAML_CPP_LIBRARIES} + ${YAML_CPP_LIB} + ) + else() + # Legacy system-installation approach with FindNetCDF.cmake + target_include_directories(otl PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${YAML_INCLUDE_DIRS} + ${Boost_INCLUDE_DIRS} + ${NETCDF_INCLUDES} + ${NETCDF_INCLUDES_CXX} + ) + + target_include_directories(make_otl_blq PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${YAML_INCLUDE_DIRS} + ${Boost_INCLUDE_DIRS} + ${NETCDF_INCLUDES} + ${NETCDF_INCLUDES_CXX} + ) + target_include_directories(interpolate_loading PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${YAML_INCLUDE_DIRS} + ${Boost_INCLUDE_DIRS} + ${NETCDF_INCLUDES} + ${NETCDF_INCLUDES_CXX} + ) + + target_link_libraries(make_otl_blq PUBLIC + otl + ${NETCDF_LIBRARIES_CXX} + ${NETCDF_LIBRARIES} + Boost::timer + Boost::program_options + Boost::log + Boost::log_setup + ${YAML_CPP_LIBRARIES} + ${YAML_CPP_LIB} + ) + + target_link_libraries(interpolate_loading PUBLIC + otl + ${NETCDF_LIBRARIES_CXX} + ${NETCDF_LIBRARIES} + Boost::timer + Boost::program_options + Boost::log + Boost::log_setup + ${YAML_CPP_LIBRARIES} + ${YAML_CPP_LIB} + ) + endif() if(OpenMP_CXX_FOUND) target_link_libraries(make_otl_blq PUBLIC OpenMP::OpenMP_CXX) @@ -283,6 +339,11 @@ if(ENABLE_PARALLELISATION) target_compile_definitions(pea PRIVATE ENABLE_PARALLELISATION=1) endif() +string(TOLOWER "${BLAS_LIBRARIES};${LAPACK_LIBRARIES}" GINAN_BLAS_LIBRARIES_LOWER) +if(GINAN_BLAS_LIBRARIES_LOWER MATCHES "openblas") + target_compile_definitions(pea PRIVATE GINAN_USE_OPENBLAS=1) +endif() + target_link_libraries(pea PUBLIC diff --git a/src/cpp/common/acsConfig.cpp b/src/cpp/common/acsConfig.cpp index bd3fb7d59..7391b9900 100644 --- a/src/cpp/common/acsConfig.cpp +++ b/src/cpp/common/acsConfig.cpp @@ -25,6 +25,7 @@ #include "common/compare.hpp" #include "common/constants.hpp" #include "common/debug.hpp" +#include "common/sanityCheckers/ConfigSanityManager.hpp" #include "configurator/htmlFooterTemplate.hpp" #include "configurator/htmlHeaderTemplate.hpp" #include "pea/inputsOutputs.hpp" @@ -843,13 +844,15 @@ void outputDefaultSiblings( while ((pos_end = enums.find(',', pos_start)) != string::npos) { string token = enums.substr(pos_start, pos_end - pos_start); - pos_start = pos_end + 1; + boost::algorithm::trim(token); + pos_start = pos_end + 1; html << "\n" << htmlIndentor << ""; } // get last one string token = enums.substr(pos_start); + boost::algorithm::trim(token); html << "\n" << htmlIndentor << ""; @@ -1856,30 +1859,6 @@ bool tryGetEnumVec( return true; } -/** Use pointer arithmetic to keep track of variables that have been initialised - */ -template -void setInited(BASE& base, COMP& comp, bool init = true) -{ - if (init == false) - { - return; - } - - int offset = (char*)(&comp) - (char*)(&base); - - base.initialisedMap[offset] = true; -} - -/** Set an option manually - */ -template -void setOption(BASE& base, COMP& comp, VALUE value) -{ - comp = value; - setInited(base, comp); -} - /** Set the variables associated with kalman filter states from yaml */ void tryGetKalmanFromYaml( @@ -2068,7 +2047,11 @@ bool tryGetMappedList( } vector optsList; - found |= tryGetFromOpts(optsList, commandOpts, {key}); + if (tryGetFromOpts(optsList, commandOpts, {key})) + { + found = true; + mappedList.clear(); + } for (auto& value : optsList) { @@ -2173,34 +2156,6 @@ void tryGetStreamFromYaml( const string estimation_parameters_str = "4! estimation_parameters"; const string processing_options_str = "2! processing_options"; -/** Copy one parameter to another, if it has been initialised. - * - * Use pointer arithmetic to determine the offset of another parameter within its parent structure, - * assuming it has the same layout as this parameter in its parent. - */ -template -bool initIfNeeded(CONTAINER& thisContainer, const CONTAINER& thatContainer, ELEMENT& thisElement) -{ - CONTAINER* thisContainer_ptr = &thisContainer; - const CONTAINER* thatContainer_ptr = &thatContainer; - ELEMENT* thisElement_ptr = &thisElement; - ELEMENT* thatElement_ptr = (ELEMENT*)(((char*)thisElement_ptr) + - ((char*)thatContainer_ptr - (char*)thisContainer_ptr)); - - auto& thatElement = *thatElement_ptr; - - if (isInited(thatContainer, thatElement)) - { - thisElement = thatElement; - - setInited(thisContainer, thisElement); - - return true; - } - - return false; -} - CommonOptions& CommonOptions::operator+=(const CommonOptions& rhs) { initIfNeeded(*this, rhs, exclude); @@ -2287,9 +2242,9 @@ KalmanModel& KalmanModel::operator+=(const KalmanModel& rhs) SatelliteOptions& SatelliteOptions::operator+=(const SatelliteOptions& rhs) { - SatelliteKalmans ::operator+=(rhs); - CommonOptions :: operator+=(rhs); - OrbitOptions :: operator+=(rhs); + SatelliteKalmans::operator+=(rhs); + CommonOptions::operator+=(rhs); + OrbitOptions::operator+=(rhs); initIfNeeded(*this, rhs, error_model); initIfNeeded(*this, rhs, code_sigma); @@ -2302,8 +2257,8 @@ SatelliteOptions& SatelliteOptions::operator+=(const SatelliteOptions& rhs) ReceiverOptions& ReceiverOptions::operator+=(const ReceiverOptions& rhs) { - ReceiverKalmans ::operator+=(rhs); - CommonOptions :: operator+=(rhs); + ReceiverKalmans::operator+=(rhs); + CommonOptions::operator+=(rhs); rinex23Conv += rhs.rinex23Conv; @@ -2312,6 +2267,7 @@ ReceiverOptions& ReceiverOptions::operator+=(const ReceiverOptions& rhs) initIfNeeded(*this, rhs, apriori_pos); initIfNeeded(*this, rhs, antenna_type); initIfNeeded(*this, rhs, receiver_type); + initIfNeeded(*this, rhs, meta_priority); initIfNeeded(*this, rhs, domes_number); initIfNeeded(*this, rhs, site_description); @@ -3481,6 +3437,16 @@ void getOptionsFromYaml( tryGetFromYaml(thing, recNode, {"4@ receiver_type"}, "Type of gnss receiver hardware") ); } + { + auto& thing = recOpts.meta_priority; + bool found = tryGetEnumVec( + thing, + recNode, + {"4@ meta_priority"}, + "Priority order for resolving receiver metadata across config, sinex, rinex, and rtcm" + ); + setInited(recOpts, thing, found); + } { auto& thing = recOpts.domes_number; setInited( @@ -4346,10 +4312,8 @@ bool configure( ("yaml-defaults,Y", boost::program_options::value(), "Print set of parsed parameters and their default values according to their priority level (1-3), and generate configurator.html for visual editing of yaml files") ("config_description,d", boost::program_options::value(), "Configuration description") ("level,l", boost::program_options::value(), "Trace level") - ("fatal_message_level,L", boost::program_options::value(), "Fatal error level") - ("elevation_mask,e", boost::program_options::value(), "Elevation Mask") ("max_epochs,n", boost::program_options::value(), "Maximum Epochs") - ("epoch_interval,i", boost::program_options::value(), "Epoch Interval") + ("epoch_interval,i", boost::program_options::value(), "Epoch Interval") ("user,u", boost::program_options::value(), "Username for RTCM streams") ("pass,p", boost::program_options::value(), "Password for RTCM streams") ("config,y", boost::program_options::value>()->multitoken(), "Configuration file") @@ -4364,28 +4328,22 @@ bool configure( ("dcb_files", boost::program_options::value>()->multitoken(), "Code Bias (DCB) files") ("bsx_files", boost::program_options::value>()->multitoken(), "Bias Sinex (BSX) files") ("ion_files", boost::program_options::value>()->multitoken(), "Ionosphere (IONEX) files") - ("igrf_files", boost::program_options::value>()->multitoken(), "Geomagnetic field coefficients (IGRF) file") - ("ocean_tide_loading_blq_files", boost::program_options::value>()->multitoken(), "BLQ (Ocean tidal loading) files") - ("atmos_tide_loading_blq_files", boost::program_options::value>()->multitoken(), "BLQ (Atmospheric tidal loading) files") ("erp_files", boost::program_options::value>()->multitoken(), "ERP files") ("rnx_inputs,r", boost::program_options::value>()->multitoken(), "RINEX receiver inputs") ("ubx_inputs", boost::program_options::value>()->multitoken(), "UBX receiver inputs") ("sbf_inputs", boost::program_options::value>()->multitoken(), "SBF receiver inputs") ("rtcm_inputs", boost::program_options::value>()->multitoken(), "RTCM receiver inputs") - ("egm_files", boost::program_options::value>()->multitoken(), "Earth gravity model coefficients file") ("crd_files", boost::program_options::value>()->multitoken(), "SLR CRD file") - ("slr_inputs", boost::program_options::value>()->multitoken(), "Tabular SLR OBS receiver file") - ("planetary_ephemeris_files", boost::program_options::value>()->multitoken(), "JPL planetary and lunar ephemerides file") ("inputs_root", boost::program_options::value(), "Root to apply to non-absolute input locations") ("outputs_root", boost::program_options::value(), "Root to apply to non-absolute output locations") ("start_epoch", boost::program_options::value(), "Start date/time") ("end_epoch", boost::program_options::value(), "Stop date/time") + ("dry-run", "Parse config, perform sanity checks, and exit") // ("run_rts_only", boost::program_options::value(), "RTS filename (without _xxxxx suffix)") ("dump-config-only", "Dump the configuration and exit") ("compare_clocks", "Compare clock files") ("compare_orbits", "Compare sp3 files") - ("compare_attitudes", "Compare orbex files") - ; + ("compare_attitudes", "Compare orbex files"); boost::program_options::variables_map vm; @@ -4393,6 +4351,8 @@ bool configure( boost::program_options::notify(vm); + acsConfig.dry_run = vm.count("dry-run"); + if (vm.count("help") || argc == 1) { BOOST_LOG_TRIVIAL(info) << desc; @@ -4548,195 +4508,8 @@ bool configure( void ACSConfig::sanityChecks() { - if (ionErrors.outage_reset_limit < epoch_interval) - BOOST_LOG_TRIVIAL(warning) << "ionospheric_components:outage_reset_limit < " - "epoch_interval, but it probably shouldnt be"; - - if (simulate_real_time == false) - { - for (E_Sys sys : magic_enum::enum_values()) - { - eph_time_delay[sys] = default_eph_time_delay[sys]; - } - } - - if (pppOpts.ionoOpts.use_if_combo) - { - for (auto& [id, recOpts] : recOptsMap) - { - if (recOpts.ionospheric_component2) - { - setOption(recOpts, recOpts.ionospheric_component2, false); - BOOST_LOG_TRIVIAL(warning) - << "Higher-order ionospheric corrections are not supported when " - "use_if_combo is enabled, " - "setting ionospheric_components:use_2nd_order to false"; - } - if (recOpts.ionospheric_component3) - { - setOption(recOpts, recOpts.ionospheric_component3, false); - BOOST_LOG_TRIVIAL(warning) - << "Higher-order ionospheric corrections are not supported when " - "use_if_combo is enabled, " - "setting ionospheric_components:use_3rd_order to false"; - } - } - } - - if (process_sbas) - { - process_preprocessor = true; - process_spp = true; - - used_nav_types = sbsOpts.sbas_nav_types; - - for (auto& [id, satOpts] : satOptsMap) - { - vector sources = {E_Source::SBAS}; - setOption((CommonOptions&)satOpts, satOpts.posModel.enable, true); - setOption((CommonOptions&)satOpts, satOpts.posModel.sources, sources); - setOption((CommonOptions&)satOpts, satOpts.clockModel.enable, true); - setOption((CommonOptions&)satOpts, satOpts.clockModel.sources, sources); - } - - switch (sbsOpts.mode) - { - case E_SbasMode::L1: - { - BOOST_LOG_TRIVIAL(info) - << "L1 SBAS processing mode is selected, make sure that:\n" - " - You have inputs containing SBAS messages (sisnet, ems, sbf, etc.)\n" - " - Parameter `sbas_inputs: prec_approach` is set appropriately"; - - sbsInOpts.freq = 1; - - for (auto& [sys, process] : process_sys) - { - if (sys != E_Sys::GPS && sys != E_Sys::GLO && sys != E_Sys::SBS) - { - process = false; - } - else - { - code_priorities[sys] = {E_ObsCode::L1C}; - } - } - - sppOpts.trop_models = {E_TropModel::SBAS}; - sppOpts.iono_mode = E_IonoMode::SBAS; - - if (sppOpts.smooth_window != 100) - { - sppOpts.smooth_window = 100; - BOOST_LOG_TRIVIAL(warning) - << "It is recommended that a 100 second smoothing window be used for L1 " - "SBAS. Changing configuration"; - } - if (sppOpts.use_smooth_only == false) - { - sppOpts.use_smooth_only = true; - BOOST_LOG_TRIVIAL(warning) - << "It is NOT recommended that measurements be used for SBAS before " - "smoothing. Changing configuration"; - } - - if (sbsOpts.use_sbas_rec_var == false) - { - sbsOpts.use_sbas_rec_var = true; - BOOST_LOG_TRIVIAL(warning) - << "It is recommended that measurement variance specific for SBAS are " - "used. Changing configuration"; - } - - break; - } - - case E_SbasMode::DFMC: - { - BOOST_LOG_TRIVIAL(info) - << "DFMC processing mode is selected, make sure that:\n" - " - You have inputs containing SBAS messages (sisnet, ems, sbf, etc.)\n" - " - If using a service follwing DO-259 (instead of DO-259A), set " - "`sbas_inputs: use_do259: true`\n" - " - If using measurements from GLO or BDS, set the `code_priorities` and " - "`used_nav_type` properly\n"; - - sbsInOpts.freq = 5; - sbsInOpts.pvs_on_dfmc = false; - - for (auto& [sys, process] : process_sys) - { - if (sys == E_Sys::GLO || sys == E_Sys::LEO) - { - process = false; - } - else if (sys != E_Sys::BDS) - { - code_priorities[sys] = sbsOpts.sbas_code_priorities_map[sys]; - } - } - - sppOpts.trop_models = {E_TropModel::SBAS}; - sppOpts.iono_mode = E_IonoMode::SBAS; - - if (sppOpts.smooth_window < - 0) // Ken to update once the smooth window requirement is clear - BOOST_LOG_TRIVIAL(warning) - << "It is recommended that a 100 second smoothing window be used for DFMC. " - "Please check your configuration"; - - break; - } - - case E_SbasMode::PVS: - { - BOOST_LOG_TRIVIAL(info) - << "PVS-via-DFMC processing mode is selected, make sure that:\n" - " - You have inputs containing SBAS messages (sisnet, ems, sbf, etc.)\n" - " - The SBAS messages come from SouthPAN's DFMC services\n"; - - process_ppp = true; - - sbsInOpts.freq = 5; - sbsInOpts.pvs_on_dfmc = true; - - for (auto& [sys, process] : process_sys) - { - if (sys == E_Sys::GPS || sys == E_Sys::GAL) - { - process = true; - code_priorities[sys] = sbsOpts.sbas_code_priorities_map[sys]; - } - else - { - process = false; - } - } - - for (auto& [id, recOpts] : recOptsMap) - { - vector tropModels = {E_TropModel::GPT2}; - setOption(recOpts, recOpts.receiver_reference_system, E_Sys::GPS); - setOption(recOpts, recOpts.tropModel.enable, true); - setOption(recOpts, recOpts.tropModel.models, tropModels); - setOption(recOpts, recOpts.tideModels.otl, false); - setOption(recOpts, recOpts.tideModels.atl, false); - setOption(recOpts, recOpts.tideModels.spole, false); - setOption(recOpts, recOpts.tideModels.opole, false); - } - - sppOpts.always_reinitialise = true; - pppOpts.use_primary_signals = true; - // pppOpts.receiver_chunking = true; // Currently chunking may not work properly - errorAccumulation.enable = true; - ambErrors.phase_reject_limit = 2; - ambErrors.resetOnSlip.LLI = true; - ambErrors.resetOnSlip.retrack = true; - - break; - } - } - } + auto sanityManager = ConfigSanityManager::defaultManager(); + sanityManager.runAllChecks(*this); } bool ACSConfig::parse() @@ -4789,6 +4562,19 @@ bool ACSConfig::parse( satOptsMap.clear(); recOptsMap.clear(); defaultOutputOptions(); + exclude_sinex_blocks.clear(); + + // Clear input stream definitions so a live config reload reflects removals as well as adds. + sisnet_inputs.clear(); + nav_rtcm_inputs.clear(); + qzs_rtcm_inputs.clear(); + rnx_inputs.clear(); + ubx_inputs.clear(); + sbf_inputs.clear(); + custom_inputs.clear(); + obs_rtcm_inputs.clear(); + pseudo_sp3_inputs.clear(); + pseudo_snx_inputs.clear(); for (E_Sys sys : magic_enum::enum_values()) { @@ -6271,6 +6057,13 @@ bool ACSConfig::parse( "Allow adding inpuut files which do not (yet) exist" ); + tryGetFromYaml( + exclude_sinex_blocks, + inputs, + {"@ exclude_sinex_blocks"}, + "List of SINEX blocks to skip while parsing" + ); + auto getAppendFiles = [&](vector& output, NodeStack& nodeStack, const string& descriptor, @@ -6278,11 +6071,22 @@ bool ACSConfig::parse( { vector vec; - tryGetFromAny(vec, commandOpts, nodeStack, {descriptor}, comment); + bool foundOpts = tryGetFromOpts(vec, commandOpts, {descriptor}); + if (foundOpts == false) + { + tryGetFromYaml(vec, nodeStack, {descriptor}, comment); + } conditionalPrefix("", vec); - output.insert(output.end(), vec.begin(), vec.end()); + if (foundOpts) + { + output = vec; + } + else + { + output.insert(output.end(), vec.begin(), vec.end()); + } }; getAppendFiles(atx_files, inputs, {"4! atx_files"}, "List of atx files to use"); @@ -7701,15 +7505,6 @@ bool ACSConfig::parse( } { - tryGetEnumOpt( - filterOpts.inverter, - nodeStack, - {"@ inverter"}, - "Inverter to be used within the Kalman filter update stage, which may " - "provide different " - "performance outcomes in terms of processing time and accuracy and " - "stability." - ); tryGetFromYaml( filterOpts.joseph_stabilisation, nodeStack, diff --git a/src/cpp/common/acsConfig.hpp b/src/cpp/common/acsConfig.hpp index 32cd4dea4..d0cc8697e 100644 --- a/src/cpp/common/acsConfig.hpp +++ b/src/cpp/common/acsConfig.hpp @@ -43,6 +43,58 @@ bool isInited(const BASE& base, const COMP& comp) return inited; } +/** Use pointer arithmetic to keep track of variables that have been initialised + */ +template +void setInited(BASE& base, COMP& comp, bool init = true) +{ + if (init == false) + { + return; + } + + int offset = (char*)(&comp) - (char*)(&base); + + base.initialisedMap[offset] = true; +} + +/** Set an option manually + */ +template +void setOption(BASE& base, COMP& comp, VALUE value) +{ + comp = value; + setInited(base, comp); +} + +/** Copy one parameter to another, if it has been initialised. + * + * Use pointer arithmetic to determine the offset of another parameter within its parent structure, + * assuming it has the same layout as this parameter in its parent. + */ +template +bool initIfNeeded(CONTAINER& thisContainer, const CONTAINER& thatContainer, ELEMENT& thisElement) +{ + CONTAINER* thisContainer_ptr = &thisContainer; + const CONTAINER* thatContainer_ptr = &thatContainer; + ELEMENT* thisElement_ptr = &thisElement; + ELEMENT* thatElement_ptr = (ELEMENT*)(((char*)thisElement_ptr) + + ((char*)thatContainer_ptr - (char*)thisContainer_ptr)); + + auto& thatElement = *thatElement_ptr; + + if (isInited(thatContainer, thatElement)) + { + thisElement = thatElement; + + setInited(thisContainer, thisElement); + + return true; + } + + return false; +} + struct SsrInputOptions { double code_bias_valid_time = 3600; ///< Valid time period of SSR code biases @@ -87,6 +139,7 @@ struct InputOptions vector atx_files; vector snx_files; + vector exclude_sinex_blocks; vector nav_files; vector ems_files; vector sp3_files; @@ -484,11 +537,11 @@ struct MeasErrorHandler struct ErrorAccumulationHandler { - bool enable = false; - int receiver_error_count_threshold = 4; - int receiver_error_epochs_threshold = 4; - int satellite_error_count_threshold = 4; - int satellite_error_epochs_threshold = 1; + bool enable = true; + int receiver_error_count_threshold = 0; + int receiver_error_epochs_threshold = 0; + int satellite_error_count_threshold = 0; + int satellite_error_epochs_threshold = 0; int state_error_count_threshold = 4; }; @@ -665,10 +718,10 @@ struct PrefitOptions struct PostfitOptions { - int max_iterations = 2; + int max_iterations = 10; bool sigma_check = false; bool omega_test = true; - double state_sigma_threshold = 4; + double state_sigma_threshold = 6; double meas_sigma_threshold = 4; }; @@ -701,7 +754,6 @@ struct FilterOptions : RtsOptions bool joseph_stabilisation = false; E_Inverter lsq_inverter = E_Inverter::INV; - E_Inverter inverter = E_Inverter::LDLT; LeastSquareOptions lsqOpts; PrefitOptions prefitOpts; @@ -796,13 +848,11 @@ struct SbasOptions {E_Sys::QZS, E_NavMsgType::LNAV} }; + /// todo: May need to update this for BDS once ICD is released map> sbas_code_priorities_map = { {E_Sys::GPS, {E_ObsCode::L1C, E_ObsCode::L5Q, E_ObsCode::L5X}}, {E_Sys::GAL, {E_ObsCode::L1C, E_ObsCode::L5Q, E_ObsCode::L1X, E_ObsCode::L5X}}, - {E_Sys::BDS, - {E_ObsCode::L1C, - E_ObsCode::L5Q, - E_ObsCode::L5X}}, // Eugene: May need to update this for BDS once ICD is released + {E_Sys::BDS, {E_ObsCode::L1C, E_ObsCode::L5Q, E_ObsCode::L5X}}, {E_Sys::QZS, {E_ObsCode::L1C, E_ObsCode::L5Q, E_ObsCode::L5X}}, {E_Sys::SBS, {E_ObsCode::L1C, E_ObsCode::L5Q}} }; @@ -1014,9 +1064,9 @@ struct SatelliteKalmans : CommonKalmans, InertialKalmans, EmpKalmans SatelliteKalmans& operator+=(const SatelliteKalmans& rhs) { - CommonKalmans :: operator+=(rhs); - InertialKalmans ::operator+=(rhs); - EmpKalmans :: operator+=(rhs); + CommonKalmans::operator+=(rhs); + InertialKalmans::operator+=(rhs); + EmpKalmans::operator+=(rhs); return *this; } @@ -1037,9 +1087,9 @@ struct ReceiverKalmans : CommonKalmans, InertialKalmans, EmpKalmans ReceiverKalmans& operator+=(const ReceiverKalmans& rhs) { - CommonKalmans :: operator+=(rhs); - InertialKalmans ::operator+=(rhs); - EmpKalmans :: operator+=(rhs); + CommonKalmans::operator+=(rhs); + InertialKalmans::operator+=(rhs); + EmpKalmans::operator+=(rhs); ambiguity += rhs.ambiguity; strain_rate += rhs.strain_rate; @@ -1233,16 +1283,22 @@ struct ReceiverOptions : ReceiverKalmans, CommonOptions Rinex23Conversion rinex23Conv; - bool kill = false; - vector zero_dcb_codes = {}; - Vector3d apriori_pos = Vector3d::Zero(); - string antenna_type; - string receiver_type; - string domes_number; - string site_description; - string sat_id; - double elevation_mask_deg = 5; - E_Sys receiver_reference_system = E_Sys::NONE; + bool kill = false; + vector zero_dcb_codes = {}; + Vector3d apriori_pos = Vector3d::Zero(); + string antenna_type; + string receiver_type; + vector meta_priority = { + E_ReceiverMetaSource::CONFIG, + E_ReceiverMetaSource::SINEX, + E_ReceiverMetaSource::RINEX, + E_ReceiverMetaSource::RTCM + }; + string domes_number; + string site_description; + string sat_id; + double elevation_mask_deg = 5; + E_Sys receiver_reference_system = E_Sys::NONE; struct { @@ -1250,6 +1306,11 @@ struct ReceiverOptions : ReceiverKalmans, CommonOptions Vector3d eccentricity = Vector3d::Zero(); } eccentricityModel; + ReceiverOptions() + { + posModel.sources = {E_Source::KALMAN, E_Source::META, E_Source::SPP, E_Source::REMOTE}; + } + struct { bool enable = true; @@ -1467,6 +1528,7 @@ struct ACSConfig : GlobalOptions, InputOptions, OutputOptions, DebugOptions vector includedFilenames; map configModifyTimeMap; boost::program_options::variables_map commandOpts; + bool dry_run = false; static map docs; diff --git a/src/cpp/common/acsQC.cpp b/src/cpp/common/acsQC.cpp index 8c694b8ce..9ba2d5f14 100644 --- a/src/cpp/common/acsQC.cpp +++ b/src/cpp/common/acsQC.cpp @@ -1,6 +1,7 @@ // #pragma GCC optimize ("O0") #include "common/acsQC.hpp" +#include #include #include #include "common/acsConfig.hpp" @@ -19,13 +20,167 @@ #define PDESLIPTHRESHOLD 0.5 #define PROC_NOISE_IONO 0.001 +enum class E_SlipDiagReason +{ + NONE, + RECEIVER_LLI, + NO_FREQUENCY_PAIR, + NO_FREQUENCIES, + INVALID_LC, + NO_PREVIOUS_GF, + NO_PREVIOUS_MW, + PHASE_JUMP, + MW_JUMP, + WITHIN_THRESHOLD, + FIRST_EPOCH, + TIME_GAP, + LOW_ELEVATION, + DUAL_FREQUENCY, + TRIPLE_FREQUENCY, + THIRD_FREQUENCY_REACQUIRED, + FREQUENCY_REACQUIRED, + SINGLE_FREQUENCY, + MISSING_FREQUENCY_VARIANCE, + INVALID_CODE_VARIANCE, + INVALID_PHASE_VARIANCE, + VALID_VARIANCE, + LOM_WITHIN_THRESHOLD, + LOM_OUTLIER, + LAMBDA_FIXED, + LAMBDA_FLOAT, + KALMAN_WAITING +}; + +/** Emit one structured cycle-slip diagnostic trace line. + * + * These lines are intended to make detector decisions auditable without changing + * the detector result. They are emitted at trace level 2 with stable + * key=value fields so trace files can be summarised with simple text tools. + */ +void traceSlipEvent( + Trace& trace, + const char* detector, + const GObs& obs, + const char* action, + E_FType frq1, + E_FType frq2, + E_FType frq3, + double value1, + double value2, + double threshold, + E_SlipDiagReason reason +) +{ + tracepdeex( + 2, + trace, + "\nPDE-CS-DIAG detector=%s action=%s epoch=%s rec=%s sat=%s f1=%s f2=%s f3=%s " + "value1=%.6f value2=%.6f threshold=%.6f reason=%s", + detector, + action, + obs.time.to_string(2).c_str(), + obs.mount.c_str(), + obs.Sat.id().c_str(), + enum_to_string(frq1).c_str(), + enum_to_string(frq2).c_str(), + enum_to_string(frq3).c_str(), + value1, + value2, + threshold, + enum_to_lowerstring(reason).c_str() + ); +} + +/** Emit a structured diagnostic line for SCDIA internals. + * + * `PDE-SCDIA-DIAG` is intentionally separate from `PDE-CS-DIAG` so existing + * detector-routing summaries remain stable while SCDIA outcomes become + * machine-readable. + */ +void traceScdiaEvent( + Trace& trace, + const GObs& obs, + const char* action, + E_FType frq1, + E_FType frq2, + E_FType frq3, + int nf, + E_FilterMode filterMode, + double value1, + double value2, + double threshold, + double amb1, + double amb2, + double amb3, + E_SlipDiagReason reason +) +{ + tracepdeex( + 2, + trace, + "\nPDE-SCDIA-DIAG action=%s epoch=%s rec=%s sat=%s nf=%d mode=%s f1=%s f2=%s f3=%s " + "value1=%.6f value2=%.6f threshold=%.6f amb1=%.6f amb2=%.6f amb3=%.6f reason=%s", + action, + obs.time.to_string(2).c_str(), + obs.mount.c_str(), + obs.Sat.id().c_str(), + nf, + enum_to_string(filterMode).c_str(), + enum_to_string(frq1).c_str(), + enum_to_string(frq2).c_str(), + enum_to_string(frq3).c_str(), + value1, + value2, + threshold, + amb1, + amb2, + amb3, + enum_to_lowerstring(reason).c_str() + ); +} + +double lomThreshold(int dof) +{ + const double chisqr_arr[100] = { + 10.8, 13.8, 16.3, 18.5, 20.5, 22.5, 24.3, 26.1, 27.9, 29.6, 31.3, 32.9, 34.5, 36.1, 37.7, + 39.3, 40.8, 42.3, 43.8, 45.3, 46.8, 48.3, 49.7, 51.2, 52.6, 54.1, 55.5, 56.9, 58.3, 59.7, + 61.1, 62.5, 63.9, 65.2, 66.6, 68.0, 69.3, 70.7, 72.1, 73.4, 74.7, 76.0, 77.3, 78.6, 80.0, + 81.3, 82.6, 84.0, 85.4, 86.7, 88.0, 89.3, 90.6, 91.9, 93.3, 94.7, 96.0, 97.4, 98.7, 100, + 101, 102, 103, 104, 105, 107, 108, 109, 110, 112, 113, 114, 115, 116, 118, + 119, 120, 122, 123, 125, 126, 127, 128, 129, 131, 132, 133, 134, 135, 137, + 138, 139, 140, 142, 143, 144, 145, 147, 148, 149 + }; + + if (dof <= 0 || dof > 100) + return 0; + + return chisqr_arr[dof - 1] / dof; +} + +/** Select up to three configured frequency bands for a satellite system. + * + * This is the legacy frequency selector used by several modelling paths outside + * slip detection. It follows `acsConfig.code_priorities[sys]` and converts the + * first unique codes into frequency bands, but it does not inspect a particular + * observation to confirm that measurements are present. + * + * The outputs retain historical defaults (`F1`, `F2`, `F5`) when priorities are + * incomplete. Do not use this helper when the algorithm must know which + * frequencies are actually observed at the current epoch; use `obsFreqs()` for + * slip detection. + * + * @param[in] sys Satellite system. + * @param[out] ft1 First configured frequency band. + * @param[out] ft2 Second configured frequency band. + * @param[out] ft3 Third configured frequency band. + * + * @return false only when no code priorities exist for `sys`; true otherwise. + */ bool satFreqs(E_Sys sys, E_FType& ft1, E_FType& ft2, E_FType& ft3) { bool ft1Ready = false; bool ft2Ready = false; - // Add defaults in case someone forgets to initialise them... - // todo Eugene: Freqs may be duplicate! Initialise with NONE and return a list of unique freqs! ft1 = F1; ft2 = F2; ft3 = F5; @@ -65,6 +220,153 @@ bool satFreqs(E_Sys sys, E_FType& ft1, E_FType& ft2, E_FType& ft3) return true; } + +/** Select observed frequency bands that are usable for slip detection. + * + * This helper is intentionally stricter than `satFreqs()`: it still honours + * configured code priorities, but only returns frequency bands that are present + * in the current observation, have a non-zero representative phase measurement, + * and have a non-zero wavelength available in the satellite navigation data. + * + * This prevents GF/MW/PDE slip checks from evaluating assumed frequency bands + * when an epoch is missing one of the configured signals. + * + * @param[in] obs Observation to inspect. + * @param[out] ft1 First observed usable frequency, or `NONE`. + * @param[out] ft2 Second observed usable frequency, or `NONE`. + * @param[out] ft3 Third observed usable frequency, or `NONE`. + * + * @return Number of usable observed frequencies written to the output + * parameters, from 0 to 3. + */ +int obsFreqs(const GObs& obs, E_FType& ft1, E_FType& ft2, E_FType& ft3) +{ + ft1 = NONE; + ft2 = NONE; + ft3 = NONE; + + E_Sys sys = obs.Sat.sys; + if (acsConfig.code_priorities.find(sys) == acsConfig.code_priorities.end()) + return 0; + + if (obs.satNav_ptr == nullptr) + return 0; + + auto sysCodeIt = code2Freq.find(sys); + if (sysCodeIt == code2Freq.end()) + return 0; + + int count = 0; + + for (auto& code : acsConfig.code_priorities[sys]) + { + auto codeIt = sysCodeIt->second.find(code); + if (codeIt == sysCodeIt->second.end()) + continue; + + E_FType ft = codeIt->second; + if (ft == NONE || ft == ft1 || ft == ft2 || ft == ft3) + continue; + + auto sigIt = obs.sigs.find(ft); + if (sigIt == obs.sigs.end() || sigIt->second.L == 0) + continue; + + auto lamIt = obs.satNav_ptr->lamMap.find(ft); + if (lamIt == obs.satNav_ptr->lamMap.end() || lamIt->second == 0) + continue; + + if (count == 0) + { + ft1 = ft; + count++; + continue; + } + if (count == 1) + { + ft2 = ft; + count++; + continue; + } + { + ft3 = ft; + count++; + break; + } + } + + return count; +} + +struct SlipNoise +{ + double sigmaCode = 0; + double sigmaPhase = 0; + E_SlipDiagReason reason = E_SlipDiagReason::NONE; +}; + +/** Derive conservative slip-detector noise from the selected observation bands. + * + * Slip detection chooses concrete observed frequencies with `obsFreqs()`. The + * corresponding noise values must come from those same bands; using an + * arbitrary first signal can pass zero or invalid variances into PDE/SCDIA and + * produce NaNs in trace output. + * + * This helper requires each selected band to have finite, positive code and + * phase variance. It then uses the largest selected variance for each + * observable type as a conservative single-noise input to the current PDE/SCDIA + * equations. + * + * @param[in] obs Observation containing selected signals. + * @param[in] freqs Selected frequency bands. + * @param[in] nf Number of selected frequencies to validate. + * @param[out] noise Derived standard deviations, or failure reason. + * + * @return true when all selected bands have usable variances. + */ +bool slipNoise(const GObs& obs, const E_FType freqs[], int nf, SlipNoise& noise) +{ + double maxCodeVar = 0; + double maxPhaseVar = 0; + + for (int i = 0; i < nf; i++) + { + auto sigIt = obs.sigs.find(freqs[i]); + if (sigIt == obs.sigs.end()) + { + noise.reason = E_SlipDiagReason::MISSING_FREQUENCY_VARIANCE; + return false; + } + + double codeVar = sigIt->second.codeVar; + double phasVar = sigIt->second.phasVar; + + if (!std::isfinite(codeVar) || codeVar <= 0) + { + noise.reason = E_SlipDiagReason::INVALID_CODE_VARIANCE; + return false; + } + + if (!std::isfinite(phasVar) || phasVar <= 0) + { + noise.reason = E_SlipDiagReason::INVALID_PHASE_VARIANCE; + return false; + } + + if (codeVar > maxCodeVar) + maxCodeVar = codeVar; + + if (phasVar > maxPhaseVar) + maxPhaseVar = phasVar; + } + + noise.sigmaCode = sqrt(maxCodeVar); + noise.sigmaPhase = sqrt(maxPhaseVar); + noise.reason = E_SlipDiagReason::VALID_VARIANCE; + + return true; +} + /** Detect cycle slip by reported loss of lock */ void detslp_ll( @@ -107,14 +409,18 @@ void detslp_ll( continue; } - tracepdeex( - 3, + traceSlipEvent( trace, - "\n%s: slip detected: epoch=%s sat=%s f=%s\n", - __FUNCTION__, - obs.time.to_string(2).c_str(), - obs.Sat.id().c_str(), - enum_to_string(ft) + "LLI", + obs, + "detected", + ft, + NONE, + NONE, + 1, + 0, + 0, + E_SlipDiagReason::RECEIVER_LLI ); obs.satStat_ptr->sigStatMap[ft2string(ft)].slip.LLI = true; @@ -158,15 +464,43 @@ void detslp_gf( E_FType frq1; E_FType frq2; E_FType frq3; - bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); - if (pass == false) + int nf = obsFreqs(obs, frq1, frq2, frq3); + if (nf < 2) + { + traceSlipEvent( + trace, + "GF", + obs, + "skipped", + NONE, + NONE, + NONE, + 0, + 0, + 0, + E_SlipDiagReason::NO_FREQUENCY_PAIR + ); continue; + } S_LC& lc = getLC(obs.satStat_ptr->lc_new, frq1, frq2); double gf1 = lc.GF_Phas_m; if (lc.valid == false || gf1 == 0) { + traceSlipEvent( + trace, + "GF", + obs, + "skipped", + frq1, + frq2, + NONE, + gf1, + 0, + 0, + E_SlipDiagReason::INVALID_LC + ); continue; } @@ -175,37 +509,57 @@ void detslp_gf( if (gf0 == 0) { + traceSlipEvent( + trace, + "GF", + obs, + "initialised", + frq1, + frq2, + NONE, + gf1, + gf0, + 0, + E_SlipDiagReason::NO_PREVIOUS_GF + ); continue; } - tracepdeex( - 3, - trace, - "\n%s: epoch=%s sat=%s gf0=%f gf1=%f", - __FUNCTION__, - obs.time.to_string(2).c_str(), - obs.Sat.id().c_str(), - gf0, - gf1 - ); - if (fabs(gf1 - gf0) > acsConfig.preprocOpts.slip_threshold) { - tracepdeex( - 3, - trace, - "\n%s: slip detected: epoch=%s sat=%s gf0=%f gf1=%f", - __FUNCTION__, - obs.time.to_string(2).c_str(), - obs.Sat.id().c_str(), - gf0, - gf1 - ); - obs.satStat_ptr->sigStatMap[ft2string(frq1)].slip.GF = true; obs.satStat_ptr->sigStatMap[ft2string(frq2)].slip.GF = true; obs.satStat_ptr->sigStatMap[ft2string(frq1)].savedSlip.GF = true; obs.satStat_ptr->sigStatMap[ft2string(frq2)].savedSlip.GF = true; + traceSlipEvent( + trace, + "GF", + obs, + "detected", + frq1, + frq2, + NONE, + gf1, + gf0, + acsConfig.preprocOpts.slip_threshold, + E_SlipDiagReason::PHASE_JUMP + ); + } + else + { + traceSlipEvent( + trace, + "GF", + obs, + "accepted", + frq1, + frq2, + NONE, + gf1, + gf0, + acsConfig.preprocOpts.slip_threshold, + E_SlipDiagReason::WITHIN_THRESHOLD + ); } } } @@ -246,15 +600,43 @@ void detslp_mw( E_FType frq1; E_FType frq2; E_FType frq3; - bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); - if (pass == false) + int nf = obsFreqs(obs, frq1, frq2, frq3); + if (nf < 2) + { + traceSlipEvent( + trace, + "MW", + obs, + "skipped", + NONE, + NONE, + NONE, + 0, + 0, + 0, + E_SlipDiagReason::NO_FREQUENCY_PAIR + ); continue; + } S_LC& lc = getLC(obs.satStat_ptr->lc_new, frq1, frq2); double mw1 = lc.MW_c; if (lc.valid == false || mw1 == 0) { + traceSlipEvent( + trace, + "MW", + obs, + "skipped", + frq1, + frq2, + NONE, + mw1, + 0, + 0, + E_SlipDiagReason::INVALID_LC + ); continue; } @@ -263,37 +645,57 @@ void detslp_mw( if (mw0 == 0) { + traceSlipEvent( + trace, + "MW", + obs, + "initialised", + frq1, + frq2, + NONE, + mw1, + mw0, + 0, + E_SlipDiagReason::NO_PREVIOUS_MW + ); continue; } - tracepdeex( - 3, - trace, - "\n%s: epoch=%s sat=%s mw0=%f mw1=%f", - __FUNCTION__, - obs.time.to_string(2).c_str(), - obs.Sat.id().c_str(), - mw0, - mw1 - ); - if (fabs(mw1 - mw0) > THRES_MW_JUMP) { - tracepdeex( - 3, - trace, - "\n%s: slip detected: epoch=%s sat=%s mw0=%f mw1=%f", - __FUNCTION__, - obs.time.to_string(2).c_str(), - obs.Sat.id().c_str(), - mw0, - mw1 - ); - obs.satStat_ptr->sigStatMap[ft2string(frq1)].slip.MW = true; obs.satStat_ptr->sigStatMap[ft2string(frq2)].slip.MW = true; obs.satStat_ptr->sigStatMap[ft2string(frq1)].savedSlip.MW = true; obs.satStat_ptr->sigStatMap[ft2string(frq2)].savedSlip.MW = true; + traceSlipEvent( + trace, + "MW", + obs, + "detected", + frq1, + frq2, + NONE, + mw1, + mw0, + THRES_MW_JUMP, + E_SlipDiagReason::MW_JUMP + ); + } + else + { + traceSlipEvent( + trace, + "MW", + obs, + "accepted", + frq1, + frq2, + NONE, + mw1, + mw0, + THRES_MW_JUMP, + E_SlipDiagReason::WITHIN_THRESHOLD + ); } } } @@ -320,26 +722,20 @@ void scdia( Trace& trace, ///< Trace to output to SatStat& satStat, ///< Persistant satellite status parameters lc_t& lc, ///< Linear combinations + const GObs& obs, ///< Observation context for diagnostics map& lam, ///< Signal wavelength map double sigmaPhase, ///< Phase noise double sigmaCode, ///< Code noise int nf, ///< Number of frequencies - E_Sys sys, ///< Satellite system - E_FilterMode filterMode ///< LSQ/Kalman filter flag + E_FilterMode filterMode, ///< LSQ/Kalman filter flag + E_FType frq1, + E_FType frq2, + E_FType frq3 ) { if (nf == 0) return; - E_FType frq1; - E_FType frq2; - E_FType frq3; - bool pass = satFreqs(sys, frq1, frq2, frq3); - if (pass == false) - { - return; - } - lc_t* lc_pre_ptr; if (filterMode == E_FilterMode::LSQ) @@ -399,14 +795,53 @@ void scdia( /* perform LOM test for outlier detection */ /* design matrix for LOM test */ - MatrixXd Hlom = H.leftCols(2); - VectorXd v = VectorXd::Zero(m); - int ind = lsqqc(trace, Hlom.data(), R.data(), Z.data(), v.data(), m, 2, 0, 0); + MatrixXd Hlom = H.leftCols(2); + VectorXd v = VectorXd::Zero(m); + int ind = lsqqc(trace, Hlom.data(), R.data(), Z.data(), v.data(), m, 2, 0, 0); + double vtpv = v.dot(R * v); + int dof = m - 2; + double val = dof > 0 ? vtpv / dof : 0; + double thres = lomThreshold(dof); if (ind == 0) { + traceScdiaEvent( + trace, + obs, + "accepted", + frq1, + frq2, + frq3, + nf, + filterMode, + vtpv, + val, + thres, + 0, + 0, + 0, + E_SlipDiagReason::LOM_WITHIN_THRESHOLD + ); return; } + traceScdiaEvent( + trace, + obs, + "detected", + frq1, + frq2, + frq3, + nf, + filterMode, + vtpv, + val, + thres, + 0, + 0, + 0, + E_SlipDiagReason::LOM_OUTLIER + ); + satStat.sigStatMap[ft2string(frq1)].slip.SCDIA = true; satStat.sigStatMap[ft2string(frq2)].slip.SCDIA = true; if (nf == 3) @@ -445,6 +880,23 @@ void scdia( satStat.flt.slip = 0; satStat.flt.ne = 0; + traceScdiaEvent( + trace, + obs, + "waiting", + frq1, + frq2, + frq3, + nf, + filterMode, + satStat.flt.ne, + 2, + 0, + 0, + 0, + 0, + E_SlipDiagReason::KALMAN_WAITING + ); return; } @@ -493,8 +945,28 @@ void scdia( /* integer cycle slip estimation */ MatrixXd F = MatrixXd::Zero(nf, 2); double s[2]; + bool pass = false; lambda(trace, nf, 2, a.data(), Qa.data(), F.data(), s, acsConfig.predefined_fail, pass); + double ratio = s[1] != 0 ? s[0] / s[1] : 0; + traceScdiaEvent( + trace, + obs, + pass ? "fixed" : "float", + frq1, + frq2, + frq3, + nf, + filterMode, + s[0], + s[1], + ratio, + F.data()[0], + nf > 1 ? F.data()[1] : 0, + nf > 2 ? F.data()[2] : 0, + pass ? E_SlipDiagReason::LAMBDA_FIXED : E_SlipDiagReason::LAMBDA_FLOAT + ); + if (filterMode == E_FilterMode::LSQ) { /* least-squares */ @@ -505,13 +977,8 @@ void scdia( if (pass) { tracepdeex(2, trace, "fixed "); - for (int i = 0; i < 3; i++) + for (int i = 0; i < nf; i++) satStat.amb[i] = ROUND(F.data()[i]); - - for (auto& [key, sigStat] : satStat.sigStatMap) - { - sigStat.slip.SCDIA = true; - } } } else @@ -523,7 +990,7 @@ void scdia( if (pass) { memset(satStat.flt.a, 0, 3 * sizeof(double)); - memset(satStat.flt.Qa, 0, 9); // todo aaron, looks sketchy + memset(satStat.flt.Qa, 0, 9); // todo? looks sketchy satStat.flt.slip |= 2; tracepdeex(1, trace, " ACC fixed "); for (int i = 0; i < nf; i++) @@ -576,13 +1043,11 @@ void cycleslip2( satStat.sigmaIono = 0.001; } - auto sys = lcBase.Sat.sys; - E_FType frq1; E_FType frq2; E_FType frq3; - bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); - if (pass == false) + int nf = obsFreqs(obs, frq1, frq2, frq3); + if (nf < 2) { return; } @@ -592,14 +1057,21 @@ void cycleslip2( double lam1 = lam[frq1]; double lam2 = lam[frq2]; - double lamw = lam1 * lam2 / (lam2 - lam1); // todo aaron, rename + double lamw = lam1 * lam2 / (lam2 - lam1); // todo? rename /* ionosphere coefficient */ double coef = SQR(lam2) / SQR(lam1) - 1; - /* elevation dependent noise */ - double sigmaCode = sqrt(obs.sigs.begin()->second.codeVar); - double sigmaPhase = sqrt(obs.sigs.begin()->second.phasVar); + E_FType freqs[] = {frq1, frq2}; + SlipNoise noise; + if (!slipNoise(obs, freqs, 2, noise)) + { + traceSlipEvent(trace, "PDE", obs, "skipped", frq1, frq2, NONE, 0, 0, 0, noise.reason); + return; + } + + double sigmaCode = noise.sigmaCode; + double sigmaPhase = noise.sigmaPhase; double sigmaGF = 2 * sigmaPhase; @@ -644,7 +1116,20 @@ void cycleslip2( /* cycle slip detection */ if (satStat.el >= recOpts.elevation_mask_deg * D2R) { - scdia(trace, satStat, lcBase, lam, sigmaPhase, sigmaCode, 2, sys, E_FilterMode::LSQ); + scdia( + trace, + satStat, + lcBase, + obs, + lam, + sigmaPhase, + sigmaCode, + 2, + E_FilterMode::LSQ, + frq1, + frq2, + frq3 + ); } /* update TD ionosphere residual */ @@ -693,13 +1178,11 @@ void cycleslip3( satStat.sigmaIono = 0.001; } - auto sys = lc.Sat.sys; - E_FType frq1; E_FType frq2; E_FType frq3; - bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); - if (pass == false) + int nf = obsFreqs(obs, frq1, frq2, frq3); + if (nf < 3) return; auto& lam = obs.satNav_ptr->lamMap; @@ -712,9 +1195,16 @@ void cycleslip3( if (lamew < 0) lamew *= -1; - /* elevation dependent noise */ - double sigmaCode = sqrt(obs.sigs.begin()->second.codeVar); - double sigmaPhase = sqrt(obs.sigs.begin()->second.phasVar); + E_FType freqs[] = {frq1, frq2, frq3}; + SlipNoise noise; + if (!slipNoise(obs, freqs, 3, noise)) + { + traceSlipEvent(trace, "PDE", obs, "skipped", frq1, frq2, frq3, 0, 0, 0, noise.reason); + return; + } + + double sigmaCode = noise.sigmaCode; + double sigmaPhase = noise.sigmaPhase; double mwNoise12 = mwnoise(sigmaCode, sigmaPhase, lam1, lam2); double mwNoise15 = mwnoise(sigmaCode, sigmaPhase, lam1, lam5); @@ -790,7 +1280,20 @@ void cycleslip3( if (satStat.el >= recOpts.elevation_mask_deg * D2R) { - scdia(trace, satStat, lc, lam, sigmaPhase, sigmaCode, 3, sys, E_FilterMode::LSQ); + scdia( + trace, + satStat, + lc, + obs, + lam, + sigmaPhase, + sigmaCode, + 3, + E_FilterMode::LSQ, + frq1, + frq2, + frq3 + ); } /* update TD ionosphere residual */ @@ -829,13 +1332,26 @@ void detectslip( E_FType frq1; E_FType frq2; E_FType frq3; - bool pass = satFreqs(obs.Sat.sys, frq1, frq2, frq3); - if (pass == false) + int nf = obsFreqs(obs, frq1, frq2, frq3); + if (nf < 2) { + traceSlipEvent( + trace, + "PDE", + obs, + "skipped", + NONE, + NONE, + NONE, + 0, + 0, + 0, + E_SlipDiagReason::NO_FREQUENCIES + ); return; } - /* first epoch or large gap or low elevation */ // todo aaron initialisation stuff, remove + /* first epoch or large gap or low elevation */ // todo? initialisation stuff, remove if (satStat.lc_pre.time.bigTime == 0 || satStat.el < recOpts.elevation_mask_deg * D2R || lc_new.time > lc_old.time + PDEGAP) { @@ -870,16 +1386,52 @@ void detectslip( satStat.el * R2D ); + E_SlipDiagReason reason = E_SlipDiagReason::FIRST_EPOCH; + if (lc_new.time > lc_old.time + PDEGAP) + { + reason = E_SlipDiagReason::TIME_GAP; + } + else if (satStat.el < recOpts.elevation_mask_deg * D2R) + { + reason = E_SlipDiagReason::LOW_ELEVATION; + } + traceSlipEvent( + trace, + "PDE", + obs, + "initialised", + frq1, + frq2, + frq3, + satStat.el * R2D, + recOpts.elevation_mask_deg, + PDEGAP, + reason + ); + return; } - if (lc_new.L_m[frq1] != 0 && lc_new.L_m[frq2] != 0 && lc_new.L_m[frq3] == 0) + if (nf == 2 && lc_new.L_m[frq1] != 0 && lc_new.L_m[frq2] != 0) { dualFreq = true; } if (dualFreq && lc_old.L_m[frq1] != 0 && lc_old.L_m[frq2] != 0) { + traceSlipEvent( + trace, + "PDE", + obs, + "evaluating", + frq1, + frq2, + NONE, + 2, + 0, + 0, + E_SlipDiagReason::DUAL_FREQUENCY + ); cycleslip2(trace, satStat, lc_new, obs); /* update averaged MW noise when no cycle slip */ @@ -895,13 +1447,27 @@ void detectslip( } } /* track L5 again */ - else if (lc_new.L_m[frq1] != 0 && lc_new.L_m[frq2] != 0 && lc_new.L_m[frq3] != 0 && - lc_old.L_m[frq1] != 0 && lc_old.L_m[frq2] != 0 && - lc_old.L_m[frq3] == 0) // was zero, now not. + else if ( + nf >= 3 && lc_new.L_m[frq1] != 0 && lc_new.L_m[frq2] != 0 && lc_new.L_m[frq3] != 0 && + lc_old.L_m[frq1] != 0 && lc_old.L_m[frq2] != 0 && lc_old.L_m[frq3] == 0 + ) // was zero, now not. { /* set slip flag for L5 (introduce new ambiguity for L5) */ satStat.sigStatMap[ft2string(frq3)].slip.retrack = true; satStat.sigStatMap[ft2string(frq3)].savedSlip.retrack = true; + traceSlipEvent( + trace, + "PDE", + obs, + "retracking", + frq1, + frq2, + frq3, + 3, + 2, + 0, + E_SlipDiagReason::THIRD_FREQUENCY_REACQUIRED + ); cycleslip2(trace, satStat, lc_new, obs); /* update averaged MW noise when no cycle slip */ @@ -917,14 +1483,29 @@ void detectslip( } } /* Triple-frequency */ - else if (lc_new.L_m[frq1] != 0 && lc_new.L_m[frq2] != 0 && lc_new.L_m[frq3] != 0 && - lc_old.L_m[frq1] != 0 && lc_old.L_m[frq2] != 0 && lc_old.L_m[frq3] != 0) + else if ( + nf >= 3 && lc_new.L_m[frq1] != 0 && lc_new.L_m[frq2] != 0 && lc_new.L_m[frq3] != 0 && + lc_old.L_m[frq1] != 0 && lc_old.L_m[frq2] != 0 && lc_old.L_m[frq3] != 0 + ) { + traceSlipEvent( + trace, + "PDE", + obs, + "evaluating", + frq1, + frq2, + frq3, + 3, + 0, + 0, + E_SlipDiagReason::TRIPLE_FREQUENCY + ); cycleslip3(trace, satStat, lc_new, obs); if (satStat.el * R2D > 30) { - if (satStat.sigStatMap[ft2string(frq1)].slip.any == 2 // todo aaron, check the 2 + if (satStat.sigStatMap[ft2string(frq1)].slip.any == 2 // todo? check the 2 && satStat.amb[0] == 0 && satStat.amb[1] == 0 && satStat.amb[2] == 0) { satStat.sigStatMap[ft2string(frq1)].slip.any = 0; @@ -965,6 +1546,19 @@ void detectslip( id, satStat.el * R2D ); + traceSlipEvent( + trace, + "PDE", + obs, + "retracking", + frq1, + frq2, + NONE, + 2, + 0, + 0, + E_SlipDiagReason::FREQUENCY_REACQUIRED + ); } else { @@ -984,6 +1578,19 @@ void detectslip( id, satStat.el * R2D ); + traceSlipEvent( + trace, + "PDE", + obs, + "flagged", + frq1, + frq2, + frq3, + 1, + 0, + 0, + E_SlipDiagReason::SINGLE_FREQUENCY + ); } } @@ -1008,7 +1615,7 @@ void clearSlips(ObsList& obsList) { SatStat& satStat = *(obs.satStat_ptr); - satStat.slip = false; // todo aaron, is this used? + satStat.slip = false; // todo? is this used? sigStat.slip.any = 0; } } diff --git a/src/cpp/common/algebra.cpp b/src/cpp/common/algebra.cpp index 11b49d68c..59172f508 100644 --- a/src/cpp/common/algebra.cpp +++ b/src/cpp/common/algebra.cpp @@ -1,15 +1,17 @@ #include "common/algebra.hpp" +#include #include #include #include #include +#include #include "architectureDocs.hpp" #include "common/acsConfig.hpp" #include "common/algebraTrace.hpp" +#include "common/blasThreading.hpp" #include "common/common.hpp" #include "common/constants.hpp" #include "common/eigenIncluder.hpp" -#include "common/kalmanBlas.hpp" #include "common/lapackWrapper.hpp" #include "common/mongo.hpp" #include "common/mongoWrite.hpp" @@ -910,9 +912,10 @@ void KFState::stateTransition( (+2 * tgap // one tau from front tau3 distributed to prevent // divide by zero - 4 * tau * (1 - exp(-1 * tgap / tau)) + - 1 * tau * (1 - exp(-2 * tgap / tau)) - ); // correct formula re-derived according - // to Ref: Carpenter and Lee (2008) + 1 * tau * + (1 - exp(-2 * tgap / + tau))); // correct formula re-derived according + // to Ref: Carpenter and Lee (2008) // Q0(sourceIndex, destIndex) += // sourceProcessNoise / 2 // * tau * tau * (1-exp(-tgap/tau)) * (1-exp(-tgap/tau)); @@ -972,6 +975,131 @@ void KFState::stateTransition( { Q0.setZero(); } + bool blockCovarianceTransition = acsConfig.pppOpts.receiver_chunking; + + struct TransitionChunk + { + string id; + vector rows; + vector cols; + }; + + vector transitionChunks; + map transitionChunkIndex; + + if (blockCovarianceTransition) + { + auto addUnique = [](vector& list, int value) + { + if (std::find(list.begin(), list.end(), value) == list.end()) + { + list.push_back(value); + } + }; + + for (auto& [dest, sourceMap] : stateTransitionMap) + { + if (dest == oneKey) + { + continue; + } + + if (dest.str.empty()) + { + blockCovarianceTransition = false; + break; + } + + auto destIter = newKFIndexMap.find(dest); + if (destIter == newKFIndexMap.end()) + { + blockCovarianceTransition = false; + break; + } + + auto& chunkId = dest.str; + + auto [chunkIndexIt, inserted] = + transitionChunkIndex.try_emplace(chunkId, transitionChunks.size()); + if (inserted) + { + transitionChunks.push_back({chunkId, {}, {}}); + } + + auto& chunk = transitionChunks[chunkIndexIt->second]; + chunk.rows.push_back(destIter->second); + + for (auto& [source, values] : sourceMap) + { + if (source == oneKey) + { + continue; + } + + if (source.str.empty() || source.str != chunkId) + { + blockCovarianceTransition = false; + break; + } + + int sourceIndex = getKFIndex(source); + if ((sourceIndex < 0) || (sourceIndex >= F.cols())) + { + continue; + } + + addUnique(chunk.cols, sourceIndex); + } + + if (blockCovarianceTransition == false) + { + break; + } + } + + for (auto& chunk : transitionChunks) + { + std::sort(chunk.cols.begin(), chunk.cols.end()); + } + } + + if (blockCovarianceTransition) + { + MatrixXd Pnew = MatrixXd::Zero(newStateCount, newStateCount); + + bool parallelChunks = transitionChunks.size() > 1; + BlasThreading::ScopedOpenBlasThreadLimit openblasThreadLimit(parallelChunks ? 1 : 0); +#ifdef ENABLE_PARALLELISATION +#pragma omp parallel for schedule(dynamic) if (parallelChunks) +#endif + for (int c = 0; c < (int)transitionChunks.size(); c++) + { + auto& chunk = transitionChunks[c]; + auto& rows = chunk.rows; + auto& cols = chunk.cols; + + if (cols.empty()) + { + Pnew(rows, rows) = Q0(rows, rows); + continue; + } + + MatrixXd Fblock = MatrixXd::Zero(rows.size(), cols.size()); + for (int i = 0; i < rows.size(); i++) + { + for (int j = 0; j < cols.size(); j++) + { + Fblock(i, j) = F.coeff(rows[i], cols[j]); + } + } + + Pnew(rows, rows) = + (Fblock * P(cols, cols) * Fblock.transpose() + Q0(rows, rows)).eval(); + } + + P = std::move(Pnew); + } + else { P = (F * P * F.transpose() + Q0).eval(); } @@ -1068,8 +1196,8 @@ void KFState::leastSquareSigmaChecks( if (maxMeasRatio > lsqOpts.meas_sigma_threshold) { trace << "\n" - << time << "\tLARGE MEAS ERROR OF : " << maxMeasRatio << "\tAT " << measIndex - << " :\t" << kfMeas.obsKeys[measIndex]; + << time << "\tLARGE MEAS ERROR OF : " << std::setw(11) << maxMeasRatio << "\tAT " + << std::setw(4) << measIndex << " :\t" << kfMeas.obsKeys[measIndex]; callbackDetails.measIndex = measIndex; } @@ -1159,11 +1287,11 @@ void KFState::preFitSigmaChecks( maxStateRatio > prefitOpts.state_sigma_threshold) { trace << "\n" - << time << "\tLARGE STATE ERROR OF : " << maxStateRatio << "\tAT " - << stateChunkIndex << " :\t" << stateKey; + << time << "\tLARGE STATE ERROR OF : " << std::setw(11) << maxStateRatio << "\tAT " + << std::setw(4) << stateChunkIndex << " :\t" << stateKey; trace << "\n" - << time << "\tLargest meas error is : " << maxMeasRatio << "\tAT " << measChunkIndex - << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; + << time << "\tLargest meas error is : " << std::setw(11) << maxMeasRatio << "\tAT " + << std::setw(4) << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; measRatios = (H.col(stateIndex).array() != 0) @@ -1171,9 +1299,8 @@ void KFState::preFitSigmaChecks( maxMeasRatio = measRatios.abs().maxCoeff(&measIndex); measChunkIndex = measIndex + begH; - trace << "\n" - << time << "\tLargest ref meas error is: " << maxMeasRatio << "\tAT " - << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; + trace << time << "\tLargest ref meas error: " << std::setw(11) << maxMeasRatio << "\tAT " + << std::setw(4) << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; callbackDetails.kfKey = stateKey; callbackDetails.stateIndex = stateChunkIndex; @@ -1183,18 +1310,17 @@ void KFState::preFitSigmaChecks( double minMeasRatio = measRatios.abs().minCoeff(&measIndex); measChunkIndex = measIndex + begH; - trace << "\n" - << time << "\tSmallest ref meas error is: " << minMeasRatio << "\tAT " - << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; + trace << time << "\tSmallest ref meas error: " << std::setw(11) << minMeasRatio << "\tAT " + << std::setw(4) << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; } else if (maxMeasRatio > prefitOpts.meas_sigma_threshold) { trace << "\n" - << time << "\tLARGE MEAS ERROR OF : " << maxMeasRatio << "\tAT " << measChunkIndex - << " :\t" << kfMeas.obsKeys[measChunkIndex]; + << time << "\tLARGE MEAS ERROR OF : " << std::setw(11) << maxMeasRatio << "\tAT " + << std::setw(4) << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex]; trace << "\n" - << time << "\tLargest state error is : " << maxStateRatio << "\tAT " - << stateChunkIndex << " :\t" << stateKey << "\n"; + << time << "\tLargest state error is : " << std::setw(11) << maxStateRatio << "\tAT " + << std::setw(4) << stateChunkIndex << " :\t" << stateKey << "\n"; callbackDetails.measIndex = measChunkIndex; } @@ -1390,11 +1516,11 @@ void KFState::postFitSigmaChecks( maxStateRatio > postfitOpts.state_sigma_threshold) { trace << "\n" - << time << "\tLARGE STATE ERROR OF : " << maxStateRatio << "\tAT " - << stateChunkIndex << " :\t" << stateKey; + << time << "\tLARGE STATE ERROR OF : " << std::setw(11) << maxStateRatio << "\tAT " + << std::setw(4) << stateChunkIndex << " :\t" << stateKey; trace << "\n" - << time << "\tLargest meas error is : " << maxMeasRatio << "\tAT " << measChunkIndex - << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; + << time << "\tLargest meas error is : " << std::setw(11) << maxMeasRatio << "\tAT " + << std::setw(4) << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; measRatios = (H.col(stateIndex).array() != 0) @@ -1402,9 +1528,8 @@ void KFState::postFitSigmaChecks( maxMeasRatio = measRatios.abs().maxCoeff(&measIndex); measChunkIndex = measIndex + begH; - trace << "\n" - << time << "\tLargest ref meas error is: " << maxMeasRatio << "\tAT " - << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; + trace << time << "\tLargest ref meas error: " << std::setw(11) << maxMeasRatio << "\tAT " + << std::setw(4) << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; callbackDetails.kfKey = stateKey; callbackDetails.stateIndex = stateChunkIndex; @@ -1414,18 +1539,17 @@ void KFState::postFitSigmaChecks( double minMeasRatio = measRatios.abs().minCoeff(&measIndex); measChunkIndex = measIndex + begH; - trace << "\n" - << time << "\tSmallest ref meas error is: " << minMeasRatio << "\tAT " - << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; + trace << time << "\tSmallest ref meas error: " << std::setw(11) << minMeasRatio << "\tAT " + << std::setw(4) << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex] << "\n"; } else if (maxMeasRatio > postfitOpts.meas_sigma_threshold) { trace << "\n" - << time << "\tLARGE MEAS ERROR OF : " << maxMeasRatio << "\tAT " << measChunkIndex - << " :\t" << kfMeas.obsKeys[measChunkIndex]; + << time << "\tLARGE MEAS ERROR OF : " << std::setw(11) << maxMeasRatio << "\tAT " + << std::setw(4) << measChunkIndex << " :\t" << kfMeas.obsKeys[measChunkIndex]; trace << "\n" - << time << "\tLargest state error is : " << maxStateRatio << "\tAT " - << stateChunkIndex << " :\t" << stateKey << "\n"; + << time << "\tLargest state error is : " << std::setw(11) << maxStateRatio << "\tAT " + << std::setw(4) << stateChunkIndex << " :\t" << stateKey << "\n"; callbackDetails.measIndex = measChunkIndex; } @@ -1537,7 +1661,8 @@ bool KFState::kFilter( int begX, ///< Index of first state element to process int numX, ///< Number of state elements to process int begH, ///< Index of first measurement to process - int numH ///< Number of measurements to process + int numH, ///< Number of measurements to process + bool resetOnFailure ) { auto& R = kfMeas.R; @@ -1545,7 +1670,7 @@ bool KFState::kFilter( auto& H = kfMeas.H; auto& H_star = kfMeas.H_star; - auto noise = kfMeas.uncorrelatedNoise.asDiagonal(); // todo Eugene: check chunking indices + auto noise = kfMeas.uncorrelatedNoise.asDiagonal(); // todo? check chunking indices // Get pointers to block data (no copying!) const double* H_ptr = H.data() + begH + begX * H.rows(); // H block starting point @@ -1677,15 +1802,24 @@ bool KFState::kFilter( << "dgetrf (general LU factorization) failed with info = " << info << " - all factorization methods exhausted"; - xp = x; - Pp = P; - dx = VectorXd::Zero(xp.rows()); + if (resetOnFailure) + { + xp = x; + Pp = P; + dx = VectorXd::Zero(xp.rows()); + } - trace << "\n" << "Kalman Filter Error - Matrix Factorization Failed"; - trace << "\n" << "Q: " << "\n" << Q; - trace << "\n" << "H block size: " << numH << "x" << numX; - trace << "\n" << "R block size: " << numH << "x" << numH; - trace << "\n" << "P block size: " << numX << "x" << numX; + trace << "\n" + << "Kalman Filter Error - Matrix Factorization Failed"; + trace << "\n" + << "Q: " << "\n" + << Q; + trace << "\n" + << "H block size: " << numH << "x" << numX; + trace << "\n" + << "R block size: " << numH << "x" << numH; + trace << "\n" + << "P block size: " << numX << "x" << numX; return false; } @@ -1746,9 +1880,12 @@ bool KFState::kFilter( { BOOST_LOG_TRIVIAL(error) << "Solve failed for Kalman gain with info = " << info; - xp = x; - Pp = P; - dx = VectorXd::Zero(xp.rows()); + if (resetOnFailure) + { + xp = x; + Pp = P; + dx = VectorXd::Zero(xp.rows()); + } return false; } @@ -2785,6 +2922,9 @@ void KFState::filterKalman( statisticsMap["States"] = x.rows(); BOOST_LOG_TRIVIAL(info) << " ------- FILTERING BY CHUNK " << filterChunkMap.size() << " --------\n"; + + vector activeChunks; + activeChunks.reserve(filterChunkMap.size()); for (auto& [id, fc] : filterChunkMap) { if (fc.numH == 0) @@ -2792,6 +2932,55 @@ void KFState::filterKalman( continue; } + activeChunks.push_back(&fc); + } + + struct ChunkFilterResult + { + bool computed = false; + bool pass = false; + MatrixXd Qinv; + MatrixXd QinvH; + }; + + vector chunkResults(activeChunks.size()); + + if (postfitOpts.max_iterations > 0) + { + bool parallelChunks = activeChunks.size() > 1; + BlasThreading::ScopedOpenBlasThreadLimit openblasThreadLimit(parallelChunks ? 1 : 0); +#ifdef ENABLE_PARALLELISATION +#pragma omp parallel for schedule(dynamic) if (parallelChunks) +#endif + for (int c = 0; c < (int)activeChunks.size(); c++) + { + auto& fc = *activeChunks[c]; + auto& result = chunkResults[c]; + + result.Qinv = MatrixXd::Identity(fc.numH, fc.numH); + result.QinvH = MatrixXd::Ones(fc.numH, fc.numX); + result.pass = kFilter( + nullStream, + kfMeas, + xp, + Pp, + dx, + result.Qinv, + result.QinvH, + fc.begX, + fc.numX, + fc.begH, + fc.numH, + false + ); + result.computed = true; + } + } + + for (int c = 0; c < activeChunks.size(); c++) + { + auto& fc = *activeChunks[c]; + if (fc.id.empty() == false) { BOOST_LOG_TRIVIAL(info) @@ -2806,8 +2995,29 @@ void KFState::filterKalman( KFStatistics statistics; for (int i = 0; i < postfitOpts.max_iterations; i++) { - bool pass = - kFilter(trace, kfMeas, xp, Pp, dx, Qinv, QinvH, fc.begX, fc.numX, fc.begH, fc.numH); + bool pass = false; + if (i == 0 && chunkResults[c].computed) + { + pass = chunkResults[c].pass; + Qinv = std::move(chunkResults[c].Qinv); + QinvH = std::move(chunkResults[c].QinvH); + } + else + { + pass = kFilter( + trace, + kfMeas, + xp, + Pp, + dx, + Qinv, + QinvH, + fc.begX, + fc.numX, + fc.begH, + fc.numH + ); + } if (pass == false) { @@ -2824,6 +3034,20 @@ void KFState::filterKalman( dx.segment(fc.begX, fc.numX); } + if (output_residuals) + { + outputResiduals(trace, kfMeas, suffix, i, fc.begH, fc.numH); + } + + if (traceLevel >= 5) + { + KFState kfStateCopy = *this; + kfStateCopy.x = xp; + kfStateCopy.P = Pp; + + kfStateCopy.outputStates(trace, suffix, i, fc.begX, fc.numX); + } + bool stopIterating = true; if (postfitOpts.sigma_check || postfitOpts.omega_test) @@ -2886,20 +3110,6 @@ void KFState::filterKalman( *fc.trace_ptr << stringBuffer.str(); } - if (output_residuals) - { - outputResiduals(trace, kfMeas, suffix, i, fc.begH, fc.numH); - } - - if (traceLevel >= 5) - { - KFState kfStateCopy = *this; - kfStateCopy.x = xp; - kfStateCopy.P = Pp; - - kfStateCopy.outputStates(trace, suffix, i, fc.begH, fc.numH); - } - if (stopIterating) { statisticsMap["Filter iterations " + std::to_string(i + 1)]++; @@ -2940,9 +3150,10 @@ void KFState::filterKalman( auto& chunkTrace = *fc.trace_ptr; - switch (chiSquareTest.mode - ) // todo Eugene: rethink Chi-Square test modes, consider keep only INNOVATION - // and determine DOF automatically based on process noises + switch ( + chiSquareTest + .mode) // todo? rethink Chi-Square test modes, consider keep only + // INNOVATION and determine DOF automatically based on process noises { case E_ChiSqMode::INNOVATION: { @@ -2974,7 +3185,7 @@ void KFState::filterKalman( testStatistics.dof = x.rows() - 1; else testStatistics.dof = - kfMeas.H.rows(); // todo Eugene: revisit DOF in the future for MEASUREMENT mode + kfMeas.H.rows(); // todo? revisit DOF in the future for MEASUREMENT mode testStatistics.chiSqPerDof = testStatistics.chiSq / testStatistics.dof; @@ -3142,7 +3353,7 @@ bool KFState::leastSquareInitStates( leastSquareMeas.V(measIndex) = x(stateIndex); // take 0 as apriori state } leastSquareMeas.R(measIndex, measIndex) = - P(stateIndex, stateIndex); // todo Eugene: check equivalence w/ back-subsitution - + P(stateIndex, stateIndex); // todo? check equivalence w/ back-subsitution - // pseudo var should be 0 instead of P, or doesn't matter? leastSquareMeas.H(measIndex, stateIndex) = 1; } @@ -3191,6 +3402,11 @@ bool KFState::leastSquareInitStates( leastSquareMeasSubs.VV = leastSquareMeasSubs.V - leastSquareMeasSubs.H * xp; + if (output_residuals && traceLevel >= 5) + { + outputResiduals(trace, leastSquareMeasSubs, suffix, i, 0, leastSquareMeasSubs.H.rows()); + } + if (chiSquareTest.enable) { chiQC(trace, leastSquareMeasSubs); @@ -3249,11 +3465,6 @@ bool KFState::leastSquareInitStates( trace << stringBuffer.str(); } - if (output_residuals && traceLevel >= 5) - { - outputResiduals(trace, leastSquareMeasSubs, suffix, i, 0, leastSquareMeasSubs.H.rows()); - } - if (stopIterating) { // statisticsMap["Least squares iterations " + std::to_string(i+1)]++; diff --git a/src/cpp/common/algebra.hpp b/src/cpp/common/algebra.hpp index b21a1b0d9..b32fe5091 100644 --- a/src/cpp/common/algebra.hpp +++ b/src/cpp/common/algebra.hpp @@ -513,9 +513,9 @@ struct KFState : KFState_ KFState() { // initialise all filter state objects with a ONE element for later use. - x = VectorXd ::Ones(1); - P = MatrixXd ::Zero(1, 1); - dx = VectorXd ::Zero(1); + x = VectorXd::Ones(1); + P = MatrixXd::Zero(1, 1); + dx = VectorXd::Zero(1); kfIndexMap[oneKey] = 0; @@ -688,10 +688,11 @@ struct KFState : KFState_ VectorXd& dx, MatrixXd& Qinv, MatrixXd& QinvH, - int begX = 0, - int numX = -1, - int begH = 0, - int numH = -1 + int begX = 0, + int numX = -1, + int begH = 0, + int numH = -1, + bool resetOnFailure = true ); bool leastSquare(Trace& trace, KFMeas& kfMeas, VectorXd& xp, MatrixXd& Pp); @@ -901,15 +902,15 @@ double dot(const double* a, const double* b, int n); double norm(const double* a, int n); void matcpy(double* A, const double* B, int n, int m); void matmul( - const char* tr, - int n, - int k, - int m, - double alpha, - const double* A, - const double* B, - double beta, - double* C - ); + const char* tr, + int n, + int k, + int m, + double alpha, + const double* A, + const double* B, + double beta, + double* C +); int matinv(double* A, int n); int solve(const char* tr, const double* A, const double* Y, int n, int m, double* X); diff --git a/src/cpp/common/algebraTrace.cpp b/src/cpp/common/algebraTrace.cpp index 8b1328b3e..caf9662ea 100644 --- a/src/cpp/common/algebraTrace.cpp +++ b/src/cpp/common/algebraTrace.cpp @@ -39,8 +39,8 @@ Architecture Binary_Archive__() {} /** Returns the type of object that is located at the specified position in a file */ E_SerialObject getFilterTypeFromFile( - long int& startPos, ///< Position of object - string filename ///< Path to archive file + std::streamoff& startPos, ///< Position of object + string filename ///< Path to archive file ) { std::fstream fileStream(filename, std::ios::binary | std::ios::in); diff --git a/src/cpp/common/algebraTrace.hpp b/src/cpp/common/algebraTrace.hpp index 225b54e18..17b992bd9 100644 --- a/src/cpp/common/algebraTrace.hpp +++ b/src/cpp/common/algebraTrace.hpp @@ -159,14 +159,14 @@ void spitFilterToFile( // On Windows/MinGW, tellp() returns 0 in append mode, so seek to end first fileStream.seekp(0, std::ios::end); - long int pos = fileStream.tellp(); + std::streamoff pos = fileStream.tellp(); int type_int = static_cast(type); serial & type_int; serial & object; - long int end = fileStream.tellp(); - long int delta = end - pos; + std::streamoff end = fileStream.tellp(); + long int delta = static_cast(end - pos); serial & delta; } catch (...) @@ -182,8 +182,8 @@ bool getFilterObjectFromFile( E_SerialObject expectedType, ///< The expected type of object, (determine using ///< getFilterTypeFromFile() first) TYPE& object, ///< The pre-declared object to set the value of - long int& startPos, ///< The position in the file of the object's record - string filename ///< The path to the archive file to read from + std::streamoff& startPos, ///< The position in the file of the object's record + string filename ///< The path to the archive file to read from ) { std::fstream fileStream(filename, std::ios::binary | std::ios::in); @@ -241,7 +241,7 @@ bool getFilterObjectFromFile( return true; } -E_SerialObject getFilterTypeFromFile(long int& startPos, string filename); +E_SerialObject getFilterTypeFromFile(std::streamoff& startPos, string filename); void tryPrepareFilterPointers(KFState& kfState, ReceiverMap& receiverMap); diff --git a/src/cpp/common/attitude.cpp b/src/cpp/common/attitude.cpp index 49e0a8808..d651f452f 100644 --- a/src/cpp/common/attitude.cpp +++ b/src/cpp/common/attitude.cpp @@ -618,6 +618,9 @@ bool satYawGpsIIF( return satYawGpsIIR(Sat, attStatus, time, satGeom, -0.7 * D2R); // Midnight turning - Shadow constant yaw steering + double maxYawRate = 0; + bool maxYawRateFound = getSnxSatMaxYawRate(Sat.svn(), time, maxYawRate); + if (startTime == GTime::noTime()) // Start of eclipse { startTime = findEclipseBoundaries(time, satGeom, false); @@ -629,7 +632,9 @@ bool satYawGpsIIF( double dYaw = endYaw - startYaw; wrapPlusMinusPi(dYaw); startYawRate = abs(dYaw) / (endTime - startTime).to_double(); - startSign = SGN(dYaw); + if (maxYawRateFound) + startYawRate = std::min(startYawRate, maxYawRate); + startSign = SGN(dYaw); } attStatus.modelYaw = startYaw + startSign * startYawRate * (time - startTime).to_double(); wrapPlusMinusPi(attStatus.modelYaw); @@ -779,8 +784,8 @@ bool satYawGalFoc( GTime currTime = time; double currMu = mu; double dt = -1; - while (colinearAngle(currMu) < colAngThresh - ) // Ignore beta when finding start of modified-steering period + while (colinearAngle(currMu) < + colAngThresh) // Ignore beta when finding start of modified-steering period { currTime += dt; currMu = mu + muRate * (currTime - time).to_double(); @@ -877,6 +882,7 @@ bool satYawGlo( << "Max yaw rate not found for " << Sat.svn() << " in " << __FUNCTION__ << ", check sinex files for '+SATELLITE/YAW_BIAS_RATE' block"; + modelYaw = nominalYaw; return false; } @@ -1360,15 +1366,15 @@ void updateSatYaw( modelYawValid = satYawBds2(Sat, attStatus, time, satGeom, 5740); break; } - case E_Block::BDS_3SI_SECM: // Unmodelled - case E_Block::BDS_3SM_CAST: // Unmodelled - case E_Block::BDS_3SI_CAST: // Unmodelled - case E_Block::BDS_3SM_SECM: // Unmodelled + case E_Block::BDS_3SI_SECM: // Unmodelled - use BDS-3I/IGSO model + case E_Block::BDS_3SI_CAST: // Unmodelled - use BDS-3I/IGSO model case E_Block::BDS_3I: { modelYawValid = satYawBds3(attStatus, time, satGeom, 5740); break; } + case E_Block::BDS_3SM_CAST: // Unmodelled - use BDS-3M/MEO model + case E_Block::BDS_3SM_SECM: // Unmodelled - use BDS-3M/MEO model case E_Block::BDS_3M_CAST: { modelYawValid = satYawBds3(attStatus, time, satGeom, 3090); @@ -1414,7 +1420,9 @@ void updateSatYaw( satYawGpsIIR(Sat, attStatus, time, satGeom); BOOST_LOG_TRIVIAL(warning) << "Attitude model not implemented for " << Sat.blockType() << " in " - << __FUNCTION__ << "; using GPS-IIR model instead."; + << __FUNCTION__ << "; using GPS-IIR model instead. Check satellite metadata " + << "SINEX block types, for example that inputs.snx_files includes " + << "igs_satellite_metadata.snx and that it contains this satellite."; } } @@ -1817,7 +1825,7 @@ void recAtt( updateAntAtt(rec.antBoresight, rec.antAzimuth, attStatus); - // SatSys Sat(rec.id.c_str()); //todo aaron, this should be the recSatId thing instead + // SatSys Sat(rec.id.c_str()); //todo? this should be the recSatId thing instead // if (Sat.prn) // { // nav.satNavMap[Sat].attStatus = attStatus; diff --git a/src/cpp/common/attitude.hpp b/src/cpp/common/attitude.hpp index 73f409e04..9ffd68a69 100644 --- a/src/cpp/common/attitude.hpp +++ b/src/cpp/common/attitude.hpp @@ -9,11 +9,11 @@ */ struct AttStatus { - GTime startTime = GTime::noTime( - ); ///< Time of switchover to modified yaw steering (due to noon/midnight turn) - double startSign = 0; ///< Sign of yaw rate at switchover - double startYaw = 0; ///< Yaw at switchover - double startYawRate = 0; ///< Yaw rate at switchover + GTime startTime = GTime::noTime(); ///< Time of switchover to modified yaw steering (due to + ///< noon/midnight turn) + double startSign = 0; ///< Sign of yaw rate at switchover + double startYaw = 0; ///< Yaw at switchover + double startYawRate = 0; ///< Yaw rate at switchover GTime excludeTime = GTime::noTime(); ///< Time to skip yaw modelling until, due to unknown yaw behaviour diff --git a/src/cpp/common/biasSINEXread.cpp b/src/cpp/common/biasSINEXread.cpp index ff4910746..a39530b14 100644 --- a/src/cpp/common/biasSINEXread.cpp +++ b/src/cpp/common/biasSINEXread.cpp @@ -123,7 +123,7 @@ bool read_biasSINEX_line( } else if (sat != " ") { - // this should be a satellite, but check its valid //todo aaron, system for receiver + // this should be a satellite, but check its valid //todo? system for receiver // dcbs if (Sat.prn == 0 || Sat.sys == E_Sys::NONE) diff --git a/src/cpp/common/biases.cpp b/src/cpp/common/biases.cpp index b4d5da733..ed4bbf30f 100644 --- a/src/cpp/common/biases.cpp +++ b/src/cpp/common/biases.cpp @@ -76,7 +76,7 @@ void addDefaultBias() // entry.cod2 = acsConfig.clock_codesL2[bias.Sat.sys]; entry.source = "def1"; - // pushBiasEntry(id, entry); //todo aaron, disabled + // pushBiasEntry(id, entry); //todo? disabled } for (auto& Sat : getSysSats(E_Sys::GPS)) @@ -92,7 +92,7 @@ void addDefaultBias() } } -void loadStateBiases( // todo aaron this probably needs to be called to write biases from filter to +void loadStateBiases( // todo? this probably needs to be called to write biases from filter to // files KFState& kfState ) @@ -311,7 +311,7 @@ bool decomposeBGDBias( pushBiasEntry( id, entry - ); // todo aaron, check which of these match the clock_codes and only create those. + ); // todo? check which of these match the clock_codes and only create those. // covert BGD E5a/E1 to C5Q-IF OSB entry.cod1 = cod2; diff --git a/src/cpp/common/blasThreading.hpp b/src/cpp/common/blasThreading.hpp new file mode 100644 index 000000000..2e7a9e6a2 --- /dev/null +++ b/src/cpp/common/blasThreading.hpp @@ -0,0 +1,78 @@ +#pragma once + +namespace BlasThreading +{ +#ifdef GINAN_USE_OPENBLAS +extern "C" +{ + int openblas_get_num_threads(); + void openblas_set_num_threads(int num_threads); +} +#endif + +inline bool openblasThreadControlAvailable() +{ +#ifdef GINAN_USE_OPENBLAS + return true; +#else + return false; +#endif +} + +/** + * Temporarily limits OpenBLAS worker threads. + * + * OpenBLAS builds that use pthreads warn, and can hang, if they are called from + * an active OpenMP parallel region: + * + * OpenBLAS Warning : Detect OpenMP Loop and this application may hang. + * + * Ginan keeps some high-level chunk loops parallel with OpenMP. For those loops + * we want each chunk's BLAS/LAPACK calls to run single-threaded instead of + * nesting another thread pool inside every OpenMP worker. This RAII guard sets + * OpenBLAS to the requested thread count for the current scope and restores the + * previous OpenBLAS setting when it leaves the scope. + * + * Passing numThreads <= 0 disables the guard. If the target is linked against + * a non-OpenBLAS BLAS/LAPACK library, the guard is compiled as a no-op. + */ +class ScopedOpenBlasThreadLimit +{ +public: + explicit ScopedOpenBlasThreadLimit(int numThreads) + { +#ifdef GINAN_USE_OPENBLAS + if (numThreads <= 0 || openblasThreadControlAvailable() == false) + { + return; + } + + previousNumThreads = openblas_get_num_threads(); + if (previousNumThreads != numThreads) + { + openblas_set_num_threads(numThreads); + changed = true; + } +#endif + } + + ~ScopedOpenBlasThreadLimit() + { +#ifdef GINAN_USE_OPENBLAS + if (changed && previousNumThreads > 0 && openblasThreadControlAvailable()) + { + openblas_set_num_threads(previousNumThreads); + } +#endif + } + + ScopedOpenBlasThreadLimit(const ScopedOpenBlasThreadLimit&) = delete; + ScopedOpenBlasThreadLimit& operator=(const ScopedOpenBlasThreadLimit&) = delete; + +#ifdef GINAN_USE_OPENBLAS +private: + int previousNumThreads = 0; + bool changed = false; +#endif +}; +} // namespace BlasThreading diff --git a/src/cpp/common/cost.cpp b/src/cpp/common/cost.cpp index fd5e45f3b..8ddcf7ea7 100644 --- a/src/cpp/common/cost.cpp +++ b/src/cpp/common/cost.cpp @@ -88,13 +88,19 @@ void outputCost( locationName.c_str() ); // Site name with country + string receiverType = + rec.metadata.receiverType.valid ? rec.metadata.receiverType.value : rec.receiverType; + string antennaType = rec.metadata.antennaDescriptor.valid + ? rec.metadata.antennaDescriptor.value + : rec.antennaType; + tracepdeex( 0, fout, "%-20s %-20s\n", - rec.receiverType.c_str(), // Receiver type - rec.antennaType.c_str() - ); // Antenna type + receiverType.c_str(), // Receiver type + antennaType.c_str() + ); // Antenna type } if (firstWrite) @@ -114,7 +120,9 @@ void outputCost( recPosEcef = rec.aprioriPos; } - VectorEcef eccEcef = body2ecef(rec.attStatus, rec.snx.ecc_ptr->ecc); + VectorEnu eccEnu = + rec.metadata.antennaDelta.valid ? rec.metadata.antennaDelta.value : rec.snx.ecc_ptr->ecc; + VectorEcef eccEcef = body2ecef(rec.attStatus, eccEnu); VectorPos recPos = ecef2pos(recPosEcef + eccEcef); if (recPos[1] < 0) @@ -130,7 +138,7 @@ void outputCost( recPos.lonDeg(), recPos.hgt(), // ARP height above ellipsoid recPos.hgt() - geoidOffset, // ARP height above geoid - rec.snx.ecc_ptr->ecc.u() + eccEnu.u() ); // ARP height above benchmark if (firstWrite) @@ -184,8 +192,8 @@ void outputCost( unsigned otl : 1; ///< Ocean tide loading correction applied unsigned atc : 1; ///< Atmospheric loading correction applied unsigned localMetData : 1; ///< Local surface met. sensor data available - unsigned - centredTime : 1; ///< Timestamps are at the centre of period [false: end of period] + unsigned centredTime + : 1; ///< Timestamps are at the centre of period [false: end of period] unsigned gpsUsed : 1; ///< GPS satellite(s) used unsigned gloUsed : 1; ///< GLONASS satellite(s) used unsigned galUsed : 1; ///< Galileo satellite(s) used diff --git a/src/cpp/common/customDecoder.cpp b/src/cpp/common/customDecoder.cpp index d9a8be06f..b83bca4b1 100644 --- a/src/cpp/common/customDecoder.cpp +++ b/src/cpp/common/customDecoder.cpp @@ -50,7 +50,15 @@ void CustomDecoder::decodeRAWX(vector& payload) obsList.push_back((shared_ptr)obs); } - obsListList.push_back(obsList); + if (obsList.empty() == false) + { + obsListList.push_back(obsList); + } + else + { + BOOST_LOG_TRIVIAL(info) << "Custom decoder produced empty ObsList at epoch flush" + << ", week=" << week << ", rcvTow=" << rcvTow; + } lastTimeTag = 0; lastTime = gpst2time(week, rcvTow); diff --git a/src/cpp/common/debug.cpp b/src/cpp/common/debug.cpp index cfef43ff5..8f3576e32 100644 --- a/src/cpp/common/debug.cpp +++ b/src/cpp/common/debug.cpp @@ -1028,7 +1028,7 @@ void reflector() Vector3d reflected = -frontalArea * (source - 2 * (source.dot(correctFace)) * correctFace) * specularity[i]; - Vector3d emissive = frontalArea * correctFace * (1 - specularity[i]) * 0.7; + Vector3d emissive = frontalArea * correctFace * (1 - specularity[i]) * 0.7; Vector3d outgoing = (1 - absorbtion[i]) * (reflected + emissive); diff --git a/src/cpp/common/enumHelpers.hpp b/src/cpp/common/enumHelpers.hpp index dce010aad..eac9da938 100644 --- a/src/cpp/common/enumHelpers.hpp +++ b/src/cpp/common/enumHelpers.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -17,6 +18,13 @@ inline std::string enum_to_string(E value) return std::string(magic_enum::enum_name(value)); } +// Enum to string conversion (replaces ._to_string()) +template +inline std::string enum_to_lowerstring(E value) +{ + return boost::algorithm::to_lower_copy(std::string(magic_enum::enum_name(value))); +} + // String to enum conversion with default fallback (replaces ::_from_string()) template inline E string_to_enum(const std::string& str, E default_value = E{}) diff --git a/src/cpp/common/enums.h b/src/cpp/common/enums.h index 019ba2953..7a7cacb8d 100644 --- a/src/cpp/common/enums.h +++ b/src/cpp/common/enums.h @@ -51,6 +51,28 @@ typedef enum SVH_UNHEALTHY = -1 // implicitly used in rtcm } E_Svh; +typedef enum +{ + P_ANT, // P: antenna //todo: check the meaning of 'P' + L_LRA // L: laser retroreflector array +} E_EccType; + +typedef enum +{ + ESTIMATE, + APRIORI, + NORMAL_EQN, + MAX_MATRIX_TYPE +} matrix_type; + +typedef enum +{ + CORRELATION, + COVARIANCE, + INFORMATION, + MAX_MATRIX_VALUE +} matrix_value; + /** * Warning: do not change the order, used by RAIM * The larger is the number better the solution is. @@ -799,14 +821,16 @@ enum class E_SbasId : short int enum class RtcmMessageType : uint16_t { NONE = 0, - - GPS_EPHEMERIS = 1, + STATIONARY_RTK_REF_ARP, // 1005 + STATIONARY_RTK_REF_ARP_HEIGHT, // 1006 + ANTENNA_DESCRIPTOR, // 1007 + ANTENNA_DESCRIPTOR_SN, // 1008 (with serial number) + GPS_EPHEMERIS, GLO_EPHEMERIS, BDS_EPHEMERIS, QZS_EPHEMERIS, GAL_FNAV_EPHEMERIS, GAL_INAV_EPHEMERIS, - GPS_SSR_ORB_CORR, GPS_SSR_CLK_CORR, GPS_SSR_CODE_BIAS, @@ -814,7 +838,6 @@ enum class RtcmMessageType : uint16_t GPS_SSR_URA, GPS_SSR_HR_CLK_CORR, GPS_SSR_PHASE_BIAS, - GLO_SSR_ORB_CORR, GLO_SSR_CLK_CORR, GLO_SSR_CODE_BIAS, @@ -822,32 +845,28 @@ enum class RtcmMessageType : uint16_t GLO_SSR_URA, GLO_SSR_HR_CLK_CORR, GLO_SSR_PHASE_BIAS, - MSM4_GPS, MSM5_GPS, MSM6_GPS, MSM7_GPS, - MSM4_GLONASS, MSM5_GLONASS, MSM6_GLONASS, MSM7_GLONASS, - MSM4_GALILEO, MSM5_GALILEO, MSM6_GALILEO, MSM7_GALILEO, - MSM4_QZSS, MSM5_QZSS, MSM6_QZSS, MSM7_QZSS, - MSM4_BEIDOU, MSM5_BEIDOU, MSM6_BEIDOU, MSM7_BEIDOU, - + PHYSICAL_REF_STATION_POSITION, // 1032 + ANTENNA_RECEIVER_DESCRIPTOR, // 1033 GAL_SSR_ORB_CORR, GAL_SSR_CLK_CORR, GAL_SSR_CODE_BIAS, @@ -855,7 +874,6 @@ enum class RtcmMessageType : uint16_t GAL_SSR_URA, GAL_SSR_HR_CLK_CORR, GAL_SSR_PHASE_BIAS, - QZS_SSR_ORB_CORR, QZS_SSR_CLK_CORR, QZS_SSR_CODE_BIAS, @@ -863,7 +881,6 @@ enum class RtcmMessageType : uint16_t QZS_SSR_URA, QZS_SSR_HR_CLK_CORR, QZS_SSR_PHASE_BIAS, - SBS_SSR_ORB_CORR, SBS_SSR_CLK_CORR, SBS_SSR_CODE_BIAS, @@ -871,7 +888,6 @@ enum class RtcmMessageType : uint16_t SBS_SSR_URA, SBS_SSR_HR_CLK_CORR, SBS_SSR_PHASE_BIAS, - BDS_SSR_ORB_CORR, BDS_SSR_CLK_CORR, BDS_SSR_CODE_BIAS, @@ -879,7 +895,6 @@ enum class RtcmMessageType : uint16_t BDS_SSR_URA, BDS_SSR_HR_CLK_CORR, BDS_SSR_PHASE_BIAS, - COMPACT_SSR, IGS_SSR, CUSTOM @@ -892,10 +907,22 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) { case RtcmMessageType::NONE: return 0; + case RtcmMessageType::STATIONARY_RTK_REF_ARP: + return 1005; + case RtcmMessageType::STATIONARY_RTK_REF_ARP_HEIGHT: + return 1006; + case RtcmMessageType::ANTENNA_DESCRIPTOR: + return 1007; + case RtcmMessageType::ANTENNA_DESCRIPTOR_SN: + return 1008; case RtcmMessageType::GPS_EPHEMERIS: return 1019; case RtcmMessageType::GLO_EPHEMERIS: return 1020; + case RtcmMessageType::PHYSICAL_REF_STATION_POSITION: + return 1032; + case RtcmMessageType::ANTENNA_RECEIVER_DESCRIPTOR: + return 1033; case RtcmMessageType::BDS_EPHEMERIS: return 1042; case RtcmMessageType::QZS_EPHEMERIS: @@ -904,7 +931,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1045; case RtcmMessageType::GAL_INAV_EPHEMERIS: return 1046; - case RtcmMessageType::GPS_SSR_ORB_CORR: return 1057; case RtcmMessageType::GPS_SSR_CLK_CORR: @@ -919,7 +945,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1062; case RtcmMessageType::GPS_SSR_PHASE_BIAS: return 1265; - case RtcmMessageType::GLO_SSR_ORB_CORR: return 1063; case RtcmMessageType::GLO_SSR_CLK_CORR: @@ -934,7 +959,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1068; case RtcmMessageType::GLO_SSR_PHASE_BIAS: return 1266; - case RtcmMessageType::MSM4_GPS: return 1074; case RtcmMessageType::MSM5_GPS: @@ -943,7 +967,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1076; case RtcmMessageType::MSM7_GPS: return 1077; - case RtcmMessageType::MSM4_GLONASS: return 1084; case RtcmMessageType::MSM5_GLONASS: @@ -952,7 +975,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1086; case RtcmMessageType::MSM7_GLONASS: return 1087; - case RtcmMessageType::MSM4_GALILEO: return 1094; case RtcmMessageType::MSM5_GALILEO: @@ -961,7 +983,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1096; case RtcmMessageType::MSM7_GALILEO: return 1097; - case RtcmMessageType::MSM4_QZSS: return 1114; case RtcmMessageType::MSM5_QZSS: @@ -970,7 +991,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1116; case RtcmMessageType::MSM7_QZSS: return 1117; - case RtcmMessageType::MSM4_BEIDOU: return 1124; case RtcmMessageType::MSM5_BEIDOU: @@ -979,7 +999,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1126; case RtcmMessageType::MSM7_BEIDOU: return 1127; - case RtcmMessageType::GAL_SSR_ORB_CORR: return 1240; case RtcmMessageType::GAL_SSR_CLK_CORR: @@ -994,7 +1013,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1245; case RtcmMessageType::GAL_SSR_PHASE_BIAS: return 1267; - case RtcmMessageType::QZS_SSR_ORB_CORR: return 1246; case RtcmMessageType::QZS_SSR_CLK_CORR: @@ -1009,7 +1027,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1251; case RtcmMessageType::QZS_SSR_PHASE_BIAS: return 1268; - case RtcmMessageType::SBS_SSR_ORB_CORR: return 1252; case RtcmMessageType::SBS_SSR_CLK_CORR: @@ -1024,7 +1041,6 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1257; case RtcmMessageType::SBS_SSR_PHASE_BIAS: return 1269; - case RtcmMessageType::BDS_SSR_ORB_CORR: return 1258; case RtcmMessageType::BDS_SSR_CLK_CORR: @@ -1039,14 +1055,12 @@ inline constexpr uint16_t rtcmTypeToMessageNumber(RtcmMessageType type) return 1263; case RtcmMessageType::BDS_SSR_PHASE_BIAS: return 1270; - case RtcmMessageType::COMPACT_SSR: return 4073; case RtcmMessageType::IGS_SSR: return 4076; case RtcmMessageType::CUSTOM: return 4082; - default: return 0; } @@ -1059,10 +1073,22 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) { case 0: return RtcmMessageType::NONE; + case 1005: + return RtcmMessageType::STATIONARY_RTK_REF_ARP; + case 1006: + return RtcmMessageType::STATIONARY_RTK_REF_ARP_HEIGHT; + case 1007: + return RtcmMessageType::ANTENNA_DESCRIPTOR; + case 1008: + return RtcmMessageType::ANTENNA_DESCRIPTOR_SN; case 1019: return RtcmMessageType::GPS_EPHEMERIS; case 1020: return RtcmMessageType::GLO_EPHEMERIS; + case 1032: + return RtcmMessageType::PHYSICAL_REF_STATION_POSITION; + case 1033: + return RtcmMessageType::ANTENNA_RECEIVER_DESCRIPTOR; case 1042: return RtcmMessageType::BDS_EPHEMERIS; case 1044: @@ -1071,7 +1097,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::GAL_FNAV_EPHEMERIS; case 1046: return RtcmMessageType::GAL_INAV_EPHEMERIS; - case 1057: return RtcmMessageType::GPS_SSR_ORB_CORR; case 1058: @@ -1086,7 +1111,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::GPS_SSR_HR_CLK_CORR; case 1265: return RtcmMessageType::GPS_SSR_PHASE_BIAS; - case 1063: return RtcmMessageType::GLO_SSR_ORB_CORR; case 1064: @@ -1101,7 +1125,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::GLO_SSR_HR_CLK_CORR; case 1266: return RtcmMessageType::GLO_SSR_PHASE_BIAS; - case 1074: return RtcmMessageType::MSM4_GPS; case 1075: @@ -1110,7 +1133,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::MSM6_GPS; case 1077: return RtcmMessageType::MSM7_GPS; - case 1084: return RtcmMessageType::MSM4_GLONASS; case 1085: @@ -1119,7 +1141,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::MSM6_GLONASS; case 1087: return RtcmMessageType::MSM7_GLONASS; - case 1094: return RtcmMessageType::MSM4_GALILEO; case 1095: @@ -1128,7 +1149,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::MSM6_GALILEO; case 1097: return RtcmMessageType::MSM7_GALILEO; - case 1114: return RtcmMessageType::MSM4_QZSS; case 1115: @@ -1137,7 +1157,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::MSM6_QZSS; case 1117: return RtcmMessageType::MSM7_QZSS; - case 1124: return RtcmMessageType::MSM4_BEIDOU; case 1125: @@ -1146,7 +1165,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::MSM6_BEIDOU; case 1127: return RtcmMessageType::MSM7_BEIDOU; - case 1240: return RtcmMessageType::GAL_SSR_ORB_CORR; case 1241: @@ -1161,7 +1179,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::GAL_SSR_HR_CLK_CORR; case 1267: return RtcmMessageType::GAL_SSR_PHASE_BIAS; - case 1246: return RtcmMessageType::QZS_SSR_ORB_CORR; case 1247: @@ -1176,7 +1193,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::QZS_SSR_HR_CLK_CORR; case 1268: return RtcmMessageType::QZS_SSR_PHASE_BIAS; - case 1252: return RtcmMessageType::SBS_SSR_ORB_CORR; case 1253: @@ -1191,7 +1207,6 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::SBS_SSR_HR_CLK_CORR; case 1269: return RtcmMessageType::SBS_SSR_PHASE_BIAS; - case 1258: return RtcmMessageType::BDS_SSR_ORB_CORR; case 1259: @@ -1206,14 +1221,12 @@ inline constexpr RtcmMessageType messageNumberToRtcmType(uint16_t msgNum) return RtcmMessageType::BDS_SSR_HR_CLK_CORR; case 1270: return RtcmMessageType::BDS_SSR_PHASE_BIAS; - case 4073: return RtcmMessageType::COMPACT_SSR; case 4076: return RtcmMessageType::IGS_SSR; case 4082: return RtcmMessageType::CUSTOM; - default: return RtcmMessageType::NONE; } @@ -1311,6 +1324,7 @@ enum class E_Source : short int { NONE, SPP, + META, CONFIG, PRECISE, SSR, @@ -1323,6 +1337,15 @@ enum class E_Source : short int REMOTE }; +enum class E_ReceiverMetaSource : short int +{ + NONE, + CONFIG, + SINEX, + RINEX, + RTCM +}; + enum class E_OrbexRecord : short int { PCS, @@ -1355,7 +1378,7 @@ enum class E_CrdEpochEvent : int enum class E_ObsAgeCode : short int { - OK, + UNKNOWN, NO_OBS, PAST_OBS, CURRENT_OBS, diff --git a/src/cpp/common/ephKalman.cpp b/src/cpp/common/ephKalman.cpp index 3ab34be35..2b4d14bd9 100644 --- a/src/cpp/common/ephKalman.cpp +++ b/src/cpp/common/ephKalman.cpp @@ -65,7 +65,7 @@ bool satClkKalman(Trace& trace, GTime time, SatPos& satPos, const KFState* kfSta satPos.satClkVel = vel; - satPos.satClkVar = 0; // todo Eugene: get actual variances from filter + satPos.satClkVar = 0; // todo? get actual variances from filter return anyFound; } @@ -137,7 +137,7 @@ bool satPosKalman(Trace& trace, GTime time, SatPos& satPos, const KFState* kfSta } } - satPos.posVar = 0; // todo Eugene: get actual variances from filter + satPos.posVar = 0; // todo? get actual variances from filter return true; } diff --git a/src/cpp/common/ephPrecise.cpp b/src/cpp/common/ephPrecise.cpp index 66a613fb8..9301af990 100644 --- a/src/cpp/common/ephPrecise.cpp +++ b/src/cpp/common/ephPrecise.cpp @@ -316,8 +316,8 @@ bool pclkMapClk( auto& [key, pclkMap] = *it; if ((pclkMap.size() < 2) || (time < pclkMap.begin()->first - nav.pclkInterval) || - (time > pclkMap.rbegin()->first + nav.pclkInterval - )) // Extrapolate for at most one data interval + (time > + pclkMap.rbegin()->first + nav.pclkInterval)) // Extrapolate for at most one data interval { BOOST_LOG_TRIVIAL(debug) << "no prec clock " << time.to_string() << " for " << id; diff --git a/src/cpp/common/ephSBAS.cpp b/src/cpp/common/ephSBAS.cpp index 7a1d0769f..2c32342b6 100644 --- a/src/cpp/common/ephSBAS.cpp +++ b/src/cpp/common/ephSBAS.cpp @@ -86,11 +86,11 @@ bool satPosSBAS(Trace& trace, GTime time, GTime teph, SatPos& satPos, Navigation posVar = 0.0; if (acsConfig.sbsInOpts.pvs_on_dfmc) { - clkVar = 2.5E-6; + clkVar = 2.5E-4 / SQR(CLIGHT); } else { - clkVar = 1E4; + clkVar = 1E4 / SQR(CLIGHT); usedSBASIODMap[Sat].tUsed = time; usedSBASIODMap[Sat].iodp = selIODP; usedSBASIODMap[Sat].iodf = selIODF; diff --git a/src/cpp/common/ephemeris.hpp b/src/cpp/common/ephemeris.hpp index 93eaf1c13..1efa672a8 100644 --- a/src/cpp/common/ephemeris.hpp +++ b/src/cpp/common/ephemeris.hpp @@ -63,29 +63,28 @@ struct Eph : BrdcEph, KeplerEph int sva; ///< SV accuracy (URA index) E_Svh svh; ///< SV health int week; ///< GPS/QZS: gps week, GAL:gps week (i.e. galileo week + 1024), BDS: beidou week - int code = 0; ///< GPS/QZS: code on L2, GAL: data source - int flag = 0; ///< GPS L2 P data flag - int howTow; ///< Hand over word time - GTime toc; ///< time of clock - GTime toe; ///< time of ephemeris - GTime ttm; ///< transmission time - - double toes; ///< TOE (s) in week - double fit; ///< fit interval (h) - double f0; ///< SV clock parameter (af0) - double f1; ///< SV clock parameter (af1) - double f2; ///< SV clock parameter (af2) - double tgd[4] = {}; ///< group delay parameters - ///< GPS/QZS:tgd[0]=TGD - ///< GAL :tgd[0]=BGD E5a/E1,tgd[1]=BGD E5b/E1 - ///< BDS :tgd[0]=BGD1,tgd[1]=BGD2 - - E_SatType orb = E_SatType::NONE; ///< BDS sat/orbit type - GTime top = {}; ///< time of prediction - double tops = 0; ///< t_op (s) in week - double ura[4] = - {}; ///< user range accuracy or GAL SISA - ///< GPS/QZS CNVX: ura[0]=URAI_NED0, ura[1]=URAI_NED1, ura[2]=URAI_NED2, ura[3]=URAI_ED + int code = 0; ///< GPS/QZS: code on L2, GAL: data source + int flag = 0; ///< GPS L2 P data flag + int howTow; ///< Hand over word time + GTime toc; ///< time of clock + GTime toe; ///< time of ephemeris + GTime ttm; ///< transmission time + + double toes; ///< TOE (s) in week + double fit; ///< fit interval (h) + double f0; ///< SV clock parameter (af0) + double f1; ///< SV clock parameter (af1) + double f2; ///< SV clock parameter (af2) + double tgd[4] = {}; ///< group delay parameters + ///< GPS/QZS:tgd[0]=TGD + ///< GAL :tgd[0]=BGD E5a/E1,tgd[1]=BGD E5b/E1 + ///< BDS :tgd[0]=BGD1,tgd[1]=BGD2 + + E_SatType orb = E_SatType::NONE; ///< BDS sat/orbit type + GTime top = {}; ///< time of prediction + double tops = 0; ///< t_op (s) in week + double ura[4] = {}; ///< user range accuracy or GAL SISA + ///< GPS/QZS CNVX: ura[0]=URAI_NED0, ura[1]=URAI_NED1, ura[2]=URAI_NED2, ura[3]=URAI_ED double isc[6] = {}; ///< inter-signal corrections ///< GPS/QZS CNAV: isc[0]=ISC_L1CA, isc[1]=ISC_L2C, isc[2]=ISC_L5I5, ///< isc[3]=ISC_L5Q5 GPS/QZS CNV2: isc[0]=ISC_L1CA, isc[1]=ISC_L2C, @@ -256,9 +255,8 @@ struct Ceph : KeplerEph GTime top = {}; ///< time of prediction GTime ttm = {}; ///< transmission time - double ura[4] = - {}; ///< user range accuracy - ///< GPS/QZS: ura[0]=URAI_NED0, ura[1]=URAI_NED1, ura[2]=URAI_NED2, ura[3]=URAI_ED + double ura[4] = {}; ///< user range accuracy + ///< GPS/QZS: ura[0]=URAI_NED0, ura[1]=URAI_NED1, ura[2]=URAI_NED2, ura[3]=URAI_ED double isc[6] = {}; ///< inter-signal corrections ///< GPS/QZS CNAV: isc[0]=ISC_L1CA, isc[1]=ISC_L2C, isc[2]=ISC_L5I5, ///< isc[3]=ISC_L5Q5 GPS/QZS CNV2: isc[0]=ISC_L1CA, isc[1]=ISC_L2C, diff --git a/src/cpp/common/gTime.cpp b/src/cpp/common/gTime.cpp index 6c45cf58e..aa87961f3 100644 --- a/src/cpp/common/gTime.cpp +++ b/src/cpp/common/gTime.cpp @@ -42,7 +42,7 @@ const GTime GAL_t0 = GEpoch{ 0, 0 }; // galileo system time reference as gps time -> 13 seconds before 0:00:00 - // UTC on Sunday, 22 August 1999 (midnight between 21 and 22 August) +// UTC on Sunday, 22 August 1999 (midnight between 21 and 22 August) const GTime BDS_t0 = GEpoch{ 2006, static_cast(E_Month::JAN), @@ -51,7 +51,7 @@ const GTime BDS_t0 = GEpoch{ 0, 0 + GPS_SUB_UTC_2006 }; // beidou time reference as gps time - // - defined in utc 11:58:55.816 +// - defined in utc 11:58:55.816 const int GPS_t0_sub_POSIX_t0 = 315964800; const double MJD_j2000 = 51544.5; @@ -496,13 +496,18 @@ double GTime::to_decYear() const double sod = yds.sod; // Determine if the year is a leap year - bool isLeapYear = (static_cast(year) % 4 == 0 && static_cast(year) % 100 != 0) || - (static_cast(year) % 400 == 0); - int totalDaysInYear = isLeapYear ? 366 : 365; + bool isLeapYear = (static_cast(year) % 4 == 0 && static_cast(year) % 100 != 0) || + (static_cast(year) % 400 == 0); + int totalDaysInYear = isLeapYear ? 366 : 365; return year + (doy + sod / secondsInDay) / totalDaysInYear; } +boost::posix_time::ptime GTime::to_posixTime() const +{ + return POSIX_GPS_t0 + boost::posix_time::microseconds((long int)(round(this->bigTime * 1e6))); +} + GTime::operator GEpoch() const { GEpoch gEpoch; @@ -601,6 +606,11 @@ GTime::operator MjDateTT() const return mjd; } +GTime::GTime(boost::posix_time::ptime posixTime) +{ + bigTime = (posixTime - POSIX_GPS_t0).total_microseconds() / 1e6; +} + GTime::GTime(MjDateTT mjdTT) { long double deltaDays = mjdTT.val - MJD_j2000; @@ -648,15 +658,15 @@ GTime::GTime(BWeek bdsWeek, BTow tow) *this = BDS_t0 + bdsWeek * secondsInWeek + tow; } -GEpoch ::operator GTime() const +GEpoch::operator GTime() const { return epoch2time(this->data()); } -UtcTime ::operator GTime() const +UtcTime::operator GTime() const { return utc2gpst(*this); } -GTime ::operator UtcTime() const +GTime::operator UtcTime() const { return gpst2utc(*this); } diff --git a/src/cpp/common/gTime.hpp b/src/cpp/common/gTime.hpp index f2a4294d7..40db70112 100644 --- a/src/cpp/common/gTime.hpp +++ b/src/cpp/common/gTime.hpp @@ -160,6 +160,8 @@ struct GTime double to_decYear() const; + boost::posix_time::ptime to_posixTime() const; + bool operator==(const GTime& t2) const { if (this->bigTime != t2.bigTime) @@ -270,6 +272,8 @@ struct GTime GTime(BWeek bdsWeek, BTow tow); + GTime(boost::posix_time::ptime posixTime); + GTime(MjDateTT mjdTT); GTime(MjDateUtc mjdUtc); diff --git a/src/cpp/common/gpx.cpp b/src/cpp/common/gpx.cpp index 30c17a2cc..d4f7673fc 100644 --- a/src/cpp/common/gpx.cpp +++ b/src/cpp/common/gpx.cpp @@ -47,8 +47,8 @@ void writeGPXHeader(Trace& output, string name, GTime time) } output << gpxHeader; - output << ""; // todo aaron, check format, different to below + output << ""; // todo? check format, different to below output << " \n"; output << "" << "" << name << "\n" << " \n"; diff --git a/src/cpp/common/icdDecoder.hpp b/src/cpp/common/icdDecoder.hpp index d5f699be3..b682b3c80 100644 --- a/src/cpp/common/icdDecoder.hpp +++ b/src/cpp/common/icdDecoder.hpp @@ -149,7 +149,7 @@ struct IcdDecoder decodeGpsTlmWord(words, eph); decodeGpsHowWord(words, eph); - eph.weekRollOver = gpsBitUFromWord(words, 3, 61, 10); // todo aaron, these all need scaling + eph.weekRollOver = gpsBitUFromWord(words, 3, 61, 10); // todo? these all need scaling eph.code = gpsBitUFromWord(words, 3, 71, 2); eph.sva = gpsBitUFromWord(words, 3, 73, 4); eph.ura[0] = svaToUra(eph.sva); @@ -172,7 +172,7 @@ struct IcdDecoder eph.tgd[0] = tgd == -128 ? 0 : tgd * P2_31; /* ref [4] */ eph.iodc = iodc_1 | iodc_2; - GTime nearTime = timeGet(); // todo aaron rtcmTime() + GTime nearTime = timeGet(); // todo? rtcmTime() // adjgpsweek() { @@ -212,10 +212,9 @@ struct IcdDecoder unsigned int sqrtA_2 = gpsBitUFromWord(words, 9, 241, 24); - eph.toes = gpsBitUFromWord(words, 10, 271, 16) * (1 << 4); - eph.fit = gpsBitUFromWord(words, 10, 287, 1) ? 0 : 4; /* 0:4hr,1:>4hr */ - int aodo = gpsBitUFromWord(words, 10, 288, 5); // todo aaron - + eph.toes = gpsBitUFromWord(words, 10, 271, 16) * (1 << 4); + eph.fit = gpsBitUFromWord(words, 10, 287, 1) ? 0 : 4; /* 0:4hr,1:>4hr */ + int aodo = gpsBitUFromWord(words, 10, 288, 5); // todo? eph.sqrtA = (sqrtA_1 | sqrtA_2) * P2_19; eph.M0 = (M0_1 | M0_2) * P2_31 * SC2RAD; eph.e = (e_1 | e_2) * P2_33; diff --git a/src/cpp/common/lapackWrapper.hpp b/src/cpp/common/lapackWrapper.hpp index 69167d935..777c10266 100644 --- a/src/cpp/common/lapackWrapper.hpp +++ b/src/cpp/common/lapackWrapper.hpp @@ -102,13 +102,19 @@ extern "C" ); } -// BLAS function declarations - handle different environments -// Eigen/OpenBLAS declares these with int return, standard BLAS uses void -#if !defined(EIGEN_USE_BLAS) && !defined(EIGEN_BLAS_H) -// Standard BLAS declarations (void return, Fortran style) +// BLAS function declarations used directly by this wrapper. +// Eigen 3 declares these Fortran BLAS entry points as returning int, while +// Eigen 5 changed them to void. Match Eigen when its version macros are visible +// so this wrapper can coexist with either Eigen BLAS header. +#if defined(EIGEN_MAJOR_VERSION) && EIGEN_MAJOR_VERSION < 5 +using BlasReturn = int; +#else +using BlasReturn = void; +#endif + extern "C" { - void dgemm_( + BlasReturn dgemm_( const char* transa, const char* transb, const int* m, @@ -123,7 +129,7 @@ extern "C" double* c, const int* ldc ); - void dgemv_( + BlasReturn dgemv_( const char* trans, const int* m, const int* n, @@ -136,8 +142,8 @@ extern "C" double* y, const int* incy ); - void dcopy_(const int* n, const double* x, const int* incx, double* y, const int* incy); - void daxpy_( + BlasReturn dcopy_(int* n, double* x, int* incx, double* y, int* incy); + BlasReturn daxpy_( const int* n, const double* alpha, const double* x, @@ -145,7 +151,7 @@ extern "C" double* y, const int* incy ); - void dsymm_( + BlasReturn dsymm_( const char* side, const char* uplo, const int* m, @@ -159,7 +165,7 @@ extern "C" double* c, const int* ldc ); - void dsyrk_( + BlasReturn dsyrk_( const char* uplo, const char* trans, const int* n, @@ -172,8 +178,6 @@ extern "C" const int* ldc ); } -#endif -// Note: When EIGEN_USE_BLAS/EIGEN_BLAS_H is defined, these are already declared by Eigen headers // Cholesky factorization (positive definite) inline int dpotrf(Layout layout, char uplo, int n, double* a, int lda) @@ -370,9 +374,6 @@ constexpr Layout ROW_MAJOR = Layout::RowMajor; // BLAS Wrappers (using pure Fortran BLAS instead of CBLAS) // ============================================================================= -// BLAS functions are declared by Eigen with int return type on some platforms -// We don't need to declare them - just use them directly - // Transpose enum to match CBLAS enum class Transpose { @@ -500,14 +501,14 @@ inline void daxpy(int n, double alpha, const double* x, int incx, double* y, int // where A is symmetric inline void dsymm( Layout layout, - char side, // 'L' for A*B, 'R' for B*A - char uplo, // 'U' or 'L' - which triangle of A is stored - int m, // Rows of C - int n, // Cols of C + char side, // 'L' for A*B, 'R' for B*A + char uplo, // 'U' or 'L' - which triangle of A is stored + int m, // Rows of C + int n, // Cols of C double alpha, - const double* a, // Symmetric matrix + const double* a, // Symmetric matrix int lda, - const double* b, // General matrix + const double* b, // General matrix int ldb, double beta, double* c, @@ -539,10 +540,10 @@ inline void dsymm( // where C is symmetric inline void dsyrk( Layout layout, - char uplo, // 'U' or 'L' - which triangle of C to update - char trans, // 'N' for A*A^T, 'T' for A^T*A - int n, // Order of C - int k, // Inner dimension + char uplo, // 'U' or 'L' - which triangle of C to update + char trans, // 'N' for A*A^T, 'T' for A^T*A + int n, // Order of C + int k, // Inner dimension double alpha, const double* a, int lda, @@ -556,18 +557,7 @@ inline void dsyrk( return; } - dsyrk_( - &uplo, - &trans, - &n, - &k, - &alpha, - const_cast(a), - &lda, - &beta, - c, - &ldc - ); + dsyrk_(&uplo, &trans, &n, &k, &alpha, const_cast(a), &lda, &beta, c, &ldc); } } // namespace LapackWrapper diff --git a/src/cpp/common/linearCombo.cpp b/src/cpp/common/linearCombo.cpp index cfb631cbd..c3f157a06 100644 --- a/src/cpp/common/linearCombo.cpp +++ b/src/cpp/common/linearCombo.cpp @@ -219,85 +219,119 @@ void obs2lc( S_LC& lc15 = getLC(obs, lcBase, frq1, frq3); S_LC& lc25 = getLC(obs, lcBase, frq2, frq3); + string frq1Str = enum_to_string(frq1); + string frq2Str = enum_to_string(frq2); + string frq3Str = enum_to_string(frq3); + string frq12Str = frq1Str + frq2Str; + string frq13Str = frq1Str + frq3Str; + string frq23Str = frq2Str + frq3Str; + tracepdeex( 3, trace, - "%s zd L -- L1 =%14.4f L2 =%14.4f L5 =%14.4f\n", + "%s zd L -- %-3s =%14.4f %-3s =%14.4f %-3s =%14.4f\n", strprefix, + frq1Str.c_str(), lcBase.L_m[frq1], + frq2Str.c_str(), lcBase.L_m[frq2], + frq3Str.c_str(), lcBase.L_m[frq3] ); tracepdeex( 3, trace, - "%s zd P -- P1 =%14.4f P2 =%14.4f P5 =%14.4f\n", + "%s zd P -- %-3s =%14.4f %-3s =%14.4f %-3s =%14.4f\n", strprefix, + frq1Str.c_str(), lcBase.P[frq1], + frq2Str.c_str(), lcBase.P[frq2], + frq3Str.c_str(), lcBase.P[frq3] ); tracepdeex( 3, trace, - "%s mp P -- mp1 =%14.4f mp2 =%14.4f mp5 =%14.4f\n", + "%s mp P -- %-3s =%14.4f %-3s =%14.4f %-3s =%14.4f\n", strprefix, + frq1Str.c_str(), lcBase.mp[frq1], + frq2Str.c_str(), lcBase.mp[frq2], + frq3Str.c_str(), lcBase.mp[frq3] ); tracepdeex( 3, trace, - "%s gf L -- gf12=%14.4f gf15=%14.4f gf25=%14.4f\n", + "%s gf L -- %-6s=%14.4f %-6s=%14.4f %-6s=%14.4f\n", strprefix, + frq12Str.c_str(), lc12.GF_Phas_m, + frq13Str.c_str(), lc15.GF_Phas_m, + frq23Str.c_str(), lc25.GF_Phas_m ); tracepdeex( 3, trace, - "%s gf P -- gf12=%14.4f gf15=%14.4f gf25=%14.4f\n", + "%s gf P -- %-6s=%14.4f %-6s=%14.4f %-6s=%14.4f\n", strprefix, + frq12Str.c_str(), lc12.GF_Code_m, + frq13Str.c_str(), lc15.GF_Code_m, + frq23Str.c_str(), lc25.GF_Code_m ); tracepdeex( 3, trace, - "%s mw L -- mw12=%14.4f mw15=%14.4f mw25=%14.4f\n", + "%s mw L -- %-6s=%14.4f %-6s=%14.4f %-6s=%14.4f\n", strprefix, + frq12Str.c_str(), lc12.MW_c, + frq13Str.c_str(), lc15.MW_c, + frq23Str.c_str(), lc25.MW_c ); tracepdeex( 3, trace, - "%s wl L -- wl12=%14.4f wl15=%14.4f wl25=%14.4f\n", + "%s wl L -- %-6s=%14.4f %-6s=%14.4f %-6s=%14.4f\n", strprefix, + frq12Str.c_str(), lc12.WL_Phas_m, + frq13Str.c_str(), lc15.WL_Phas_m, + frq23Str.c_str(), lc25.WL_Phas_m ); tracepdeex( 3, trace, - "%s if L -- if12=%14.4f if15=%14.4f if25=%14.4f\n", + "%s if L -- %-6s=%14.4f %-6s=%14.4f %-6s=%14.4f\n", strprefix, + frq12Str.c_str(), lc12.IF_Phas_m, + frq13Str.c_str(), lc15.IF_Phas_m, + frq23Str.c_str(), lc25.IF_Phas_m ); tracepdeex( 3, trace, - "%s if P -- if12=%14.4f if15=%14.4f if25=%14.4f\n", + "%s if P -- %-6s=%14.4f %-6s=%14.4f %-6s=%14.4f\n", strprefix, + frq12Str.c_str(), lc12.IF_Code_m, + frq13Str.c_str(), lc15.IF_Code_m, + frq23Str.c_str(), lc25.IF_Code_m ); @@ -306,7 +340,7 @@ void obs2lc( trace, obs.time, {{"data", "linearCombos"}, {"Sat", obs.Sat.id()}}, - {{"L1", lcBase.L_m[frq1]}, {"L2", lcBase.L_m[frq2]}} + {{frq1Str, lcBase.L_m[frq1]}, {frq2Str, lcBase.L_m[frq2]}} ); } diff --git a/src/cpp/common/localAtmosRegion.cpp b/src/cpp/common/localAtmosRegion.cpp index f666145ce..423b6caac 100644 --- a/src/cpp/common/localAtmosRegion.cpp +++ b/src/cpp/common/localAtmosRegion.cpp @@ -173,7 +173,7 @@ bool configAtmosRegion_File() tmp[10] = '\0'; latInt = atof(tmp); - if (gridType == 1) // todo aaron magic numbers + if (gridType == 1) // todo? magic numbers { regMaps[regID].minLatDeg = lat0 - latNgrid * latInt; regMaps[regID].maxLatDeg = lat0; @@ -360,15 +360,19 @@ bool configAtmosRegions(Trace& trace, ReceiverMap& receiverMap) { for (auto& [id, rec] : receiverMap) { - VectorEcef& snxPos = rec.snx.pos; - auto& recOpts = acsConfig.getRecOpts(id); - if (recOpts.apriori_pos.isZero() == false) - snxPos = recOpts.apriori_pos; + rec.metadata.setPriority(recOpts.meta_priority); + rec.metadata.ingestConfig(recOpts); + rec.metadata.ingestSinex(rec.snx); + + if (rec.metadata.stationPosition.valid == false) + { + continue; + } auto& pos = rec.pos; - pos = ecef2pos(snxPos); + pos = ecef2pos(rec.metadata.stationPosition.value); if (atmRegion.gridLatDeg.empty()) { @@ -399,6 +403,12 @@ bool configAtmosRegions(Trace& trace, ReceiverMap& receiverMap) atmRegion.gridLonDeg[ngrid] = pos.lonDeg(); ngrid++; } + + if (ngrid == 0) + { + return false; + } + atmRegion.minLonDeg -= 0.001; atmRegion.minLatDeg -= 0.001; atmRegion.maxLatDeg += 0.001; diff --git a/src/cpp/common/mongo.hpp b/src/cpp/common/mongo.hpp index ac2df7704..6ca0ec324 100644 --- a/src/cpp/common/mongo.hpp +++ b/src/cpp/common/mongo.hpp @@ -159,7 +159,7 @@ struct Mongo #define MONGO_AVAILABLE "Available" -// @todo seb put all define as const char* in a namespace +// @todo? put all define as const char* in a namespace b_date bDate(const GTime& time); diff --git a/src/cpp/common/mongoRead.cpp b/src/cpp/common/mongoRead.cpp index b9aacea75..ec1d52f0b 100644 --- a/src/cpp/common/mongoRead.cpp +++ b/src/cpp/common/mongoRead.cpp @@ -9,7 +9,7 @@ using std::deque; -short int currentSSRIod = 0; // todo aaron, sketchy global? +short int currentSSRIod = 0; // todo? sketchy global? map lastBrdcIode; template diff --git a/src/cpp/common/mongoWrite.cpp b/src/cpp/common/mongoWrite.cpp index 6c5369cb1..2273b93ae 100644 --- a/src/cpp/common/mongoWrite.cpp +++ b/src/cpp/common/mongoWrite.cpp @@ -538,8 +538,8 @@ void mongoMeasResiduals( } arrayDoc << close_array; - auto findDoc = document{} << toString(Constants::Mongo::TYPE_VAR) << name - << finalize; + auto findDoc = document{} << toString(Constants::Mongo::TYPE_VAR) << name + << finalize; auto updateDoc = document{} << "$addToSet" << open_document << toString(Constants::Mongo::VALUE_VAR) << eachDoc << close_document << finalize; @@ -766,8 +766,8 @@ void mongoStates(KFState& kfState, MongoStatesOptions opts) } arrayDoc << close_array; - auto findDoc = document{} << toString(Constants::Mongo::TYPE_VAR) << name - << finalize; + auto findDoc = document{} << toString(Constants::Mongo::TYPE_VAR) << name + << finalize; auto updateDoc = document{} << "$addToSet" << open_document << toString(Constants::Mongo::VALUE_VAR) << eachDoc << close_document << finalize; @@ -1027,7 +1027,7 @@ void prepareSsrStates( E_OffsetType::APC, nav, &kfState - ); // todo aaron, ssra streams expect common_sat_pco to be true + ); // todo? ssra streams expect common_sat_pco to be true if (obs.satClk == INVALID_CLOCK_VALUE) { pass = false; diff --git a/src/cpp/common/ntripBroadcast.cpp b/src/cpp/common/ntripBroadcast.cpp index c38a22cba..45e52e31e 100644 --- a/src/cpp/common/ntripBroadcast.cpp +++ b/src/cpp/common/ntripBroadcast.cpp @@ -534,7 +534,7 @@ void NtripUploader::messageTimeoutHandler(const boost::system::error_code& err) case CompactSSRSubtype::CMB: case CompactSSRSubtype::URA: for (auto [sys, proc] : - acsConfig.process_sys) // todo aaron, this is all just copying + acsConfig.process_sys) // todo? this is all just copying // stuff from one map to another if (proc) { diff --git a/src/cpp/common/ntripBroadcast.hpp b/src/cpp/common/ntripBroadcast.hpp index 424502922..0299bdfeb 100644 --- a/src/cpp/common/ntripBroadcast.hpp +++ b/src/cpp/common/ntripBroadcast.hpp @@ -41,6 +41,7 @@ struct NtripUploader : NtripResponder, RtcmEncoder if (acsConfig.output_encoded_rtcm_json) { rtcmTraceFilename = acsConfig.encoded_rtcm_json_filename; + setTraceFilename(rtcmTraceFilename); } std::stringstream requestStream; diff --git a/src/cpp/common/observations.hpp b/src/cpp/common/observations.hpp index a71005d90..cbcaf8d7b 100644 --- a/src/cpp/common/observations.hpp +++ b/src/cpp/common/observations.hpp @@ -110,7 +110,7 @@ struct IonoObs double stecVar; int stecCodeCombo; - SatSys ionoSat; // todo aaron, remove when possible + SatSys ionoSat; // todo? remove when possible map ippMap; diff --git a/src/cpp/common/receiver.cpp b/src/cpp/common/receiver.cpp index 6794bac86..39c38825e 100644 --- a/src/cpp/common/receiver.cpp +++ b/src/cpp/common/receiver.cpp @@ -1,19 +1,41 @@ #include "common/receiver.hpp" #include -#include "common/sinex.hpp" +#include "common/acsConfig.hpp" #include "common/streamParser.hpp" #include "common/streamRinex.hpp" #include "common/streamRtcm.hpp" -SinexSiteId dummySiteid; -SinexReceiver dummyReceiver; -SinexAntenna dummyAntenna; -SinexSiteEcc dummySiteEcc; +ReceiverMap receiverMap; -SinexSatIdentity dummySinexSatIdentity; -SinexSatEcc dummySinexSatEcc; +void extractReceiverMetadata(Receiver& rec, Parser& parser, ObsList* obsList) +{ + auto& recOpts = acsConfig.getRecOpts(rec.id); + rec.metadata.setPriority(recOpts.meta_priority); -ReceiverMap receiverMap; + string parserType = parser.parserType(); + + if (parserType == "RinexParser") + { + auto& rinexParser = static_cast(parser); + rec.metadata.ingestRinex(rinexParser.rnxRec); + } + else if (parserType == "RtcmParser") + { + auto& rtcmParser = static_cast(parser); + + if (auto* info_ptr = selectRtcmStationInfoForMetadata( + rtcmParser.stationInfoMap, + rtcmParser.lastReferenceStationId + )) + { + rec.metadata.ingestRtcm(*info_ptr); + } + } + + (void)obsList; + + syncReceiverMetadata(rec); +} void extractTrackedSignals(Receiver& rec, Parser& parser, ObsList* obsList) { diff --git a/src/cpp/common/receiver.hpp b/src/cpp/common/receiver.hpp index 17e27eeda..c20ab5c14 100644 --- a/src/cpp/common/receiver.hpp +++ b/src/cpp/common/receiver.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "common/attitude.hpp" #include "common/cache.hpp" #include "common/common.hpp" @@ -42,6 +44,116 @@ struct RinexStation Vector3d pos = Vector3d::Zero(); }; +constexpr uint32_t receiverMetaSourceBit(E_ReceiverMetaSource source) +{ + return source == E_ReceiverMetaSource::NONE + ? 0 + : (uint32_t(1) << (static_cast(source) - 1)); +} + +template +struct ReceiverMetaField +{ + T value = {}; + bool valid = false; + E_ReceiverMetaSource winningSource = E_ReceiverMetaSource::NONE; + uint32_t sourceMask = 0; + + bool hasSource(E_ReceiverMetaSource source) const + { + return (sourceMask & receiverMetaSourceBit(source)) != 0; + } +}; + +struct ReceiverOptions; +struct RtcmStationInfo; +struct SinexRecData; + +inline vector defaultReceiverMetaSourcePriority() +{ + return { + E_ReceiverMetaSource::CONFIG, + E_ReceiverMetaSource::SINEX, + E_ReceiverMetaSource::RINEX, + E_ReceiverMetaSource::RTCM + }; +} + +inline size_t receiverMetaPriorityIndex( + E_ReceiverMetaSource source, + const vector& priorityOrder +) +{ + auto it = std::find(priorityOrder.begin(), priorityOrder.end(), source); + if (it != priorityOrder.end()) + { + return std::distance(priorityOrder.begin(), it); + } + + return priorityOrder.size() + static_cast(source); +} + +inline bool receiverMetaSourceEnabled( + E_ReceiverMetaSource source, + const vector& priorityOrder +) +{ + return std::find(priorityOrder.begin(), priorityOrder.end(), source) != priorityOrder.end(); +} + +template +void ingestReceiverMetaField( + ReceiverMetaField& field, + const T& candidate, + bool present, + E_ReceiverMetaSource source, + const vector& priorityOrder +) +{ + if (present == false) + { + return; + } + + if (receiverMetaSourceEnabled(source, priorityOrder) == false) + { + return; + } + + field.sourceMask |= receiverMetaSourceBit(source); + + if (field.valid == false || receiverMetaPriorityIndex(source, priorityOrder) < + receiverMetaPriorityIndex(field.winningSource, priorityOrder)) + { + field.value = candidate; + field.valid = true; + field.winningSource = source; + } +} + +struct ReceiverMetadata +{ + vector sourcePriority = defaultReceiverMetaSourcePriority(); + + ReceiverMetaField receiverType; + ReceiverMetaField receiverFirmware; + ReceiverMetaField receiverSerial; + ReceiverMetaField antennaDescriptor; + ReceiverMetaField antennaSerial; + ReceiverMetaField markerName; + ReceiverMetaField markerNumber; + ReceiverMetaField antennaDelta; + ReceiverMetaField stationPosition; + + void reset(); + void setPriority(const vector& priorityOrder); + + void ingestConfig(const ReceiverOptions& recOpts); + void ingestSinex(const SinexRecData& recSnx); + void ingestRinex(const RinexStation& rnxRec); + void ingestRtcm(const RtcmStationInfo& rtcmInfo); +}; + struct ReceiverLogs { PTime firstEpoch = GTime::noTime(); @@ -61,9 +173,9 @@ struct Rtk Solution sol; ///< RTK solution string antennaType; string receiverType; - string antennaId; + string antennaId; ///< Derived ATX lookup id after antenna/radome resolution map satStatMap; - VectorEnu antDelta; ///< antenna delta {rov_e,rov_n,rov_u} + VectorEnu antDelta; ///< antenna delta {rov_e,rov_n,rov_u} AttStatus attStatus; }; @@ -79,13 +191,13 @@ extern SinexSiteEcc dummySiteEcc; struct SinexRecData { - SinexSiteId* id_ptr = &dummySiteid; + SinexSiteId* id_ptr = &dummySiteid; // Eugene: should be initialised with nullptr? SinexReceiver* rec_ptr = &dummyReceiver; SinexAntenna* ant_ptr = &dummyAntenna; SinexSiteEcc* ecc_ptr = &dummySiteEcc; UYds start; - UYds stop = UYds(-1, -1, -1); + UYds stop; bool primary = false; ///< this position estimate is considered to come from a primary source VectorEcef pos; @@ -98,9 +210,10 @@ struct SinexRecData */ struct Receiver : ReceiverLogs, Rtk { - bool isPseudoRec = false; - bool invalid = false; - SinexRecData snx; ///< Antenna information + bool isPseudoRec = false; + bool invalid = false; + SinexRecData snx; ///< Antenna information + ReceiverMetadata metadata; map metaDataMap; @@ -140,7 +253,6 @@ struct Receiver : ReceiverLogs, Rtk unsigned failureSinex : 1; unsigned failureAprioriPos : 1; unsigned failureEccentricity : 1; - unsigned failureAntenna : 1; }; }; Cache> pppTideCache; @@ -154,6 +266,8 @@ struct ReceiverMap : map extern ReceiverMap receiverMap; void extractTrackedSignals(Receiver& rec, Parser& parser, ObsList* obsList = nullptr); +void extractReceiverMetadata(Receiver& rec, Parser& parser, ObsList* obsList = nullptr); +void syncReceiverMetadata(Receiver& rec); struct Network { diff --git a/src/cpp/common/receiverMetadata.cpp b/src/cpp/common/receiverMetadata.cpp new file mode 100644 index 000000000..b3ff12dbd --- /dev/null +++ b/src/cpp/common/receiverMetadata.cpp @@ -0,0 +1,281 @@ +#include +#include "common/acsConfig.hpp" +#include "common/receiver.hpp" +#include "common/rtcmDecoder.hpp" +#include "common/sinex.hpp" + +SinexSiteId dummySiteid; +SinexReceiver dummyReceiver; +SinexAntenna dummyAntenna; +SinexSiteEcc dummySiteEcc; + +SinexSatIdentity dummySinexSatIdentity; +SinexSatEcc dummySinexSatEcc; + +static string trimmedCopy(const string& value) +{ + return boost::algorithm::trim_copy(value); +} + +static void ingestTrimmedReceiverMetaField( + ReceiverMetaField& field, + const string& candidate, + E_ReceiverMetaSource source, + const vector& priorityOrder +) +{ + string trimmed = trimmedCopy(candidate); + + ingestReceiverMetaField(field, trimmed, trimmed.empty() == false, source, priorityOrder); +} + +static bool hasRtcmStationPosition(const RtcmStationInfo& rtcmInfo) +{ + if (rtcmInfo.physicalStationId >= 0) + { + return rtcmInfo.physEcefX != 0 || rtcmInfo.physEcefY != 0 || rtcmInfo.physEcefZ != 0; + } + + return rtcmInfo.ecefX != 0 || rtcmInfo.ecefY != 0 || rtcmInfo.ecefZ != 0; +} + +static bool hasConfigAntennaDelta(const ReceiverOptions& recOpts) +{ + return recOpts.eccentricityModel.enable && + (isInited(recOpts, recOpts.eccentricityModel.eccentricity) || + recOpts.eccentricityModel.eccentricity.isZero() == false); +} + +static bool sinexAntennaDelta(const SinexRecData& recSnx, Vector3d& antennaDelta) +{ + if (recSnx.ecc_ptr == &dummySiteEcc) + { + return false; + } + + antennaDelta = Vector3d(recSnx.ecc_ptr->ecc); + return true; +} + +void ReceiverMetadata::reset() +{ + *this = ReceiverMetadata(); +} + +void ReceiverMetadata::setPriority(const vector& priorityOrder) +{ + if (priorityOrder.empty()) + { + sourcePriority = defaultReceiverMetaSourcePriority(); + return; + } + + sourcePriority = priorityOrder; +} + +void ReceiverMetadata::ingestConfig(const ReceiverOptions& recOpts) +{ + setPriority(recOpts.meta_priority); + + ingestTrimmedReceiverMetaField( + receiverType, + recOpts.receiver_type, + E_ReceiverMetaSource::CONFIG, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + antennaDescriptor, + recOpts.antenna_type, + E_ReceiverMetaSource::CONFIG, + sourcePriority + ); + ingestReceiverMetaField( + antennaDelta, + recOpts.eccentricityModel.eccentricity, + hasConfigAntennaDelta(recOpts), + E_ReceiverMetaSource::CONFIG, + sourcePriority + ); + ingestReceiverMetaField( + stationPosition, + recOpts.apriori_pos, + recOpts.apriori_pos.isZero() == false, + E_ReceiverMetaSource::CONFIG, + sourcePriority + ); +} + +void ReceiverMetadata::ingestSinex(const SinexRecData& recSnx) +{ + Vector3d antennaDeltaCandidate = Vector3d::Zero(); + bool hasAntennaDelta = sinexAntennaDelta(recSnx, antennaDeltaCandidate); + + ingestTrimmedReceiverMetaField( + receiverType, + recSnx.rec_ptr->type, + E_ReceiverMetaSource::SINEX, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + receiverFirmware, + recSnx.rec_ptr->firm, + E_ReceiverMetaSource::SINEX, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + receiverSerial, + recSnx.rec_ptr->sn, + E_ReceiverMetaSource::SINEX, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + antennaDescriptor, + recSnx.ant_ptr->type, + E_ReceiverMetaSource::SINEX, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + antennaSerial, + recSnx.ant_ptr->sn, + E_ReceiverMetaSource::SINEX, + sourcePriority + ); + ingestReceiverMetaField( + antennaDelta, + antennaDeltaCandidate, + hasAntennaDelta, + E_ReceiverMetaSource::SINEX, + sourcePriority + ); + ingestReceiverMetaField( + stationPosition, + Vector3d(recSnx.pos), + recSnx.pos.isZero() == false, + E_ReceiverMetaSource::SINEX, + sourcePriority + ); +} + +void ReceiverMetadata::ingestRinex(const RinexStation& rnxRec) +{ + ingestTrimmedReceiverMetaField( + receiverType, + rnxRec.recType, + E_ReceiverMetaSource::RINEX, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + receiverFirmware, + rnxRec.recFWVersion, + E_ReceiverMetaSource::RINEX, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + receiverSerial, + rnxRec.recSerial, + E_ReceiverMetaSource::RINEX, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + antennaDescriptor, + rnxRec.antDesc, + E_ReceiverMetaSource::RINEX, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + antennaSerial, + rnxRec.antSerial, + E_ReceiverMetaSource::RINEX, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + markerName, + rnxRec.id, + E_ReceiverMetaSource::RINEX, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + markerNumber, + rnxRec.marker, + E_ReceiverMetaSource::RINEX, + sourcePriority + ); + ingestReceiverMetaField( + antennaDelta, + rnxRec.del, + true, + E_ReceiverMetaSource::RINEX, + sourcePriority + ); + ingestReceiverMetaField( + stationPosition, + rnxRec.pos, + rnxRec.pos.isZero() == false, + E_ReceiverMetaSource::RINEX, + sourcePriority + ); +} + +void ReceiverMetadata::ingestRtcm(const RtcmStationInfo& rtcmInfo) +{ + ingestTrimmedReceiverMetaField( + receiverType, + rtcmInfo.receiverType, + E_ReceiverMetaSource::RTCM, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + receiverFirmware, + rtcmInfo.receiverFirmware, + E_ReceiverMetaSource::RTCM, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + receiverSerial, + rtcmInfo.receiverSerial, + E_ReceiverMetaSource::RTCM, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + antennaDescriptor, + rtcmInfo.antennaDesc, + E_ReceiverMetaSource::RTCM, + sourcePriority + ); + ingestTrimmedReceiverMetaField( + antennaSerial, + rtcmInfo.antennaSerial, + E_ReceiverMetaSource::RTCM, + sourcePriority + ); + ingestReceiverMetaField( + antennaDelta, + Vector3d(0, 0, rtcmInfo.antennaHeight), + rtcmInfo.hasAntennaHeight, + E_ReceiverMetaSource::RTCM, + sourcePriority + ); + + Vector3d rtcmPosition = {rtcmInfo.ecefX, rtcmInfo.ecefY, rtcmInfo.ecefZ}; + if (rtcmInfo.physicalStationId >= 0) + { + rtcmPosition = {rtcmInfo.physEcefX, rtcmInfo.physEcefY, rtcmInfo.physEcefZ}; + } + + ingestReceiverMetaField( + stationPosition, + rtcmPosition, + hasRtcmStationPosition(rtcmInfo), + E_ReceiverMetaSource::RTCM, + sourcePriority + ); +} + +void syncReceiverMetadata(Receiver& rec) +{ + rec.receiverType = rec.metadata.receiverType.valid ? rec.metadata.receiverType.value : ""; + rec.antennaType = + rec.metadata.antennaDescriptor.valid ? rec.metadata.antennaDescriptor.value : ""; + rec.antDelta = + rec.metadata.antennaDelta.valid ? rec.metadata.antennaDelta.value : VectorEnu::Zero(); +} diff --git a/src/cpp/common/rinex.cpp b/src/cpp/common/rinex.cpp index 9acc380e2..8a52899d7 100644 --- a/src/cpp/common/rinex.cpp +++ b/src/cpp/common/rinex.cpp @@ -822,7 +822,6 @@ int decodeObsEpoch( int n = 0; char* buff = &line[0]; - if (ver <= 2.99) { // ver.2 @@ -879,7 +878,7 @@ int decodeObsEpoch( if (flag >= 3 && flag <= 5) return n; - if (buff[0] != '>' || str2time(buff, 1, 28, time, tsys)) + if (buff[0] != '>' || str2time(buff, 2, 29, time, tsys)) { BOOST_LOG_TRIVIAL(debug) << "rinex obs invalid epoch: epoch=" << buff; return 0; @@ -1510,7 +1509,7 @@ int decodeGeph( if (ver >= 3.05) { - // todo Eugene: additional records from version 3.05 and on + // todo? additional records from version 3.05 and on } // some receiver output >128 for minus frequency number @@ -2040,7 +2039,7 @@ int readRnxNavB( return decodeGeph(ver, Sat, toc, data, geph); } break; - } // todo Eugene: additional records from version 3.05 and on + } // todo? additional records from version 3.05 and on case E_NavMsgType::SBAS: { if (data.size() >= 15) diff --git a/src/cpp/common/rinexClkWrite.cpp b/src/cpp/common/rinexClkWrite.cpp index 4977cca6a..058ee4dbf 100644 --- a/src/cpp/common/rinexClkWrite.cpp +++ b/src/cpp/common/rinexClkWrite.cpp @@ -234,8 +234,7 @@ void getSatClksFromEph( ephType, E_OffsetType::COM, nav - ); // use both for now to get ssr - // clocks if required + ); // use both for now to get ssr clocks if required if (pass == false) { BOOST_LOG_TRIVIAL(warning) @@ -466,7 +465,7 @@ void outputClocksSet( ClockEntry referenceRec; referenceRec.isRec = false; - switch (clkDataSatSrcs.front()) // todo aaron, remove this function + switch (clkDataSatSrcs.front()) // todo? remove this function { case E_Source::NONE: break; diff --git a/src/cpp/common/rinexNavWrite.cpp b/src/cpp/common/rinexNavWrite.cpp index be3e0e5a7..0a4517baf 100644 --- a/src/cpp/common/rinexNavWrite.cpp +++ b/src/cpp/common/rinexNavWrite.cpp @@ -257,7 +257,7 @@ void outputNavRinexGeph(Geph& geph, Trace& trace, const double rnxver) if (rnxver >= 3.05) { - // todo Eugene: additional records from version 3.05 and on + // todo? additional records from version 3.05 and on } } diff --git a/src/cpp/common/rinexObsWrite.cpp b/src/cpp/common/rinexObsWrite.cpp index a1ac472b0..fdd9756f5 100644 --- a/src/cpp/common/rinexObsWrite.cpp +++ b/src/cpp/common/rinexObsWrite.cpp @@ -225,13 +225,22 @@ void writeRinexObsHeader( "OBSERVER / AGENCY" ); + string receiverType = + rec.metadata.receiverType.valid ? rec.metadata.receiverType.value : rec.receiverType; + string antennaType = rec.metadata.antennaDescriptor.valid ? rec.metadata.antennaDescriptor.value + : rec.antennaType; + VectorEnu antennaDelta = + rec.metadata.antennaDelta.valid ? rec.metadata.antennaDelta.value : rec.antDelta; + tracepdeex( 0, rinexStream, "%-20.20s%-20.20s%-20.20s%-20s\n", - snx.rec_ptr->sn, - rec.receiverType.c_str(), - snx.rec_ptr->firm, + rec.metadata.receiverSerial.valid ? rec.metadata.receiverSerial.value.c_str() + : snx.rec_ptr->sn, + receiverType.c_str(), + rec.metadata.receiverFirmware.valid ? rec.metadata.receiverFirmware.value.c_str() + : snx.rec_ptr->firm, "REC # / TYPE / VERS" ); @@ -239,8 +248,9 @@ void writeRinexObsHeader( 0, rinexStream, "%-20.20s%-20.20s%-20.20s%-20s\n", - snx.ant_ptr->sn, - rec.antennaType.c_str(), + rec.metadata.antennaSerial.valid ? rec.metadata.antennaSerial.value.c_str() + : snx.ant_ptr->sn, + antennaType.c_str(), "", "ANT # / TYPE" ); @@ -260,9 +270,9 @@ void writeRinexObsHeader( 0, rinexStream, "%14.4f%14.4f%14.4f%-18s%-20s\n", - rec.antDelta[2], - rec.antDelta[0], - rec.antDelta[1], + antennaDelta[2], + antennaDelta[0], + antennaDelta[1], "", "ANTENNA: DELTA H/E/N" ); diff --git a/src/cpp/common/rtcmDecoder.cpp b/src/cpp/common/rtcmDecoder.cpp index 63547a09b..20d74a7f3 100644 --- a/src/cpp/common/rtcmDecoder.cpp +++ b/src/cpp/common/rtcmDecoder.cpp @@ -43,7 +43,7 @@ GTime RtcmDecoder::rtcmTime() time = rtcmTimestampTime; else if (tsync != GTime::noTime()) time = tsync; - // todo Eugene: gps nav + // todo? gps nav else time = timeGet(); @@ -145,7 +145,15 @@ void RtcmDecoder::decodeSSR(vector& data) ///< stream data string messCodeStr = enum_to_string(messCode); string messTypeStr = messCodeStr.substr(8); - E_Sys sys = rtcmMessageSystemMap[messCode]; + auto sysIt = rtcmMessageSystemMap.find(messCode); + if (sysIt == rtcmMessageSystemMap.end()) + { + BOOST_LOG_TRIVIAL(error) << "No message-system mapping for " << messCode << " in " + << __FUNCTION__; + return; + } + + E_Sys sys = sysIt->second; if (sys == E_Sys::NONE) { @@ -464,7 +472,7 @@ void RtcmDecoder::decodeSSR(vector& data) ///< stream data continue; } - ssrBiasCode.obsCodeBiasMap[obsCode].bias = bias; // todo aaron missing var + ssrBiasCode.obsCodeBiasMap[obsCode].bias = bias; // todo? missing var if (acsConfig.output_decoded_rtcm_json) traceSsrCodeBias(messCode, Sat, obsCode, ssrBiasCode); @@ -575,7 +583,7 @@ void RtcmDecoder::decodeSSR(vector& data) ///< stream data } ssrBiasPhas.obsCodeBiasMap[obsCode].bias = - bias; // offset meters due to satellite rotation. //todo aaron missing var + bias; // offset meters due to satellite rotation. //todo? missing var ssrBiasPhas.ssrPhaseChs[obsCode] = ssrPhaseCh; if (acsConfig.output_decoded_rtcm_json) @@ -917,33 +925,33 @@ void RtcmDecoder::decodeEphemeris(vector& data) ///< stream data int prn = getbituInc(data, i, 6); eph.weekRollOver = getbituInc(data, i, 12); eph.week = adjGstWeek(eph.weekRollOver) + - 1024; // rolled-over week -> full week number and align to GPST - eph.iode = getbituInc(data, i, 10); - eph.iodc = eph.iode; // Documented as IODnav - eph.sva = getbituInc(data, i, 8); - eph.ura[0] = svaToSisa(eph.sva); // Documented SISA - eph.idot = getbitsInc(data, i, 14) * P2_43 * SC2RAD; - eph.tocs = getbituInc(data, i, 14) * 60.0; - eph.f2 = getbitsInc(data, i, 6) * P2_59; - eph.f1 = getbitsInc(data, i, 21) * P2_46; - eph.f0 = getbitsInc(data, i, 31) * P2_34; - eph.crs = getbitsInc(data, i, 16) * P2_5; - eph.deln = getbitsInc(data, i, 16) * P2_43 * SC2RAD; - eph.M0 = getbitsInc(data, i, 32) * P2_31 * SC2RAD; - eph.cuc = getbitsInc(data, i, 16) * P2_29; - eph.e = getbituInc(data, i, 32) * P2_33; - eph.cus = getbitsInc(data, i, 16) * P2_29; - eph.sqrtA = getbituInc(data, i, 32) * P2_19; - eph.A = SQR(eph.sqrtA); - eph.toes = getbituInc(data, i, 14) * 60.0; - eph.cic = getbitsInc(data, i, 16) * P2_29; - eph.OMG0 = getbitsInc(data, i, 32) * P2_31 * SC2RAD; - eph.cis = getbitsInc(data, i, 16) * P2_29; - eph.i0 = getbitsInc(data, i, 32) * P2_31 * SC2RAD; - eph.crc = getbitsInc(data, i, 16) * P2_5; - eph.omg = getbitsInc(data, i, 32) * P2_31 * SC2RAD; - eph.OMGd = getbitsInc(data, i, 24) * P2_43 * SC2RAD; - eph.tgd[0] = getbitsInc(data, i, 10) * P2_32; + 1024; // rolled-over week -> full week number and align to GPST + eph.iode = getbituInc(data, i, 10); + eph.iodc = eph.iode; // Documented as IODnav + eph.sva = getbituInc(data, i, 8); + eph.ura[0] = svaToSisa(eph.sva); // Documented SISA + eph.idot = getbitsInc(data, i, 14) * P2_43 * SC2RAD; + eph.tocs = getbituInc(data, i, 14) * 60.0; + eph.f2 = getbitsInc(data, i, 6) * P2_59; + eph.f1 = getbitsInc(data, i, 21) * P2_46; + eph.f0 = getbitsInc(data, i, 31) * P2_34; + eph.crs = getbitsInc(data, i, 16) * P2_5; + eph.deln = getbitsInc(data, i, 16) * P2_43 * SC2RAD; + eph.M0 = getbitsInc(data, i, 32) * P2_31 * SC2RAD; + eph.cuc = getbitsInc(data, i, 16) * P2_29; + eph.e = getbituInc(data, i, 32) * P2_33; + eph.cus = getbitsInc(data, i, 16) * P2_29; + eph.sqrtA = getbituInc(data, i, 32) * P2_19; + eph.A = SQR(eph.sqrtA); + eph.toes = getbituInc(data, i, 14) * 60.0; + eph.cic = getbitsInc(data, i, 16) * P2_29; + eph.OMG0 = getbitsInc(data, i, 32) * P2_31 * SC2RAD; + eph.cis = getbitsInc(data, i, 16) * P2_29; + eph.i0 = getbitsInc(data, i, 32) * P2_31 * SC2RAD; + eph.crc = getbitsInc(data, i, 16) * P2_5; + eph.omg = getbitsInc(data, i, 32) * P2_31 * SC2RAD; + eph.OMGd = getbitsInc(data, i, 24) * P2_43 * SC2RAD; + eph.tgd[0] = getbitsInc(data, i, 10) * P2_32; if (messCode == RtcmMessageType::GAL_FNAV_EPHEMERIS) { @@ -1236,6 +1244,8 @@ ObsList RtcmDecoder::decodeMSM(vector& data) int smoothing_indicator = getbituInc(data, i, 1); int smoothing_interval = getbituInc(data, i, 3); + lastReferenceStationId = reference_station_id; + RtcmMessageType messCode; try { @@ -1363,6 +1373,11 @@ ObsList RtcmDecoder::decodeMSM(vector& data) nsat++; } + if (nsat == 0) + { + return obsList; + } + // create a temporary list of signals vector signalMaskList; for (int sig = 0; sig < 32; sig++) @@ -1397,9 +1412,9 @@ ObsList RtcmDecoder::decodeMSM(vector& data) if (code2Freq.find(rtcmsys) != code2Freq.end()) { if (code2Freq[rtcmsys].find(sig.code) != - code2Freq[rtcmsys].end( - )) // must not skip unknwon/unsupported systems or signals in the list of - // signals -- unknown != no observation + code2Freq[rtcmsys] + .end()) // must not skip unknwon/unsupported systems or signals in the list + // of signals -- unknown != no observation { ft = code2Freq[rtcmsys][sig.code]; } @@ -1638,6 +1653,471 @@ void RtcmDecoder::traceLatency(GTime tobs) numMessagesLatency++; } +static void +logRtcmStationInfoDebug(int stationId, const RtcmStationInfo& info, const char* sourceMsg) +{ + BOOST_LOG_TRIVIAL(debug) << "RTCM stationInfo (" << sourceMsg << "): stationId=" << stationId + << " ecef=[" << info.ecefX << "," << info.ecefY << "," << info.ecefZ + << "]" + << " antH=" << info.antennaHeight << " sys[gps,glo,gal,ref]=[" + << info.gpsSys << "," << info.gloSys << "," << info.galSys << "," + << info.refStation << "]" + << " singleOsc=" << info.singleOsc + << " quarterCycle=" << info.quarterCycle << " antDesc='" + << info.antennaDesc << "'" + << " antSetup=" << info.antennaSetupId << " antSerial='" + << info.antennaSerial << "'" + << " recType='" << info.receiverType << "'" + << " recFw='" << info.receiverFirmware << "'" + << " recSerial='" << info.receiverSerial << "'" + << " physId=" << info.physicalStationId << " physEcef=[" + << info.physEcefX << "," << info.physEcefY << "," << info.physEcefZ + << "]"; +} + +static bool decodeStationaryRtkRefArp1005( + vector& message, + map& stationInfoMap +) +{ + constexpr int RTCM1005_BITS = 152; + if ((int)message.size() * 8 < RTCM1005_BITS) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1005 length error: len=" << message.size(); + return false; + } + + int i = 0; + + uint32_t messageNumber = getbituInc(message, i, 12); + uint32_t stationId = getbituInc(message, i, 12); + uint32_t reserved6 = getbituInc(message, i, 6); + uint32_t gpsInd = getbituInc(message, i, 1); + uint32_t gloInd = getbituInc(message, i, 1); + uint32_t galInd = getbituInc(message, i, 1); + uint32_t refStationInd = getbituInc(message, i, 1); + + auto getSigned38 = [&](int& bitPos) -> int64_t + { + uint64_t raw = (uint64_t)getbituInc(message, bitPos, 32); + raw = (raw << 6) | getbituInc(message, bitPos, 6); + + if (raw & (uint64_t(1) << 37)) + { + raw |= (~uint64_t(0) << 38); + } + + return (int64_t)raw; + }; + + double ecefX = getSigned38(i) * 0.0001; + + uint32_t singleReceiverOsc = getbituInc(message, i, 1); + uint32_t reserved1 = getbituInc(message, i, 1); + + double ecefY = getSigned38(i) * 0.0001; + + uint32_t quarterCycleInd = getbituInc(message, i, 2); + + double ecefZ = getSigned38(i) * 0.0001; + + auto& info = stationInfoMap[stationId]; + info.ecefX = ecefX; + info.ecefY = ecefY; + info.ecefZ = ecefZ; + info.hasAntennaHeight = false; + info.gpsSys = gpsInd != 0; + info.gloSys = gloInd != 0; + info.galSys = galInd != 0; + info.refStation = refStationInd != 0; + info.singleOsc = singleReceiverOsc != 0; + info.quarterCycle = (int)quarterCycleInd; + + BOOST_LOG_TRIVIAL(debug) << "RTCM 1005 decoded: stationId=" << stationId << " gps=" << gpsInd + << " glo=" << gloInd << " gal=" << galInd << " ref=" << refStationInd + << " x=" << ecefX << " y=" << ecefY << " z=" << ecefZ + << " singleOsc=" << singleReceiverOsc + << " quarterCycle=" << quarterCycleInd; + logRtcmStationInfoDebug((int)stationId, info, "1005"); + + (void)messageNumber; + (void)reserved1; + (void)reserved6; + + return true; +} + +static bool decodeStationaryRtkRefAprHeight1006( + vector& message, + map& stationInfoMap +) +{ + constexpr int RTCM1006_BITS = 168; + if ((int)message.size() * 8 < RTCM1006_BITS) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1006 length error: len=" << message.size(); + return false; + } + + int i = 0; + + uint32_t messageNumber = getbituInc(message, i, 12); + uint32_t stationId = getbituInc(message, i, 12); + uint32_t reserved6 = getbituInc(message, i, 6); + uint32_t gpsInd = getbituInc(message, i, 1); + uint32_t gloInd = getbituInc(message, i, 1); + uint32_t galInd = getbituInc(message, i, 1); + uint32_t refStationInd = getbituInc(message, i, 1); + + auto getSigned38 = [&](int& bitPos) -> int64_t + { + uint64_t raw = (uint64_t)getbituInc(message, bitPos, 32); + raw = (raw << 6) | getbituInc(message, bitPos, 6); + + if (raw & (uint64_t(1) << 37)) + { + raw |= (~uint64_t(0) << 38); + } + + return (int64_t)raw; + }; + + double ecefX = getSigned38(i) * 0.0001; + + uint32_t singleReceiverOsc = getbituInc(message, i, 1); + uint32_t reserved1 = getbituInc(message, i, 1); + + double ecefY = getSigned38(i) * 0.0001; + + uint32_t quarterCycleInd = getbituInc(message, i, 2); + + double ecefZ = getSigned38(i) * 0.0001; + + uint32_t antennaHeight = getbituInc(message, i, 16); + double antennaHeightMeters = antennaHeight * 0.0001; + + auto& info = stationInfoMap[stationId]; + info.ecefX = ecefX; + info.ecefY = ecefY; + info.ecefZ = ecefZ; + info.antennaHeight = antennaHeightMeters; + info.hasAntennaHeight = true; + info.gpsSys = gpsInd != 0; + info.gloSys = gloInd != 0; + info.galSys = galInd != 0; + info.refStation = refStationInd != 0; + info.singleOsc = singleReceiverOsc != 0; + info.quarterCycle = (int)quarterCycleInd; + + BOOST_LOG_TRIVIAL(debug) << "RTCM 1006 decoded: stationId=" << stationId << " gps=" << gpsInd + << " glo=" << gloInd << " gal=" << galInd << " ref=" << refStationInd + << " x=" << ecefX << " y=" << ecefY << " z=" << ecefZ + << " singleOsc=" << singleReceiverOsc + << " quarterCycle=" << quarterCycleInd + << " height=" << antennaHeightMeters; + logRtcmStationInfoDebug((int)stationId, info, "1006"); + + (void)messageNumber; + (void)reserved1; + (void)reserved6; + + return true; +} + +static bool decodeAntennaDescriptors1007( + vector& message, + map& stationInfoMap +) +{ + constexpr int RTCM1007_MIN_BITS = 40; // 12+12+8+0+8 (minimum with zero-length descriptor) + + int totalBits = (int)message.size() * 8; + if (totalBits < RTCM1007_MIN_BITS) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1007 length error: len=" << message.size(); + return false; + } + + int i = 0; + + uint32_t messageNumber = getbituInc(message, i, 12); + uint32_t stationId = getbituInc(message, i, 12); + uint32_t descriptorLen = getbituInc(message, i, 8); + + // Validate we have enough bits for descriptor + setup ID + int requiredBits = i + (descriptorLen * 8) + 8; + if (totalBits < requiredBits) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1007 descriptor overrun: len=" << message.size() + << " needed=" << (requiredBits / 8); + return false; + } + + // Read ASCII descriptor string + std::string descriptor; + for (uint32_t j = 0; j < descriptorLen; ++j) + { + descriptor += (char)getbituInc(message, i, 8); + } + + uint32_t setupId = getbituInc(message, i, 8); + + auto& info = stationInfoMap[stationId]; + info.antennaDesc = descriptor; + info.antennaSetupId = (int)setupId; + + BOOST_LOG_TRIVIAL(debug) << "RTCM 1007 decoded: stationId=" << stationId << " descriptor='" + << descriptor << "' setupId=" << setupId; + logRtcmStationInfoDebug((int)stationId, info, "1007"); + + (void)messageNumber; + + return true; +} + +static bool decodeAntennaDescriptorsSerial1008( + vector& message, + map& stationInfoMap +) +{ + constexpr int RTCM1008_MIN_BITS = + 56; // 12+12+8+0+8+8+0 (minimum with zero-length descriptor and serial) + + int totalBits = (int)message.size() * 8; + if (totalBits < RTCM1008_MIN_BITS) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1008 length error: len=" << message.size(); + return false; + } + + int i = 0; + + uint32_t messageNumber = getbituInc(message, i, 12); + uint32_t stationId = getbituInc(message, i, 12); + uint32_t descriptorLen = getbituInc(message, i, 8); + + // Validate we have enough bits for descriptor + setup ID + serial count + int requiredBits = i + (descriptorLen * 8) + 8 + 8; + if (totalBits < requiredBits) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1008 descriptor overrun: len=" << message.size() + << " needed=" << (requiredBits / 8); + return false; + } + + // Read ASCII descriptor string + std::string descriptor; + for (uint32_t j = 0; j < descriptorLen; ++j) + { + descriptor += (char)getbituInc(message, i, 8); + } + + uint32_t setupId = getbituInc(message, i, 8); + uint32_t serialNumberLen = getbituInc(message, i, 8); + + // Validate we have enough bits for serial number string + requiredBits = i + (serialNumberLen * 8); + if (totalBits < requiredBits) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1008 serial number overrun: len=" << message.size() + << " needed=" << (requiredBits / 8); + return false; + } + + // Read ASCII serial number string + std::string serialNumber; + for (uint32_t j = 0; j < serialNumberLen; ++j) + { + serialNumber += (char)getbituInc(message, i, 8); + } + + auto& info = stationInfoMap[stationId]; + info.antennaDesc = descriptor; + info.antennaSetupId = (int)setupId; + info.antennaSerial = serialNumber; + + BOOST_LOG_TRIVIAL(debug) << "RTCM 1008 decoded: stationId=" << stationId << " descriptor='" + << descriptor << "' setupId=" << setupId << " serialNumber='" + << serialNumber << "'"; + logRtcmStationInfoDebug((int)stationId, info, "1008"); + + (void)messageNumber; + + return true; +} + +static bool decodeAntennaReceiverDescriptor1033( + vector& message, + map& stationInfoMap +) +{ + constexpr int RTCM1033_MIN_BITS = + 80; // 12+12+8+0+8+8+0+8+0+8+0 (minimum with all zero-length descriptors) + + int totalBits = (int)message.size() * 8; + if (totalBits < RTCM1033_MIN_BITS) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1033 length error: len=" << message.size(); + return false; + } + + int i = 0; + + uint32_t messageNumber = getbituInc(message, i, 12); + uint32_t stationId = getbituInc(message, i, 12); + + // Antenna descriptor + uint32_t antennaDescLen = getbituInc(message, i, 8); + int requiredBits = i + (antennaDescLen * 8) + 8 + 8; + if (totalBits < requiredBits) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1033 antenna descriptor overrun: len=" << message.size(); + return false; + } + + std::string antennaDesc; + for (uint32_t j = 0; j < antennaDescLen; ++j) + { + antennaDesc += (char)getbituInc(message, i, 8); + } + + uint32_t antennaSetupId = getbituInc(message, i, 8); + + // Antenna serial number + uint32_t antennaSerialLen = getbituInc(message, i, 8); + requiredBits = i + (antennaSerialLen * 8) + 8 + 8 + 8; + if (totalBits < requiredBits) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1033 antenna serial overrun: len=" << message.size(); + return false; + } + + std::string antennaSerial; + for (uint32_t j = 0; j < antennaSerialLen; ++j) + { + antennaSerial += (char)getbituInc(message, i, 8); + } + + // Receiver type descriptor + uint32_t receiverTypeLen = getbituInc(message, i, 8); + requiredBits = i + (receiverTypeLen * 8) + 8 + 8; + if (totalBits < requiredBits) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1033 receiver type overrun: len=" << message.size(); + return false; + } + + std::string receiverType; + for (uint32_t j = 0; j < receiverTypeLen; ++j) + { + receiverType += (char)getbituInc(message, i, 8); + } + + // Receiver firmware version + uint32_t receiverFirmwareLen = getbituInc(message, i, 8); + requiredBits = i + (receiverFirmwareLen * 8) + 8; + if (totalBits < requiredBits) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1033 receiver firmware overrun: len=" << message.size(); + return false; + } + + std::string receiverFirmware; + for (uint32_t j = 0; j < receiverFirmwareLen; ++j) + { + receiverFirmware += (char)getbituInc(message, i, 8); + } + + // Receiver serial number + uint32_t receiverSerialLen = getbituInc(message, i, 8); + requiredBits = i + (receiverSerialLen * 8); + if (totalBits < requiredBits) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1033 receiver serial overrun: len=" << message.size(); + return false; + } + + std::string receiverSerial; + for (uint32_t j = 0; j < receiverSerialLen; ++j) + { + receiverSerial += (char)getbituInc(message, i, 8); + } + + auto& info = stationInfoMap[stationId]; + info.antennaDesc = antennaDesc; + info.antennaSetupId = antennaSetupId; + info.antennaSerial = antennaSerial; + info.receiverType = receiverType; + info.receiverFirmware = receiverFirmware; + info.receiverSerial = receiverSerial; + + BOOST_LOG_TRIVIAL(debug) << "RTCM 1033 decoded: stationId=" << stationId << " antenna='" + << antennaDesc << "' antennaSetup=" << antennaSetupId << " antSerial='" + << antennaSerial << "'" + << " receiver='" << receiverType << "' firmware='" << receiverFirmware + << "' serial='" << receiverSerial << "'"; + logRtcmStationInfoDebug((int)stationId, info, "1033"); + + (void)messageNumber; + + return true; +} + +static bool decodePhysicalRefStationPosition1032( + vector& message, + map& stationInfoMap +) +{ + // 1032 layout: msgNum(12) + nonPhysicalStationId(12) + physicalStationId(12) + // + reserved6(6) + ecefX(38) + ecefY(38) + ecefZ(38) = 156 bits + constexpr int RTCM1032_BITS = 156; + if ((int)message.size() * 8 < RTCM1032_BITS) + { + BOOST_LOG_TRIVIAL(error) << "RTCM3 1032 length error: len=" << message.size(); + return false; + } + + int i = 0; + + uint32_t messageNumber = getbituInc(message, i, 12); + uint32_t nonPhysicalStationId = getbituInc(message, i, 12); + uint32_t physicalStationId = getbituInc(message, i, 12); + uint32_t reserved6 = getbituInc(message, i, 6); + + auto getSigned38 = [&](int& bitPos) -> int64_t + { + uint64_t raw = (uint64_t)getbituInc(message, bitPos, 32); + raw = (raw << 6) | getbituInc(message, bitPos, 6); + + if (raw & (uint64_t(1) << 37)) + { + raw |= (~uint64_t(0) << 38); + } + + return (int64_t)raw; + }; + + double ecefX = getSigned38(i) * 0.0001; + double ecefY = getSigned38(i) * 0.0001; + double ecefZ = getSigned38(i) * 0.0001; + + BOOST_LOG_TRIVIAL(debug) << "RTCM 1032 decoded: nonPhysicalStationId=" << nonPhysicalStationId + << " physicalStationId=" << physicalStationId << " x=" << ecefX + << " y=" << ecefY << " z=" << ecefZ; + auto& info = stationInfoMap[nonPhysicalStationId]; + info.physicalStationId = physicalStationId; + info.physEcefX = ecefX; + info.physEcefY = ecefY; + info.physEcefZ = ecefZ; + + logRtcmStationInfoDebug((int)nonPhysicalStationId, info, "1032"); + + (void)messageNumber; + (void)reserved6; + + return true; +} + E_ReturnType RtcmDecoder::decode(vector& message) { E_ReturnType retVal = E_ReturnType::OK; @@ -1645,7 +2125,7 @@ E_ReturnType RtcmDecoder::decode(vector& message) int messageNumber = getbitu(message, 0, 12); RtcmMessageType messCode = messageNumberToRtcmType(messageNumber); - // std::cout << "\n" << "Received " << enum_to_string(messageNumberToRtcmType(messageNumber)); + BOOST_LOG_TRIVIAL(debug) << "Received " << enum_to_string(messCode) << ", decoding ..."; switch (messCode) { @@ -1656,6 +2136,42 @@ E_ReturnType RtcmDecoder::decode(vector& message) case RtcmMessageType::CUSTOM: retVal = decodeCustom(message); break; + case RtcmMessageType::STATIONARY_RTK_REF_ARP: + if (decodeStationaryRtkRefArp1005(message, stationInfoMap) == false) + { + retVal = E_ReturnType::BAD_LENGTH; + } + break; + case RtcmMessageType::STATIONARY_RTK_REF_ARP_HEIGHT: + if (decodeStationaryRtkRefAprHeight1006(message, stationInfoMap) == false) + { + retVal = E_ReturnType::BAD_LENGTH; + } + break; + case RtcmMessageType::ANTENNA_DESCRIPTOR: + if (decodeAntennaDescriptors1007(message, stationInfoMap) == false) + { + retVal = E_ReturnType::BAD_LENGTH; + } + break; + case RtcmMessageType::ANTENNA_DESCRIPTOR_SN: + if (decodeAntennaDescriptorsSerial1008(message, stationInfoMap) == false) + { + retVal = E_ReturnType::BAD_LENGTH; + } + break; + case RtcmMessageType::ANTENNA_RECEIVER_DESCRIPTOR: + if (decodeAntennaReceiverDescriptor1033(message, stationInfoMap) == false) + { + retVal = E_ReturnType::BAD_LENGTH; + } + break; + case RtcmMessageType::PHYSICAL_REF_STATION_POSITION: + if (decodePhysicalRefStationPosition1032(message, stationInfoMap) == false) + { + retVal = E_ReturnType::BAD_LENGTH; + } + break; case RtcmMessageType::GPS_EPHEMERIS: // fallthrough case RtcmMessageType::GLO_EPHEMERIS: // fallthrough case RtcmMessageType::BDS_EPHEMERIS: // fallthrough @@ -1736,12 +2252,23 @@ E_ReturnType RtcmDecoder::decode(vector& message) int i = 54; int multimessage = getbituInc(message, i, 1); + if (obsList.size() > 0) + { + BOOST_LOG_TRIVIAL(debug) << "Parsed " << obsList.size() + << " obs, obsTime=" << obsList.front()->time.to_string(6) + << ", multimessage=" << multimessage; + } + else + { + BOOST_LOG_TRIVIAL(debug) << "No obs parsed, multimessage=" << multimessage; + } + // tracepdeex(0, std::cout, "\n%2d %s %2d %2d ", messageId, // obsList.front()->time.to_string().c_str(), obsList.size(), multimessage); if (superObsList.empty() == false && obsList.empty() == false && fabs((superObsList.front()->time - obsList.front()->time).to_double()) > - DTTOL) // todo aaron ew, fix + DTTOL) // todo? Use epoch tolerance? { // time delta, push the old list and start a new one obsListList.push_back(std::move(superObsList)); @@ -1755,10 +2282,20 @@ E_ReturnType RtcmDecoder::decode(vector& message) if (multimessage == 0) { - obsListList.push_back(std::move(superObsList)); - superObsList.clear(); + if (superObsList.empty() == false) + { + obsListList.push_back(std::move(superObsList)); + superObsList.clear(); - retVal = E_ReturnType::GOT_OBS; + retVal = E_ReturnType::GOT_OBS; + } + else + { + BOOST_LOG_TRIVIAL(info) + << "RTCM decoder reached multimessage flush with empty superObsList" + << ", messageType=" << enum_to_string(messCode) + << ", multimessage=" << multimessage << ", decoded_obs=" << obsList.size(); + } } if (superObsList.size() > 1000) @@ -1794,22 +2331,22 @@ E_ReturnType RtcmDecoder::decode(vector& message) /** extract unsigned bits from byte data */ -unsigned int getbitu( +uint32_t getbitu( const unsigned char* buff, ///< byte data int pos, ///< bit position from start of data (bits) int len ///< bit length (bits) (len<=32) ) { - unsigned int bits = 0; + uint32_t bits = 0; for (int i = pos; i < pos + len; i++) - bits = (bits << 1) + ((buff[i / 8] >> (7 - i % 8)) & 1u); + bits = (bits << 1) + ((buff[i / 8] >> (7 - i % 8)) & uint32_t(1)); return bits; } /** extract unsigned bits from RTCM messages */ -unsigned int getbitu( +uint32_t getbitu( vector& buff, ///< RTCM messages int pos, ///< bit position from start of data (bits) int len ///< bit length (bits) (len<=32) @@ -1820,16 +2357,16 @@ unsigned int getbitu( /** extract signed bits from byte data */ -int getbits( +int32_t getbits( const unsigned char* buff, ///< byte data int pos, ///< bit position from start of data (bits) int len, ///< bit length (bits) (len<=32) bool* failure_ptr ///< pointer for failure flag ) { - unsigned int bits = getbitu(buff, pos, len); + uint32_t bits = getbitu(buff, pos, len); - long int invalid = (1ul << (len - 1)); + uint32_t invalid = (uint32_t(1) << (len - 1)); if (bits == invalid) { @@ -1842,29 +2379,29 @@ int getbits( } } - if (len <= 0 || len >= 32 || !(bits & (1u << (len - 1)))) + if (len <= 0 || len >= 32 || !(bits & (uint32_t(1) << (len - 1)))) { - return (int)bits; + return (int32_t)bits; } - return (int)(bits | (~0u << len)); /* extend sign */ + return (int32_t)(bits | (~uint32_t(0) << len)); /* extend sign */ } /** increasingly extract unsigned bits from byte data */ -unsigned int getbituInc( +uint32_t getbituInc( const unsigned char* buff, ///< byte data int& pos, ///< bit position from start of data (bits) int len ///< bit length (bits) (len<=32) ) { - unsigned int ans = getbitu(buff, pos, len); + uint32_t ans = getbitu(buff, pos, len); pos += len; return ans; } /** increasingly extract unsigned bits from RTCM messages */ -unsigned int getbituInc( +uint32_t getbituInc( vector& buff, ///< byte data int& pos, ///< bit position from start of data (bits) int len ///< bit length (bits) (len<=32) @@ -1875,21 +2412,56 @@ unsigned int getbituInc( /** increasingly extract signed bits from byte data */ -int getbitsInc( +int32_t getbitsInc( const unsigned char* buff, ///< byte data int& pos, ///< bit position from start of data (bits) int len, ///< bit length (bits) (len<=32) bool* failure_ptr ///< pointer for failure flag ) { - int ans = getbits(buff, pos, len, failure_ptr); + int32_t ans = getbits(buff, pos, len, failure_ptr); pos += len; return ans; } +bool getbituIncChecked( + const unsigned char* buff, ///< byte data + int buffBits, + int& pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + uint32_t& out +) +{ + if (len <= 0 || len > 32 || pos < 0 || pos + len > buffBits) + { + return false; + } + + out = getbituInc(buff, pos, len); + return true; +} + +bool getbitsIncChecked( + const unsigned char* buff, ///< byte data + int buffBits, + int& pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + int32_t& out, + bool* failure_ptr ///< pointer for failure flag +) +{ + if (len <= 0 || len > 32 || pos < 0 || pos + len > buffBits) + { + return false; + } + + out = getbitsInc(buff, pos, len, failure_ptr); + return true; +} + /** increasingly extract signed bits from RTCM messages */ -int getbitsInc( +int32_t getbitsInc( vector& buff, ///< byte data int& pos, ///< bit position from start of data (bits) int len, ///< bit length (bits) (len<=32) @@ -1899,6 +2471,27 @@ int getbitsInc( return getbitsInc(buff.data(), pos, len, failure_ptr); } +bool getbituIncChecked( + vector& buff, ///< byte data + int& pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + uint32_t& out +) +{ + return getbituIncChecked(buff.data(), (int)buff.size() * 8, pos, len, out); +} + +bool getbitsIncChecked( + vector& buff, ///< byte data + int& pos, ///< bit position from start of data (bits) + int len, ///< bit length (bits) (len<=32) + int32_t& out, + bool* failure_ptr ///< pointer for failure flag +) +{ + return getbitsIncChecked(buff.data(), (int)buff.size() * 8, pos, len, out, failure_ptr); +} + /** increasingly extract signed bits from RTCM messages with scale factor/resolution applied */ double getbitsIncScale( @@ -1926,32 +2519,32 @@ double getbituIncScale( /** extract sign-magnitude bits applied in GLO nav messages from byte data */ -int getbitg( +int32_t getbitg( const unsigned char* buff, ///< byte data int pos, ///< bit position from start of data (bits) int len ///< bit length (bits) (len<=32) ) { - int value = getbitu(buff, pos + 1, len - 1); + int32_t value = getbitu(buff, pos + 1, len - 1); return getbitu(buff, pos, 1) ? -value : value; } /** increasingly extract sign-magnitude bits applied in GLO nav messages from byte data */ -int getbitgInc( +int32_t getbitgInc( const unsigned char* buff, ///< byte data int& pos, ///< bit position from start of data (bits) int len ///< bit length (bits) (len<=32) ) { - int ans = getbitg(buff, pos, len); + int32_t ans = getbitg(buff, pos, len); pos += len; return ans; } /** increasingly extract sign-magnitude bits applied in GLO nav messages from RTCM messages */ -int getbitgInc( +int32_t getbitgInc( vector& buff, ///< byte data int& pos, ///< bit position from start of data (bits) int len ///< bit length (bits) (len<=32) diff --git a/src/cpp/common/rtcmDecoder.hpp b/src/cpp/common/rtcmDecoder.hpp index 560f6520b..f1112153a 100644 --- a/src/cpp/common/rtcmDecoder.hpp +++ b/src/cpp/common/rtcmDecoder.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "common/acsConfig.hpp" #include "common/enums.h" #include "common/gTime.hpp" @@ -16,12 +18,71 @@ struct SignalInfo E_ObsCode obsCode; }; +struct RtcmStationInfo +{ + // From messages 1005/1006 (Stationary RTK Reference ARP) + double ecefX = 0; ///< Reference station ECEF X (m) + double ecefY = 0; ///< Reference station ECEF Y (m) + double ecefZ = 0; ///< Reference station ECEF Z (m) + double antennaHeight = 0; ///< Antenna height above marker (m), populated by 1006 + bool hasAntennaHeight = false; + bool gpsSys = false; + bool gloSys = false; + bool galSys = false; + bool refStation = false; + bool singleOsc = false; + int quarterCycle = 0; + + // From messages 1007/1008 (Antenna Descriptors) + string antennaDesc; + int antennaSetupId = 0; + string antennaSerial; ///< populated by 1008 + + // From message 1033 (Antenna and Receiver Descriptor) + string receiverType; + string receiverFirmware; + string receiverSerial; + + // From message 1032 (Physical Reference Station Position) + int physicalStationId = -1; ///< -1 if not yet received + double physEcefX = 0; + double physEcefY = 0; + double physEcefZ = 0; +}; + +inline const RtcmStationInfo* selectRtcmStationInfoForMetadata( + const map& stationInfoMap, + int lastReferenceStationId +) +{ + if (lastReferenceStationId >= 0) + { + auto it = stationInfoMap.find(lastReferenceStationId); + if (it != stationInfoMap.end()) + { + return &it->second; + } + + return nullptr; + } + + if (stationInfoMap.size() == 1) + { + return &stationInfoMap.begin()->second; + } + + return nullptr; +} + struct RtcmDecoder : RtcmTrace, ObsLister, PacketStatistics { static double rtcmDeltaTime; ///< Common time used among all rtcmDecoders for delaying decoding ///< when realtime is enabled static map receivedTimeMap; + map stationInfoMap; ///< Station metadata keyed by RTCM station ID + int lastReferenceStationId = -1; + GTime lastTimeStamp; GTime receivedTime; ///< Recent internal time from decoded rtcm messages @@ -61,7 +122,25 @@ struct RtcmDecoder : RtcmTrace, ObsLister, PacketStatistics ObsList decodeMSM(vector& message); - string recordFilename; + string recordFilename; + std::ofstream recordFile; + std::mutex recordFileMutex; + + void setRecordFilename(const string& filename) + { + std::lock_guard lock(recordFileMutex); + + if (filename == recordFilename) + return; + + if (recordFile.is_open()) + { + recordFile.flush(); + recordFile.close(); + } + + recordFilename = filename; + } void recordFrame(vector& data, unsigned int crcRead) { @@ -70,28 +149,43 @@ struct RtcmDecoder : RtcmTrace, ObsLister, PacketStatistics return; } - std::ofstream ofs(recordFilename, std::ofstream::app); + std::lock_guard lock(recordFileMutex); + + if (recordFile.is_open() == false) + { + recordFile.open(recordFilename, std::ios::app | std::ios::binary); + } - if (!ofs) + if (!recordFile) { return; } // Write the custom time stamp message. RtcmEncoder encoder; - encoder.rtcmTraceFilename = rtcmTraceFilename; + setTraceFilename(rtcmTraceFilename); auto buffer = encoder.encodeTimeStampRTCM(); bool write = encoder.encodeWriteMessageToBuffer(buffer); if (write) { - encoder.encodeWriteMessages(ofs); + encoder.encodeWriteMessages(recordFile); } // copy the message to the output file too - ofs.write((char*)data.data(), data.size()); - ofs.write((char*)&crcRead, 3); + recordFile.write((char*)data.data(), data.size()); + recordFile.write((char*)&crcRead, 3); + } + + ~RtcmDecoder() + { + std::lock_guard lock(recordFileMutex); + if (recordFile.is_open()) + { + recordFile.flush(); + recordFile.close(); + } } E_ReturnType decodeCustom(vector& message) @@ -132,7 +226,7 @@ struct RtcmDecoder : RtcmTrace, ObsLister, PacketStatistics break; } - if (1) + if (0) { int& waitingStreams = receivedTimeMap[timeStamp]; @@ -192,21 +286,62 @@ struct RtcmDecoder : RtcmTrace, ObsLister, PacketStatistics E_ReturnType decode(vector& message); }; -unsigned int getbitu(const unsigned char* buff, int pos, int len); -int getbits(const unsigned char* buff, int pos, int len, bool* failure_ptr = nullptr); -unsigned int getbituInc(const unsigned char* buff, int& pos, int len); -int getbitsInc(const unsigned char* buff, int& pos, int len, bool* failure_ptr = nullptr); +uint32_t getbitu(const unsigned char* buff, int pos, int len); +int32_t getbits(const unsigned char* buff, int pos, int len, bool* failure_ptr = nullptr); +uint32_t getbituInc(const unsigned char* buff, int& pos, int len); +int32_t getbitsInc(const unsigned char* buff, int& pos, int len, bool* failure_ptr = nullptr); + +/** Bounds-checked variant of getbituInc(). + * Returns false instead of reading when the requested bit range is outside the + * supplied buffer bounds. Intended for future decoder hardening paths that need + * to reject truncated or malformed RTCM payloads safely. + */ +[[maybe_unused]] bool +getbituIncChecked(const unsigned char* buff, int buffBits, int& pos, int len, uint32_t& out); + +/** Bounds-checked variant of getbitsInc(). + * Returns false instead of reading when the requested bit range is outside the + * supplied buffer bounds. The optional failure pointer is forwarded to the + * signed extractor when the read is valid. + */ +[[maybe_unused]] bool getbitsIncChecked( + const unsigned char* buff, + int buffBits, + int& pos, + int len, + int32_t& out, + bool* failure_ptr = nullptr +); + +int32_t getbitg(const unsigned char* buff, int pos, int len); +int32_t getbitgInc(const unsigned char* buff, int& pos, int len); + +int32_t getbitgInc(vector& buff, int& pos, int len); -int getbitg(const unsigned char* buff, int pos, int len); -int getbitgInc(const unsigned char* buff, int& pos, int len); +uint32_t getbitu(vector& buff, int pos, int len); -int getbitgInc(vector& buff, int& pos, int len); +uint32_t getbituInc(vector& buff, int& pos, int len); -unsigned int getbitu(vector& buff, int pos, int len); +int32_t getbitsInc(vector& buff, int& pos, int len, bool* failure_ptr = nullptr); -unsigned int getbituInc(vector& buff, int& pos, int len); +/** Vector overload of the bounds-checked unsigned incremental bit reader. + * Uses the vector size to derive the valid bit range before delegating to the + * raw-buffer implementation. + */ +[[maybe_unused]] bool +getbituIncChecked(vector& buff, int& pos, int len, uint32_t& out); -int getbitsInc(vector& buff, int& pos, int len, bool* failure_ptr = nullptr); +/** Vector overload of the bounds-checked signed incremental bit reader. + * Uses the vector size to derive the valid bit range before delegating to the + * raw-buffer implementation. + */ +[[maybe_unused]] bool getbitsIncChecked( + vector& buff, + int& pos, + int len, + int32_t& out, + bool* failure_ptr = nullptr +); double getbituIncScale(vector& buff, int& pos, int len, double scale); diff --git a/src/cpp/common/rtcmEncoder.cpp b/src/cpp/common/rtcmEncoder.cpp index 3a40ac112..d7ecb6aa7 100644 --- a/src/cpp/common/rtcmEncoder.cpp +++ b/src/cpp/common/rtcmEncoder.cpp @@ -85,8 +85,7 @@ void calculateSsrComb( uras[i] = ephVarToUra(ssrEphInput.vals[i].ephVar); } - if (acsConfig.ssrOpts - .extrapolate_corrections) // todo Eugene: check if ura can be interpolated + if (acsConfig.ssrOpts.extrapolate_corrections) // todo? check if ura can be interpolated { Vector3d diffRAC[2]; double diffClock[2]; @@ -139,8 +138,8 @@ void calculateSsrComb( } else { - ssrEph.deph = ecef2rac(ssrEphInput.vals[1].brdcPos, ssrEphInput.vals[1].brdcVel) * - posCorrections[1]; + ssrEph.deph = ecef2rac(ssrEphInput.vals[1].brdcPos, ssrEphInput.vals[1].brdcVel) * + posCorrections[1]; ssrClk.dclk[0] = -clkCorrections[1]; ssrUra.ura = uras[1]; tracepdeex( @@ -753,7 +752,7 @@ vector RtcmEncoder::encodeSsrPhase( if (Sat.sys == E_Sys::GPS) { rtcmCode = mCodes_gps.left.at(obsCode); - } // todo aaron, crash heaven, needs else, try + } // todo? crash heaven, needs else, try else if (Sat.sys == E_Sys::GLO) { rtcmCode = mCodes_glo.left.at(obsCode); diff --git a/src/cpp/common/rtcmTrace.cpp b/src/cpp/common/rtcmTrace.cpp index a71fa5d8c..972bfb0c9 100644 --- a/src/cpp/common/rtcmTrace.cpp +++ b/src/cpp/common/rtcmTrace.cpp @@ -86,13 +86,6 @@ void RtcmTrace::traceSsrEph(RtcmMessageType messCode, SatSys Sat, SSREph& ssrEph return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - GTime nearTime = timeGet(); boost::json::object doc; @@ -122,7 +115,8 @@ void RtcmTrace::traceSsrEph(RtcmMessageType messCode, SatSys Sat, SSREph& ssrEph doc["DotDeltaAlongTrack"] = ssrEph.ddeph[1]; doc["DotDeltaCrossTrack"] = ssrEph.ddeph[2]; - fout << boost::json::serialize(doc) << "\n"; + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } void RtcmTrace::traceSsrClk(RtcmMessageType messCode, SatSys Sat, SSRClk& ssrClk) @@ -132,13 +126,6 @@ void RtcmTrace::traceSsrClk(RtcmMessageType messCode, SatSys Sat, SSRClk& ssrClk return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - GTime nearTime = timeGet(); boost::json::object doc; @@ -153,17 +140,19 @@ void RtcmTrace::traceSsrClk(RtcmMessageType messCode, SatSys Sat, SSRClk& ssrClk doc["SSRUpdateIntervalSec"] = ssrClk.udi; doc["SSRUpdateIntervalIndex"] = ssrClk.ssrMeta.updateIntIndex; doc["MultipleMessageIndicator"] = ssrClk.ssrMeta.multipleMessage; - doc["SatReferenceDatum"] = static_cast(ssrClk.ssrMeta.referenceDatum + doc["SatReferenceDatum"] = static_cast( + ssrClk.ssrMeta.referenceDatum ); // 0 = ITRF, 1 = Regional // could be combined corrections - doc["IODSSR"] = ssrClk.iod; - doc["SSRProviderID"] = static_cast(ssrClk.ssrMeta.provider); - doc["SSRSolutionID"] = static_cast(ssrClk.ssrMeta.solution); - doc["Sat"] = Sat.id(); - doc["DeltaClockC0"] = ssrClk.dclk[0]; - doc["DeltaClockC1"] = ssrClk.dclk[1]; - doc["DeltaClockC2"] = ssrClk.dclk[2]; - - fout << boost::json::serialize(doc) << "\n"; + doc["IODSSR"] = ssrClk.iod; + doc["SSRProviderID"] = static_cast(ssrClk.ssrMeta.provider); + doc["SSRSolutionID"] = static_cast(ssrClk.ssrMeta.solution); + doc["Sat"] = Sat.id(); + doc["DeltaClockC0"] = ssrClk.dclk[0]; + doc["DeltaClockC1"] = ssrClk.dclk[1]; + doc["DeltaClockC2"] = ssrClk.dclk[2]; + + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } void RtcmTrace::traceSsrUra(RtcmMessageType messCode, SatSys Sat, SSRUra& ssrUra) @@ -173,13 +162,6 @@ void RtcmTrace::traceSsrUra(RtcmMessageType messCode, SatSys Sat, SSRUra& ssrUra return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - GTime nearTime = timeGet(); boost::json::object doc; @@ -200,7 +182,8 @@ void RtcmTrace::traceSsrUra(RtcmMessageType messCode, SatSys Sat, SSRUra& ssrUra doc["Sat"] = Sat.id(); doc["SSRURA"] = ssrUra.ura; - fout << boost::json::serialize(doc) << "\n"; + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } void RtcmTrace::traceSsrHRClk(RtcmMessageType messCode, SatSys Sat, SSRHRClk& SsrHRClk) @@ -210,13 +193,6 @@ void RtcmTrace::traceSsrHRClk(RtcmMessageType messCode, SatSys Sat, SSRHRClk& Ss return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - GTime nearTime = timeGet(); boost::json::object doc; @@ -237,7 +213,8 @@ void RtcmTrace::traceSsrHRClk(RtcmMessageType messCode, SatSys Sat, SSRHRClk& Ss doc["Sat"] = Sat.id(); doc["HighRateClockCorr"] = SsrHRClk.hrclk; - fout << boost::json::serialize(doc) << "\n"; + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } void RtcmTrace::traceSsrCodeBias( @@ -252,13 +229,6 @@ void RtcmTrace::traceSsrCodeBias( return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - GTime nearTime = timeGet(); boost::json::object doc; @@ -280,7 +250,8 @@ void RtcmTrace::traceSsrCodeBias( doc["Code"] = enum_to_string(code); doc["Bias"] = ssrBias.obsCodeBiasMap[code].bias; - fout << boost::json::serialize(doc) << "\n"; + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } void RtcmTrace::traceSsrPhasBias( @@ -295,13 +266,6 @@ void RtcmTrace::traceSsrPhasBias( return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - GTime nearTime = timeGet(); boost::json::object doc; @@ -330,7 +294,8 @@ void RtcmTrace::traceSsrPhasBias( doc["SignalDiscontinuityCount"] = (int)ssrBias.ssrPhaseChs[code].signalDisconCnt; doc["Bias"] = ssrBias.obsCodeBiasMap[code].bias; - fout << boost::json::serialize(doc) << "\n"; + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } void RtcmTrace::traceTimestamp(GTime time) @@ -340,25 +305,19 @@ void RtcmTrace::traceTimestamp(GTime time) return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - boost::json::object doc; doc["type"] = "timestamp"; doc["Mountpoint"] = rtcmMountpoint; doc["time"] = (string)time; doc["ticks"] = (double)time.bigTime; - fout << boost::json::serialize(doc) << "\n"; + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } /** Write decoded/encoded GPS/GAL/BDS/QZS ephemeris messages to a json file */ -void RtcmTrace::traceBrdcEph( // todo aaron, template this for gps/glo? +void RtcmTrace::traceBrdcEph( // todo? template this for gps/glo? RtcmMessageType messCode, Eph& eph ) @@ -368,13 +327,6 @@ void RtcmTrace::traceBrdcEph( // todo aaron, template this for gps/glo? return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - boost::json::object doc; GTime nearTime = timeGet(); @@ -392,7 +344,8 @@ void RtcmTrace::traceBrdcEph( // todo aaron, template this for gps/glo? traceBrdcEphBody(doc, eph); - fout << boost::json::serialize(doc) << "\n"; + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } /** Write decoded/encoded GAL ephemeris messages to a json file @@ -404,13 +357,6 @@ void RtcmTrace::traceBrdcEph(RtcmMessageType messCode, Geph& geph) return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - boost::json::object doc; GTime nearTime = timeGet(); @@ -429,7 +375,8 @@ void RtcmTrace::traceBrdcEph(RtcmMessageType messCode, Geph& geph) traceBrdcEphBody(doc, geph); - fout << boost::json::serialize(doc) << "\n"; + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } void traceBrdcEphBody(boost::json::object& doc, Eph& eph) @@ -651,13 +598,6 @@ void RtcmTrace::traceMSM(RtcmMessageType messCode, GTime time, SatSys Sat, Sig& return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - GTime nearTime = timeGet(); boost::json::object doc; @@ -676,7 +616,8 @@ void RtcmTrace::traceMSM(RtcmMessageType messCode, GTime time, SatSys Sat, Sig& doc["LLI"] = sig.LLI; doc["IsInvalid"] = sig.invalid; - fout << boost::json::serialize(doc) << "\n"; + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } /** Write unknown message to a json file @@ -688,15 +629,9 @@ void RtcmTrace::traceUnknown() return; } - std::ofstream fout(rtcmTraceFilename, std::ios::app); - if (!fout) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - boost::json::object doc; doc["type"] = "?"; - fout << boost::json::serialize(doc) << "\n"; + rtcmTraceFile << boost::json::serialize(doc) << "\n"; + flush(); } diff --git a/src/cpp/common/rtcmTrace.hpp b/src/cpp/common/rtcmTrace.hpp index 1877d9fd4..123d73c2c 100644 --- a/src/cpp/common/rtcmTrace.hpp +++ b/src/cpp/common/rtcmTrace.hpp @@ -23,41 +23,80 @@ struct SSRPhasBias; struct RtcmTrace { - string rtcmTraceFilename = ""; - string rtcmMountpoint; - bool qzssL6 = false; + string rtcmTraceFilename = ""; + std::ofstream rtcmTraceFile; + string rtcmMountpoint; + bool qzssL6 = false; RtcmTrace(string mountpoint = "", string filename = "") : rtcmTraceFilename{filename}, rtcmMountpoint{mountpoint} { + if (rtcmTraceFilename.empty()) + { + return; + } + rtcmTraceFile.open(rtcmTraceFilename, std::ios::app); + if (!rtcmTraceFile) + { + std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; + } + std::cout << "opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; } - void networkLog(string message) + void openTraceFile() { - std::ofstream outStream(rtcmTraceFilename, std::iostream::app); - if (!outStream) + if (rtcmTraceFilename.empty()) + return; + + if (rtcmTraceFile.is_open()) + { + rtcmTraceFile.flush(); + rtcmTraceFile.close(); + } + + rtcmTraceFile.open(rtcmTraceFilename, std::ios::app); + if (!rtcmTraceFile) { std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; + } + } + + void setTraceFilename(const string& filename) + { + if (filename == rtcmTraceFilename && rtcmTraceFile.is_open()) return; + + rtcmTraceFilename = filename; + openTraceFile(); + } + + ~RtcmTrace() + { + if (rtcmTraceFile.is_open()) + { + rtcmTraceFile.flush(); + rtcmTraceFile.close(); } + } + + void flush() + { + if (rtcmTraceFile.is_open()) + rtcmTraceFile.flush(); + } - outStream << timeGet(); - outStream << " " << __FUNCTION__ << message << "\n"; + void networkLog(string message) + { + rtcmTraceFile << timeGet(); + rtcmTraceFile << " " << __FUNCTION__ << message << "\n"; } void messageChunkLog(string message) {} void messageRtcmLog(string message) { - std::ofstream outStream(rtcmTraceFilename, std::ios::app); - if (!outStream) - { - std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << "\n"; - return; - } - - outStream << timeGet(); - outStream << " messageRtcmLog" << message << "\n"; + rtcmTraceFile << timeGet(); + rtcmTraceFile << " messageRtcmLog" << message << "\n"; } void traceSsrEph(RtcmMessageType messCode, SatSys Sat, SSREph& ssrEph); diff --git a/src/cpp/common/rtsSmoothing.cpp b/src/cpp/common/rtsSmoothing.cpp index b0180b093..6ba542e8b 100644 --- a/src/cpp/common/rtsSmoothing.cpp +++ b/src/cpp/common/rtsSmoothing.cpp @@ -3,10 +3,12 @@ #include #include #include +#include #include "architectureDocs.hpp" #include "common/acsConfig.hpp" #include "common/algebra.hpp" #include "common/algebraTrace.hpp" +#include "common/blasThreading.hpp" #include "common/constants.hpp" #include "common/eigenIncluder.hpp" #include "common/lapackWrapper.hpp" @@ -424,9 +426,7 @@ void RtsTimingLogger::logEpochTiming( GTime epochStopTime ) { - auto boostTime = formatTimeForLogging(filterData.kalmanPlus.time); - - BOOST_LOG_TRIVIAL(info) << "Processed epoch" << " - " << boostTime << " (took " + BOOST_LOG_TRIVIAL(info) << "Processed epoch" << " - " << filterData.kalmanPlus.time << " (took " << (epochStopTime - epochStartTime) << ")"; updateTerminalProgress(filterData, epochStartTime, epochStopTime); @@ -434,15 +434,6 @@ void RtsTimingLogger::logEpochTiming( epochStartTime = timeGet(); } -/** Format time for logging output */ -boost::posix_time::ptime RtsTimingLogger::formatTimeForLogging(const GTime& time) -{ - int fractionalMilliseconds = calculateFractionalMilliseconds(time); - - return boost::posix_time::from_time_t((time_t)((PTime)time).bigTime) + - boost::posix_time::millisec(fractionalMilliseconds); -} - /** Update interactive terminal with progress information */ void RtsTimingLogger::updateTerminalProgress( const FilterData& filterData, @@ -453,12 +444,6 @@ void RtsTimingLogger::updateTerminalProgress( // todo: function to delete? } -/** Calculate fractional milliseconds from time */ -int RtsTimingLogger::calculateFractionalMilliseconds(const GTime& time) -{ - return (time.bigTime - (long int)time.bigTime) * 1000; -} - //================================================================================ // FilterData Mathematical Computation //================================================================================ @@ -546,7 +531,7 @@ bool FilterData::performRtsComputation(KFState& kfState, const RtsConfiguration& { for (int j = i + 1; j < n; j++) { - double avg = (smoothedKF.P(i, j) + smoothedKF.P(j, i)) * 0.5; + double avg = (smoothedKF.P(i, j) + smoothedKF.P(j, i)) * 0.5; smoothedKF.P(i, j) = avg; smoothedKF.P(j, i) = avg; } @@ -566,27 +551,45 @@ bool FilterData::performRtsComputation(KFState& kfState, const RtsConfiguration& VectorXd deltaX = VectorXd::Zero(kalmanPlus.x.rows()); MatrixXd deltaP = MatrixXd::Zero(kalmanPlus.P.rows(), kalmanPlus.P.cols()); - // Pre-allocate temporary matrices for reuse across chunks - MatrixXd temp; - int maxChunkSize = 0; - map filterChunks; for (auto& [id, fcP] : kalmanPlus.filterChunkMap) filterChunks[id] = true; for (auto& [id, fcM] : kalmanMinus.filterChunkMap) filterChunks[id] = true; + struct RtsChunkWork + { + string id; + int plusBegX = 0; + int plusNumX = 0; + int minusBegX = 0; + int minusNumX = 0; + }; + + vector chunkWorkList; + chunkWorkList.reserve(filterChunks.size()); for (auto& [id, dummy] : filterChunks) { - auto& fcP = kalmanPlus.filterChunkMap[id]; - auto& fcM = kalmanMinus.filterChunkMap[id]; + auto fcP_it = kalmanPlus.filterChunkMap.find(id); + auto fcM_it = kalmanMinus.filterChunkMap.find(id); - if (fcP.begX == 0) + FilterChunk fcP; + FilterChunk fcM; + if (fcP_it != kalmanPlus.filterChunkMap.end()) + { + fcP = fcP_it->second; + } + if (fcM_it != kalmanMinus.filterChunkMap.end()) + { + fcM = fcM_it->second; + } + + if (fcP.begX == 0 && fcP.numX > 0) { fcP.begX = 1; fcP.numX -= 1; } - if (fcM.begX == 0) + if (fcM.begX == 0 && fcM.numX > 0) { fcM.begX = 1; fcM.numX -= 1; @@ -598,24 +601,38 @@ bool FilterData::performRtsComputation(KFState& kfState, const RtsConfiguration& continue; } - int n = fcM.numX; - int neqs = fcP.numX; + chunkWorkList.push_back({id, fcP.begX, fcP.numX, fcM.begX, fcM.numX}); + } + + bool parallelChunks = chunkWorkList.size() > 1; + BlasThreading::ScopedOpenBlasThreadLimit openblasThreadLimit(parallelChunks ? 1 : 0); +#ifdef ENABLE_PARALLELISATION +#pragma omp parallel for schedule(dynamic) if (parallelChunks) +#endif + for (int c = 0; c < (int)chunkWorkList.size(); c++) + { + auto& work = chunkWorkList[c]; + + int n = work.minusNumX; + int neqs = work.plusNumX; // Copy Q block and add regularization (needed for in-place solving) - MatrixXd Q = kalmanMinus.P.block(fcM.begX, fcM.begX, n, n); + MatrixXd Q = kalmanMinus.P.block(work.minusBegX, work.minusBegX, n, n); Q += MatrixXd::Identity(n, n) * config.regularisation; // Copy FP block for solving (will be overwritten by solution) - MatrixXd FP_solved = FP.block(fcM.begX, fcP.begX, n, neqs); + MatrixXd FP_solved = FP.block(work.minusBegX, work.plusBegX, n, neqs); solveSystem(n, neqs, Q.data(), FP_solved.data()); // Get pointers to blocks for direct LAPACK operations - double* pDeltaX = deltaX.data() + fcP.begX; - double* pSmoothedX = smoothedKF.x.data() + fcM.begX; - double* pXMinus = kalmanMinus.x.data() + fcM.begX; - double* pSmoothedP = smoothedKF.P.data() + fcM.begX * smoothedKF.P.rows() + fcM.begX; - double* pMinusP = kalmanMinus.P.data() + fcM.begX * kalmanMinus.P.rows() + fcM.begX; - double* pDeltaP = deltaP.data() + fcP.begX * deltaP.rows() + fcP.begX; + double* pDeltaX = deltaX.data() + work.plusBegX; + double* pSmoothedX = smoothedKF.x.data() + work.minusBegX; + double* pXMinus = kalmanMinus.x.data() + work.minusBegX; + double* pSmoothedP = + smoothedKF.P.data() + work.minusBegX * smoothedKF.P.rows() + work.minusBegX; + double* pMinusP = + kalmanMinus.P.data() + work.minusBegX * kalmanMinus.P.rows() + work.minusBegX; + double* pDeltaP = deltaP.data() + work.plusBegX * deltaP.rows() + work.plusBegX; int ldSmoothedP = smoothedKF.P.rows(); int ldMinusP = kalmanMinus.P.rows(); @@ -633,90 +650,86 @@ bool FilterData::performRtsComputation(KFState& kfState, const RtsConfiguration& LapackWrapper::dgemv( LapackWrapper::COL_MAJOR, LapackWrapper::CblasTrans, - n, // rows of FP_solved - neqs, // cols of FP_solved - 1.0, // alpha - FP_solved.data(), // matrix A - n, // leading dimension of A - xChanged.data(), // vector x - 1, // stride of x - 0.0, // beta (overwrite, not accumulate) - pDeltaX, // vector y - 1 // stride of y + n, // rows of FP_solved + neqs, // cols of FP_solved + 1.0, // alpha + FP_solved.data(), // matrix A + n, // leading dimension of A + xChanged.data(), // vector x + 1, // stride of x + 0.0, // beta (overwrite, not accumulate) + pDeltaX, // vector y + 1 // stride of y ); - // Resize temporary matrix if needed (reuse across chunks) - if (temp.rows() != neqs || temp.cols() != n) - { - temp.resize(neqs, n); - } + MatrixXd temp(neqs, n); // Compute: temp = FP_solved^T * (smoothedP - minusP) // Note: Could use dsymm since P matrices are symmetric, but we need the transpose // operation FP_solved^T which dsymm doesn't directly support, so dgemm is clearer - + // Step 1: temp = FP_solved^T * smoothedP LapackWrapper::dgemm( LapackWrapper::COL_MAJOR, - LapackWrapper::CblasTrans, // transpose FP_solved - LapackWrapper::CblasNoTrans, // don't transpose smoothedP - neqs, // rows of result - n, // cols of result - n, // inner dimension - 1.0, // alpha - FP_solved.data(), // A - n, // leading dim of A - pSmoothedP, // B (smoothed P block) - ldSmoothedP, // leading dim of B - 0.0, // beta - temp.data(), // C - neqs // leading dim of C + LapackWrapper::CblasTrans, // transpose FP_solved + LapackWrapper::CblasNoTrans, // don't transpose smoothedP + neqs, // rows of result + n, // cols of result + n, // inner dimension + 1.0, // alpha + FP_solved.data(), // A + n, // leading dim of A + pSmoothedP, // B (smoothed P block) + ldSmoothedP, // leading dim of B + 0.0, // beta + temp.data(), // C + neqs // leading dim of C ); // Step 2: temp -= FP_solved^T * minusP LapackWrapper::dgemm( LapackWrapper::COL_MAJOR, - LapackWrapper::CblasTrans, // transpose FP_solved - LapackWrapper::CblasNoTrans, // don't transpose minusP - neqs, // rows of result - n, // cols of result - n, // inner dimension - -1.0, // alpha (subtract) - FP_solved.data(), // A - n, // leading dim of A - pMinusP, // B (minus P block) - ldMinusP, // leading dim of B - 1.0, // beta (accumulate) - temp.data(), // C - neqs // leading dim of C + LapackWrapper::CblasTrans, // transpose FP_solved + LapackWrapper::CblasNoTrans, // don't transpose minusP + neqs, // rows of result + n, // cols of result + n, // inner dimension + -1.0, // alpha (subtract) + FP_solved.data(), // A + n, // leading dim of A + pMinusP, // B (minus P block) + ldMinusP, // leading dim of B + 1.0, // beta (accumulate) + temp.data(), // C + neqs // leading dim of C ); // Final step: deltaP += temp * FP_solved LapackWrapper::dgemm( LapackWrapper::COL_MAJOR, - LapackWrapper::CblasNoTrans, // don't transpose temp - LapackWrapper::CblasNoTrans, // don't transpose FP_solved - neqs, // rows of result - neqs, // cols of result - n, // inner dimension - 1.0, // alpha - temp.data(), // A - neqs, // leading dim of A - FP_solved.data(), // B - n, // leading dim of B - 0.0, // beta (overwrite) - pDeltaP, // C (deltaP block) - ldDeltaP // leading dim of parent matrix + LapackWrapper::CblasNoTrans, // don't transpose temp + LapackWrapper::CblasNoTrans, // don't transpose FP_solved + neqs, // rows of result + neqs, // cols of result + n, // inner dimension + 1.0, // alpha + temp.data(), // A + neqs, // leading dim of A + FP_solved.data(), // B + n, // leading dim of B + 0.0, // beta (overwrite) + pDeltaP, // C (deltaP block) + ldDeltaP // leading dim of parent matrix ); } smoothedKF.dx = deltaX; - + // Use BLAS for vector/matrix additions for better performance smoothedKF.x = kalmanPlus.x; LapackWrapper::daxpy(deltaX.size(), 1.0, deltaX.data(), 1, smoothedKF.x.data(), 1); - - smoothedKF.P = kalmanPlus.P; + + smoothedKF.P = kalmanPlus.P; int totalSize = kalmanPlus.P.rows() * kalmanPlus.P.cols(); LapackWrapper::daxpy(totalSize, 1.0, deltaP.data(), 1, smoothedKF.P.data(), 1); @@ -1103,10 +1116,10 @@ void rtsSmoothing( std::fstream inputStream(inputFile, std::ifstream::binary | std::ifstream::in); inputStream.seekg(0, inputStream.end); - long int lengthPos = inputStream.tellg(); - long int currentPos = reader.getCurrentPosition(); + std::streamoff lengthPos = inputStream.tellg(); + std::streamoff currentPos = reader.getCurrentPosition(); - vector fileContents(lengthPos - currentPos); + vector fileContents(static_cast(lengthPos - currentPos)); inputStream.seekg(currentPos, inputStream.beg); diff --git a/src/cpp/common/rtsSmoothing.hpp b/src/cpp/common/rtsSmoothing.hpp index 2fa9c322d..216f619e6 100644 --- a/src/cpp/common/rtsSmoothing.hpp +++ b/src/cpp/common/rtsSmoothing.hpp @@ -69,8 +69,8 @@ struct FilterData class RtsFileReader { private: - string inputFile; - long int currentPosition = -1; + string inputFile; + std::streamoff currentPosition = -1; // Data presence flags for current epoch bool hasMetadata = false; @@ -93,7 +93,7 @@ class RtsFileReader void resetEpochFlags(); /** Get current file position */ - long int getCurrentPosition() const { return currentPosition; } + std::streamoff getCurrentPosition() const { return currentPosition; } /** Check if we've reached the beginning of file */ bool isAtBeginning() const { return currentPosition == 0; } @@ -179,8 +179,8 @@ class RtsTimingLogger class RtsOutputFileReader { private: - string reversedStatesFilename; - long int currentPosition = -1; + string reversedStatesFilename; + std::streamoff currentPosition = -1; public: /** Constructor */ diff --git a/src/cpp/common/sanityCheckers/ConfigSanityManager.cpp b/src/cpp/common/sanityCheckers/ConfigSanityManager.cpp new file mode 100644 index 000000000..14f88028e --- /dev/null +++ b/src/cpp/common/sanityCheckers/ConfigSanityManager.cpp @@ -0,0 +1,69 @@ +#include "common/sanityCheckers/ConfigSanityManager.hpp" +#include +#include "common/sanityCheckers/EphemerisTimeDelayChecker.hpp" +#include "common/sanityCheckers/EpochToleranceChecker.hpp" +#include "common/sanityCheckers/IonosphericFreeComboChecker.hpp" +#include "common/sanityCheckers/IonosphericOutageChecker.hpp" +#include "common/sanityCheckers/RequiredSiteEccentricityChecker.hpp" +#include "common/sanityCheckers/SbasSanityChecker.hpp" + +void ConfigSanityManager::addChecker(std::unique_ptr checker) +{ + if (checker) + { + checkers.push_back(std::move(checker)); + } +} + +bool ConfigSanityManager::runAllChecks(ACSConfig& config) const +{ + bool allPassed = true; + + for (auto& checker : checkers) + { + try + { + allPassed &= checker->check(config); + } + catch (const std::exception& e) + { + allPassed = false; + BOOST_LOG_TRIVIAL(error) << "Exception in configuration sanity checker " + << checker->name() << ": " << e.what(); + } + } + + return allPassed; +} + +size_t ConfigSanityManager::checkerCount() const +{ + return checkers.size(); +} + +std::vector ConfigSanityManager::checkerNames() const +{ + std::vector names; + names.reserve(checkers.size()); + + for (auto& checker : checkers) + { + names.push_back(checker->name()); + } + + return names; +} + +ConfigSanityManager ConfigSanityManager::defaultManager() +{ + ConfigSanityManager manager; + + manager.addChecker(std::make_unique()); + manager.addChecker(std::make_unique()); + manager.addChecker(std::make_unique()); + manager.addChecker(std::make_unique()); + manager.addChecker(std::make_unique()); + manager.addChecker(std::make_unique()); + + return manager; +} diff --git a/src/cpp/common/sanityCheckers/ConfigSanityManager.hpp b/src/cpp/common/sanityCheckers/ConfigSanityManager.hpp new file mode 100644 index 000000000..987da1d5b --- /dev/null +++ b/src/cpp/common/sanityCheckers/ConfigSanityManager.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include "common/sanityCheckers/ISanityChecker.hpp" + +struct ConfigSanityManager +{ + void addChecker(std::unique_ptr checker); + bool runAllChecks(ACSConfig& config) const; + + size_t checkerCount() const; + std::vector checkerNames() const; + + static ConfigSanityManager defaultManager(); + + private: + std::vector> checkers; +}; diff --git a/src/cpp/common/sanityCheckers/EphemerisTimeDelayChecker.cpp b/src/cpp/common/sanityCheckers/EphemerisTimeDelayChecker.cpp new file mode 100644 index 000000000..96de5a123 --- /dev/null +++ b/src/cpp/common/sanityCheckers/EphemerisTimeDelayChecker.cpp @@ -0,0 +1,22 @@ +#include "common/sanityCheckers/EphemerisTimeDelayChecker.hpp" +#include "common/acsConfig.hpp" + +bool EphemerisTimeDelayChecker::check(ACSConfig& config) +{ + if (config.simulate_real_time) + { + return true; + } + + for (E_Sys sys : magic_enum::enum_values()) + { + config.eph_time_delay[sys] = config.default_eph_time_delay[sys]; + } + + return true; +} + +std::string EphemerisTimeDelayChecker::name() const +{ + return "EphemerisTimeDelayChecker"; +} diff --git a/src/cpp/common/sanityCheckers/EphemerisTimeDelayChecker.hpp b/src/cpp/common/sanityCheckers/EphemerisTimeDelayChecker.hpp new file mode 100644 index 000000000..296085b82 --- /dev/null +++ b/src/cpp/common/sanityCheckers/EphemerisTimeDelayChecker.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "common/sanityCheckers/ISanityChecker.hpp" + +struct EphemerisTimeDelayChecker : ISanityChecker +{ + bool check(ACSConfig& config) override; + std::string name() const override; +}; diff --git a/src/cpp/common/sanityCheckers/EpochToleranceChecker.cpp b/src/cpp/common/sanityCheckers/EpochToleranceChecker.cpp new file mode 100644 index 000000000..260954728 --- /dev/null +++ b/src/cpp/common/sanityCheckers/EpochToleranceChecker.cpp @@ -0,0 +1,22 @@ +#include "common/sanityCheckers/EpochToleranceChecker.hpp" +#include +#include "common/acsConfig.hpp" + +bool EpochToleranceChecker::check(ACSConfig& config) +{ + if (config.epoch_tolerance <= config.epoch_interval / 2) + { + return true; + } + + BOOST_LOG_TRIVIAL(warning) << "`epoch_tolerance` should not exceed half of " + "`epoch_interval`, setting it to `epoch_interval / 2`"; + config.epoch_tolerance = config.epoch_interval / 2; + + return false; +} + +std::string EpochToleranceChecker::name() const +{ + return "EpochToleranceChecker"; +} diff --git a/src/cpp/common/sanityCheckers/EpochToleranceChecker.hpp b/src/cpp/common/sanityCheckers/EpochToleranceChecker.hpp new file mode 100644 index 000000000..c44dcb2fd --- /dev/null +++ b/src/cpp/common/sanityCheckers/EpochToleranceChecker.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "common/sanityCheckers/ISanityChecker.hpp" + +struct EpochToleranceChecker : ISanityChecker +{ + bool check(ACSConfig& config) override; + std::string name() const override; +}; diff --git a/src/cpp/common/sanityCheckers/ISanityChecker.hpp b/src/cpp/common/sanityCheckers/ISanityChecker.hpp new file mode 100644 index 000000000..b2cf17483 --- /dev/null +++ b/src/cpp/common/sanityCheckers/ISanityChecker.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +struct ACSConfig; + +struct ISanityChecker +{ + virtual ~ISanityChecker() = default; + + virtual bool check(ACSConfig& config) = 0; + virtual std::string name() const = 0; +}; diff --git a/src/cpp/common/sanityCheckers/IonosphericFreeComboChecker.cpp b/src/cpp/common/sanityCheckers/IonosphericFreeComboChecker.cpp new file mode 100644 index 000000000..0588b5056 --- /dev/null +++ b/src/cpp/common/sanityCheckers/IonosphericFreeComboChecker.cpp @@ -0,0 +1,43 @@ +#include "common/sanityCheckers/IonosphericFreeComboChecker.hpp" +#include +#include "common/acsConfig.hpp" + +bool IonosphericFreeComboChecker::check(ACSConfig& config) +{ + if (config.pppOpts.ionoOpts.use_if_combo == false) + { + return true; + } + + bool valid = true; + + for (auto& [id, recOpts] : config.recOptsMap) + { + if (recOpts.ionospheric_component2) + { + valid = false; + setOption(recOpts, recOpts.ionospheric_component2, false); + BOOST_LOG_TRIVIAL(warning) + << "Higher-order ionospheric corrections are not supported when " + "use_if_combo is enabled, " + "setting ionospheric_components:use_2nd_order to false"; + } + + if (recOpts.ionospheric_component3) + { + valid = false; + setOption(recOpts, recOpts.ionospheric_component3, false); + BOOST_LOG_TRIVIAL(warning) + << "Higher-order ionospheric corrections are not supported when " + "use_if_combo is enabled, " + "setting ionospheric_components:use_3rd_order to false"; + } + } + + return valid; +} + +std::string IonosphericFreeComboChecker::name() const +{ + return "IonosphericFreeComboChecker"; +} diff --git a/src/cpp/common/sanityCheckers/IonosphericFreeComboChecker.hpp b/src/cpp/common/sanityCheckers/IonosphericFreeComboChecker.hpp new file mode 100644 index 000000000..57182f23b --- /dev/null +++ b/src/cpp/common/sanityCheckers/IonosphericFreeComboChecker.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "common/sanityCheckers/ISanityChecker.hpp" + +struct IonosphericFreeComboChecker : ISanityChecker +{ + bool check(ACSConfig& config) override; + std::string name() const override; +}; diff --git a/src/cpp/common/sanityCheckers/IonosphericOutageChecker.cpp b/src/cpp/common/sanityCheckers/IonosphericOutageChecker.cpp new file mode 100644 index 000000000..d46149944 --- /dev/null +++ b/src/cpp/common/sanityCheckers/IonosphericOutageChecker.cpp @@ -0,0 +1,20 @@ +#include "common/sanityCheckers/IonosphericOutageChecker.hpp" +#include +#include "common/acsConfig.hpp" + +bool IonosphericOutageChecker::check(ACSConfig& config) +{ + if (config.ionErrors.outage_reset_limit >= config.epoch_interval) + { + return true; + } + + BOOST_LOG_TRIVIAL(warning) << "ionospheric_components:outage_reset_limit < " + "epoch_interval, but it probably shouldnt be"; + return false; +} + +std::string IonosphericOutageChecker::name() const +{ + return "IonosphericOutageChecker"; +} diff --git a/src/cpp/common/sanityCheckers/IonosphericOutageChecker.hpp b/src/cpp/common/sanityCheckers/IonosphericOutageChecker.hpp new file mode 100644 index 000000000..a7c45d6a4 --- /dev/null +++ b/src/cpp/common/sanityCheckers/IonosphericOutageChecker.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "common/sanityCheckers/ISanityChecker.hpp" + +struct IonosphericOutageChecker : ISanityChecker +{ + bool check(ACSConfig& config) override; + std::string name() const override; +}; diff --git a/src/cpp/common/sanityCheckers/RequiredSiteEccentricityChecker.cpp b/src/cpp/common/sanityCheckers/RequiredSiteEccentricityChecker.cpp new file mode 100644 index 000000000..5df207161 --- /dev/null +++ b/src/cpp/common/sanityCheckers/RequiredSiteEccentricityChecker.cpp @@ -0,0 +1,33 @@ +#include "common/sanityCheckers/RequiredSiteEccentricityChecker.hpp" +#include +#include "common/acsConfig.hpp" + +bool RequiredSiteEccentricityChecker::check(ACSConfig& config) +{ + if (config.require_site_eccentricity == false) + { + return true; + } + + bool valid = true; + + for (auto& [id, recOpts] : config.recOptsMap) + { + if (recOpts.eccentricityModel.enable) + { + continue; + } + + valid = false; + setOption(recOpts, recOpts.eccentricityModel.enable, true); + BOOST_LOG_TRIVIAL(warning) << "Site eccentricity is required but `" << id + << ": models: eccentricity` is not enabled, setting it to true"; + } + + return valid; +} + +std::string RequiredSiteEccentricityChecker::name() const +{ + return "RequiredSiteEccentricityChecker"; +} diff --git a/src/cpp/common/sanityCheckers/RequiredSiteEccentricityChecker.hpp b/src/cpp/common/sanityCheckers/RequiredSiteEccentricityChecker.hpp new file mode 100644 index 000000000..5ea0c19eb --- /dev/null +++ b/src/cpp/common/sanityCheckers/RequiredSiteEccentricityChecker.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "common/sanityCheckers/ISanityChecker.hpp" + +struct RequiredSiteEccentricityChecker : ISanityChecker +{ + bool check(ACSConfig& config) override; + std::string name() const override; +}; diff --git a/src/cpp/common/sanityCheckers/SbasSanityChecker.cpp b/src/cpp/common/sanityCheckers/SbasSanityChecker.cpp new file mode 100644 index 000000000..91a2868ef --- /dev/null +++ b/src/cpp/common/sanityCheckers/SbasSanityChecker.cpp @@ -0,0 +1,221 @@ +#include "common/sanityCheckers/SbasSanityChecker.hpp" +#include +#include "common/acsConfig.hpp" + +bool SbasSanityChecker::check(ACSConfig& config) +{ + bool valid = true; + + if (config.sbsInOpts.freq == 1) + { + if (config.sbsInOpts.use_do259) + { + valid = false; + config.sbsInOpts.use_do259 = false; + BOOST_LOG_TRIVIAL(warning) + << "DO-259 is not to be used with L1 SBAS. Setting use_do259 to false"; + } + + if (config.sbsInOpts.pvs_on_dfmc) + { + valid = false; + config.sbsInOpts.pvs_on_dfmc = false; + BOOST_LOG_TRIVIAL(warning) + << "PVS on DFMC is not to be used with L1 SBAS. Setting pvs_on_dfmc to false"; + } + } + + if (config.process_sbas == false) + { + return valid; + } + + config.process_preprocessor = true; + config.process_spp = true; + + config.used_nav_types = config.sbsOpts.sbas_nav_types; + + for (auto& [id, satOpts] : config.satOptsMap) + { + vector sources = {E_Source::SBAS}; + setOption((CommonOptions&)satOpts, satOpts.posModel.enable, true); + setOption((CommonOptions&)satOpts, satOpts.posModel.sources, sources); + setOption((CommonOptions&)satOpts, satOpts.clockModel.enable, true); + setOption((CommonOptions&)satOpts, satOpts.clockModel.sources, sources); + } + + switch (config.sbsOpts.mode) + { + case E_SbasMode::L1: + { + BOOST_LOG_TRIVIAL(info) + << "L1 SBAS processing mode is selected, make sure that:\n" + " - You have inputs containing SBAS messages (sisnet, ems, sbf, etc.)\n" + " - Parameter `sbas_inputs: prec_approach` is set appropriately"; + + config.sbsInOpts.freq = 1; + + for (auto& [sys, process] : config.process_sys) + { + if (sys != E_Sys::GPS && sys != E_Sys::GLO && sys != E_Sys::SBS) + { + process = false; + } + else + { + config.code_priorities[sys] = {E_ObsCode::L1C}; + } + } + + config.sppOpts.trop_models = {E_TropModel::SBAS}; + config.sppOpts.iono_mode = E_IonoMode::SBAS; + + if (config.sppOpts.smooth_window != 100) + { + valid = false; + config.sppOpts.smooth_window = 100; + BOOST_LOG_TRIVIAL(warning) + << "It is recommended that a 100 second smoothing window be used for L1 " + "SBAS. Changing configuration"; + } + + if (config.sppOpts.use_smooth_only == false) + { + valid = false; + config.sppOpts.use_smooth_only = true; + BOOST_LOG_TRIVIAL(warning) + << "It is NOT recommended that measurements be used for SBAS before " + "smoothing. Changing configuration"; + } + + if (config.sbsOpts.use_sbas_rec_var == false) + { + valid = false; + config.sbsOpts.use_sbas_rec_var = true; + BOOST_LOG_TRIVIAL(warning) + << "It is recommended that measurement variance specific for SBAS are " + "used. Changing configuration"; + } + + if (config.sbsInOpts.use_do259) + { + valid = false; + config.sbsInOpts.use_do259 = false; + BOOST_LOG_TRIVIAL(warning) + << "DO-259 is not to be use with L1 SBAS. Setting use_do259 to false"; + } + + if (config.sbsInOpts.pvs_on_dfmc) + { + valid = false; + config.sbsInOpts.pvs_on_dfmc = false; + BOOST_LOG_TRIVIAL(warning) + << "PVS on DFMC is not to be use with L1 SBAS. Setting pvs_on_dfmc to false"; + } + + break; + } + + case E_SbasMode::DFMC: + { + BOOST_LOG_TRIVIAL(info) + << "DFMC processing mode is selected, make sure that:\n" + " - You have inputs containing SBAS messages (sisnet, ems, sbf, etc.)\n" + " - If using a service follwing DO-259 (instead of DO-259A), set " + "`sbas_inputs: use_do259: true`\n" + " - If using measurements from GLO or BDS, set the `code_priorities` and " + "`used_nav_type` properly\n"; + + config.sbsInOpts.freq = 5; + config.sbsInOpts.pvs_on_dfmc = false; + + for (auto& [sys, process] : config.process_sys) + { + if (sys == E_Sys::GLO || sys == E_Sys::LEO) + { + process = false; + } + else if (sys != E_Sys::BDS) + { + config.code_priorities[sys] = config.sbsOpts.sbas_code_priorities_map[sys]; + } + } + + config.sppOpts.trop_models = {E_TropModel::SBAS}; + config.sppOpts.iono_mode = E_IonoMode::SBAS; + + if (config.sppOpts.smooth_window < 0) + { + BOOST_LOG_TRIVIAL(warning) + << "It is recommended that a 100 second smoothing window be used for DFMC. " + "Please check your configuration"; + } + + break; + } + + case E_SbasMode::PVS: + { + BOOST_LOG_TRIVIAL(info) + << "PVS-via-DFMC processing mode is selected, make sure that:\n" + " - You have inputs containing SBAS messages (sisnet, ems, sbf, etc.)\n" + " - The SBAS messages come from SouthPAN's DFMC services\n" + " The following processing options will be used:\n" + " - GPS and/or GAL constellations will be used (with GPS as refeence " + "system)\n" + " - Saastamoinen model wil be used for troposphere delay " + "mapping/estimation\n" + " - If using solid earth tide models, ocean tide loading will be applied, " + "while atmospheric tide loading and pole tide loadings will not\n"; + + config.process_ppp = true; + + config.sbsInOpts.freq = 5; + config.sbsInOpts.pvs_on_dfmc = true; + + for (auto& [sys, process] : config.process_sys) + { + if (sys == E_Sys::GPS || sys == E_Sys::GAL) + { + process = true; + config.code_priorities[sys] = config.sbsOpts.sbas_code_priorities_map[sys]; + } + else + { + process = false; + } + } + + for (auto& [id, recOpts] : config.recOptsMap) + { + vector tropModels = {E_TropModel::STANDARD}; + setOption(recOpts, recOpts.receiver_reference_system, E_Sys::GPS); + setOption(recOpts, recOpts.tropModel.enable, true); + setOption(recOpts, recOpts.tropModel.models, tropModels); + if (recOpts.tideModels.solid) + { + setOption(recOpts, recOpts.tideModels.otl, true); + setOption(recOpts, recOpts.tideModels.atl, false); + setOption(recOpts, recOpts.tideModels.spole, false); + setOption(recOpts, recOpts.tideModels.opole, false); + } + } + + config.sppOpts.always_reinitialise = true; + config.pppOpts.use_primary_signals = true; + config.errorAccumulation.enable = true; + config.ambErrors.phase_reject_limit = 2; + config.ambErrors.resetOnSlip.LLI = true; + config.ambErrors.resetOnSlip.retrack = true; + + break; + } + } + + return valid; +} + +std::string SbasSanityChecker::name() const +{ + return "SbasSanityChecker"; +} diff --git a/src/cpp/common/sanityCheckers/SbasSanityChecker.hpp b/src/cpp/common/sanityCheckers/SbasSanityChecker.hpp new file mode 100644 index 000000000..83be5819b --- /dev/null +++ b/src/cpp/common/sanityCheckers/SbasSanityChecker.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "common/sanityCheckers/ISanityChecker.hpp" + +struct SbasSanityChecker : ISanityChecker +{ + bool check(ACSConfig& config) override; + std::string name() const override; +}; diff --git a/src/cpp/common/sanityCheckers/sanityCheckers.hpp b/src/cpp/common/sanityCheckers/sanityCheckers.hpp new file mode 100644 index 000000000..95e4dc337 --- /dev/null +++ b/src/cpp/common/sanityCheckers/sanityCheckers.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "common/sanityCheckers/ConfigSanityManager.hpp" +#include "common/sanityCheckers/EphemerisTimeDelayChecker.hpp" +#include "common/sanityCheckers/EpochToleranceChecker.hpp" +#include "common/sanityCheckers/IonosphericFreeComboChecker.hpp" +#include "common/sanityCheckers/IonosphericOutageChecker.hpp" +#include "common/sanityCheckers/RequiredSiteEccentricityChecker.hpp" +#include "common/sanityCheckers/SbasSanityChecker.hpp" diff --git a/src/cpp/common/sbfDecoder.cpp b/src/cpp/common/sbfDecoder.cpp index b8ead9253..bf272ab88 100644 --- a/src/cpp/common/sbfDecoder.cpp +++ b/src/cpp/common/sbfDecoder.cpp @@ -269,14 +269,29 @@ void SbfDecoder::decodeMeasEpoch(GTime time, vector& data) continue; // GLONASS not supported for now sbfObsList.push_back((shared_ptr)obs); } - obsListList.push_back(sbfObsList); + if (sbfObsList.empty() == false) + { + obsListList.push_back(sbfObsList); + } + else + { + BOOST_LOG_TRIVIAL(info) << "SBF decoder produced empty ObsList at end of message block"; + } sbfObsList.clear(); return; } void SbfDecoder::decodeEndOfMeas(GTime time) { - obsListList.push_back(sbfObsList); + if (sbfObsList.empty() == false) + { + obsListList.push_back(sbfObsList); + } + else + { + BOOST_LOG_TRIVIAL(info) << "SBF decoder end-of-measurement flush with empty ObsList" + << ", time=" << time.to_string(6); + } sbfObsList.clear(); lastObstime = time; } @@ -991,6 +1006,6 @@ void SbfDecoder::decode(unsigned short int id, vector& data) case 5922: decodeEndOfMeas(time); return; - // default: std::cout << " ... not supported, yet"; return; + // default: std::cout << " ... not supported, yet"; return; } } diff --git a/src/cpp/common/sinex.cpp b/src/cpp/common/sinex.cpp index 613b2565e..ddc8fbd2d 100644 --- a/src/cpp/common/sinex.cpp +++ b/src/cpp/common/sinex.cpp @@ -1,7 +1,9 @@ // #pragma GCC optimize ("O0") #include "common/sinex.hpp" +#include #include +#include #include #include #include @@ -11,12 +13,14 @@ #include #endif #include "architectureDocs.hpp" +#include "common/acsConfig.hpp" #include "common/algebra.hpp" #include "common/eigenIncluder.hpp" #include "common/gTime.hpp" #include "common/navigation.hpp" #include "common/receiver.hpp" #include "common/trace.hpp" +#include "orbprop/coordinates.hpp" using std::getline; using std::ifstream; @@ -26,7 +30,9 @@ using std::ofstream; */ FileType SNX__() {} -Sinex theSinex(false); // the one and only sinex object. +Sinex theSinex(false); // the one and only sinex object. +string separator = + "*-------------------------------------------------------------------------------"; // Sinex 2.02 documentation indicates 2 digit years. >50 means 1900+N. <=50 means 2000+N // To achieve this, when we read years, if >50 add 1900 else add 2000. This source will @@ -66,84 +72,72 @@ void trimCut(string& line) ///< string to trim line.pop_back(); } -bool compare(string& one, string& two) -{ - if (one.compare(two) == 0) - { - return true; - } - return false; -} - -bool compare(SinexInputFile& one, SinexInputFile& two) +bool sinexBlockExcluded(const string& blockName) { - if (one.yds[0] == two.yds[0] && one.yds[1] == two.yds[1] && one.yds[2] == two.yds[2] && - one.agency.compare(two.agency) == 0 && one.file.compare(two.file) == 0 && - one.description.compare(two.description) == 0) - { - return true; - } - return false; + auto& excludedBlocks = acsConfig.exclude_sinex_blocks; + return std::find(excludedBlocks.begin(), excludedBlocks.end(), blockName) != + excludedBlocks.end(); } -bool compare(SinexSolStatistic& one, SinexSolStatistic& two) +void truncateSomething(char* buf) { - if (one.name.compare(two.name) == 0) + if (strlen(buf) == 7 && buf[1] == '0' && buf[0] == '-') { - return true; + for (int j = 2; j < 8; j++) + { + buf[j - 1] = buf[j]; + } } - return false; } -bool compare(SinexSatPc& one, SinexSatPc& two) +bool compare(string& one, string& two) { - if (one.svn.compare(two.svn) == 0 && one.freq == two.freq && one.freq2 == two.freq2) + if (one.compare(two) == 0) { return true; } return false; } -bool compare(SinexSatEcc& one, SinexSatEcc& two) +bool compare(SinexInputHistory& one, SinexInputHistory& two) { - if (one.svn.compare(two.svn) == 0 && one.type == two.type) + if (one.code == two.code && one.fmt == two.fmt && one.create_time[0] == two.create_time[0] && + one.create_time[1] == two.create_time[1] && one.create_time[2] == two.create_time[2] && + one.start[0] == two.start[0] && one.start[1] == two.start[1] && + one.start[2] == two.start[2] && one.stop[0] == two.stop[0] && one.stop[1] == two.stop[2] && + one.stop[2] == two.stop[2] && one.obs_tech == two.obs_tech && + one.num_estimates == two.num_estimates && one.constraint == two.constraint && + one.contents.compare(two.contents) == 0 && one.data_agency.compare(two.data_agency) == 0 && + one.create_agency.compare(two.create_agency) == 0) { return true; } return false; } -bool compare(SinexSatMass& one, SinexSatMass& two) +bool compare(SinexInputFile& one, SinexInputFile& two) { - if (one.svn.compare(two.svn) == 0 && one.start[0] == two.start[0] && - one.start[1] == two.start[1] && one.start[2] == two.start[2] && - one.stop[0] == two.stop[0] && one.stop[1] == two.stop[1] && one.stop[2] == two.stop[2]) + if (one.yds[0] == two.yds[0] && one.yds[1] == two.yds[1] && one.yds[2] == two.yds[2] && + one.agency.compare(two.agency) == 0 && one.file.compare(two.file) == 0 && + one.description.compare(two.description) == 0) { return true; } return false; } -bool compare(SinexSatFreqChn& one, SinexSatFreqChn& two) +bool compare(SinexAck& one, SinexAck& two) { - if (one.svn.compare(two.svn) == 0 && one.start[0] == two.start[0] && - one.start[1] == two.start[1] && one.start[2] == two.start[2] && - one.stop[0] == two.stop[0] && one.stop[1] == two.stop[1] && one.stop[2] == two.stop[2]) + if (one.agency.compare(two.agency) == 0 && one.description.compare(two.description) == 0) { return true; } return false; } -bool compare(SinexSatId& one, SinexSatId& two) +bool compare(SinexNutCode& one, SinexNutCode& two) { - if (one.svn.compare(two.svn) == 0 && one.prn.compare(two.prn) == 0 && - one.timeSinceLaunch[0] == two.timeSinceLaunch[0] && - one.timeSinceLaunch[1] == two.timeSinceLaunch[1] && - one.timeSinceLaunch[2] == two.timeSinceLaunch[2] && - one.timeUntilDecom[0] == two.timeUntilDecom[0] && - one.timeUntilDecom[1] == two.timeUntilDecom[1] && - one.timeUntilDecom[2] == two.timeUntilDecom[2]) + if (one.nutcode.compare(two.nutcode) == 0) { return true; } @@ -168,103 +162,98 @@ bool compare(SinexSourceId& one, SinexSourceId& two) return false; } -bool compare(SinexNutCode& one, SinexNutCode& two) +bool compare(SinexSiteId& one, SinexSiteId& two) { - if (one.nutcode.compare(two.nutcode) == 0) + if (one.sitecode.compare(two.sitecode) == 0) { return true; } return false; } -bool compare(SinexSatPrn& one, SinexSatPrn& two) +bool compare(SinexSiteData& one, SinexSiteData& two) { - if (one.svn.compare(two.svn) == 0 && one.prn.compare(two.prn) == 0 && - one.start[0] == two.start[0] && one.start[1] == two.start[1] && - one.start[2] == two.start[2]) + if (one.site.compare(two.site) == 0 && one.sitecode.compare(two.sitecode) == 0) { return true; } return false; } -bool compare(SinexSatPower& one, SinexSatPower& two) +// compare by the 2 station ids only. +bool compareSiteData(const SinexSiteData& left, const SinexSiteData& right) { - if (one.svn.compare(two.svn) == 0 && one.start[0] == two.start[0] && - one.start[1] == two.start[1] && one.start[2] == two.start[2] && - one.stop[0] == two.stop[0] && one.stop[1] == two.stop[1] && one.stop[2] == two.stop[2]) - { - return true; - } - return false; + int comp = left.site.compare(right.site); + + if (comp == 0) + comp = left.sitecode.compare(right.sitecode); + + return (comp < 0); } -bool compare(SinexSatCom& one, SinexSatCom& two) +bool compare(SinexReceiver& one, SinexReceiver& two) { - if (one.svn.compare(two.svn) == 0 && one.start[0] == two.start[0] && - one.start[1] == two.start[1] && one.start[2] == two.start[2] && - one.stop[0] == two.stop[0] && one.stop[1] == two.stop[1] && one.stop[2] == two.stop[2]) + if (one.sitecode.compare(two.sitecode) == 0 && one.start[0] == two.start[0] && + one.start[1] == two.start[1] && one.start[2] == two.start[2] && one.end[0] == two.end[0] && + one.end[1] == two.end[1] && one.end[2] == two.end[2]) { return true; } return false; } -bool compare(SinexAck& one, SinexAck& two) +bool compare(SinexAntenna& one, SinexAntenna& two) { - if (one.agency.compare(two.agency) == 0 && one.description.compare(two.description) == 0) + if (one.sitecode.compare(two.sitecode) == 0 && one.start[0] == two.start[0] && + one.start[1] == two.start[1] && one.start[2] == two.start[2] && one.end[0] == two.end[0] && + one.end[1] == two.end[1] && one.end[2] == two.end[2]) { return true; } return false; } -bool compare(SinexInputHistory& one, SinexInputHistory& two) +bool compare(SinexGpsPhaseCenter& one, SinexGpsPhaseCenter& two) { - if (one.code == two.code && one.fmt == two.fmt && one.create_time[0] == two.create_time[0] && - one.create_time[1] == two.create_time[1] && one.create_time[2] == two.create_time[2] && - one.start[0] == two.start[0] && one.start[1] == two.start[1] && - one.start[2] == two.start[2] && one.stop[0] == two.stop[0] && one.stop[1] == two.stop[2] && - one.stop[2] == two.stop[2] && one.obs_tech == two.obs_tech && - one.num_estimates == two.num_estimates && one.constraint == two.constraint && - one.contents.compare(two.contents) == 0 && one.data_agency.compare(two.data_agency) == 0 && - one.create_agency.compare(two.create_agency) == 0) + if (one.antname.compare(two.antname) == 0 && one.serialno.compare(two.serialno) == 0) { return true; } return false; } -bool compare(SinexSiteId& one, SinexSiteId& two) +// compare by antenna type and serial number. +bool compareGpsPc(SinexGpsPhaseCenter& left, SinexGpsPhaseCenter& right) { - if (one.sitecode.compare(two.sitecode) == 0) - { - return true; - } - return false; + int comp = left.antname.compare(right.antname); + + if (comp == 0) + comp = left.serialno.compare(right.serialno); + + return (comp < 0); } -bool compare(SinexSiteData& one, SinexSiteData& two) +bool compare(SinexGalPhaseCenter& one, SinexGalPhaseCenter& two) { - if (one.site.compare(two.site) == 0 && one.sitecode.compare(two.sitecode) == 0) + if (one.antname.compare(two.antname) == 0 && one.serialno.compare(two.serialno) == 0) { return true; } return false; } -bool compare(SinexReceiver& one, SinexReceiver& two) +// compare by antenna type and serial number. return true0 if left < right +bool compareGalPc(SinexGalPhaseCenter& left, SinexGalPhaseCenter& right) { - if (one.sitecode.compare(two.sitecode) == 0 && one.start[0] == two.start[0] && - one.start[1] == two.start[1] && one.start[2] == two.start[2] && one.end[0] == two.end[0] && - one.end[1] == two.end[1] && one.end[2] == two.end[2]) - { - return true; - } - return false; + int comp = left.antname.compare(right.antname); + + if (comp == 0) + comp = left.serialno.compare(right.serialno); + + return (comp < 0); } -bool compare(SinexAntenna& one, SinexAntenna& two) +bool compare(SinexSiteEcc& one, SinexSiteEcc& two) { if (one.sitecode.compare(two.sitecode) == 0 && one.start[0] == two.start[0] && one.start[1] == two.start[1] && one.start[2] == two.start[2] && one.end[0] == two.end[0] && @@ -275,93 +264,239 @@ bool compare(SinexAntenna& one, SinexAntenna& two) return false; } -bool compare(SinexGpsPhaseCenter& one, SinexGpsPhaseCenter& two) +bool compare(SinexSatId& one, SinexSatId& two) { - if (one.antname.compare(two.antname) == 0 && one.serialno.compare(two.serialno) == 0) + if (one.svn.compare(two.svn) == 0 && one.prn.compare(two.prn) == 0 && + one.timeSinceLaunch[0] == two.timeSinceLaunch[0] && + one.timeSinceLaunch[1] == two.timeSinceLaunch[1] && + one.timeSinceLaunch[2] == two.timeSinceLaunch[2] && + one.timeUntilDecom[0] == two.timeUntilDecom[0] && + one.timeUntilDecom[1] == two.timeUntilDecom[1] && + one.timeUntilDecom[2] == two.timeUntilDecom[2]) { return true; } return false; } -bool compare(SinexGalPhaseCenter& one, SinexGalPhaseCenter& two) +bool compareSatIds(SinexSatId& left, SinexSatId& right) { - if (one.antname.compare(two.antname) == 0 && one.serialno.compare(two.serialno) == 0) - { - return true; - } - return false; + int comp = left.svn.compare(right.svn); + + return (comp < 0); } -bool compare(SinexSiteEcc& one, SinexSiteEcc& two) +bool compare(SinexSatPc& one, SinexSatPc& two) { - if (one.sitecode.compare(two.sitecode) == 0 && one.start[0] == two.start[0] && - one.start[1] == two.start[1] && one.start[2] == two.start[2] && one.end[0] == two.end[0] && - one.end[1] == two.end[1] && one.end[2] == two.end[2]) + if (one.svn.compare(two.svn) == 0 && one.freq == two.freq && one.freq2 == two.freq2) { return true; } return false; } -bool compare(SinexSolEpoch& one, SinexSolEpoch& two) +bool compareSatPc(SinexSatPc& left, SinexSatPc& right) { - if (one.sitecode.compare(two.sitecode) == 0 && one.start[0] == two.start[0] && - one.start[1] == two.start[1] && one.start[2] == two.start[2] && one.end[0] == two.end[0] && - one.end[1] == two.end[1] && one.end[2] == two.end[2]) - { - return true; - } - return false; + // start by comparing SVN... + int comp = left.svn.compare(right.svn); + + // then by the first freq number + if (comp == 0) + comp = static_cast(left.freq) - static_cast(right.freq); + + return (comp < 0); } -bool compare(SinexSolEstimate& one, SinexSolEstimate& two) +bool compare(SinexSatPrn& one, SinexSatPrn& two) { - if (one.sitecode.compare(two.sitecode) == 0 && one.type.compare(two.type) == 0 && - one.refepoch == two.refepoch) + if (one.svn.compare(two.svn) == 0 && one.prn.compare(two.prn) == 0 && + one.start[0] == two.start[0] && one.start[1] == two.start[1] && + one.start[2] == two.start[2]) { return true; } return false; } -bool compare(SinexSolApriori& one, SinexSolApriori& two) +bool compareSatPrns(SinexSatPrn& left, SinexSatPrn& right) { - if (one.sitecode.compare(two.sitecode) == 0 && one.param_type.compare(two.param_type) == 0 && - one.epoch == two.epoch) + int comp = left.prn.compare(right.prn); + + return (comp < 0); +} + +bool compare(SinexSatFreqChn& one, SinexSatFreqChn& two) +{ + if (one.svn.compare(two.svn) == 0 && one.start[0] == two.start[0] && + one.start[1] == two.start[1] && one.start[2] == two.start[2] && + one.stop[0] == two.stop[0] && one.stop[1] == two.stop[1] && one.stop[2] == two.stop[2]) { return true; } return false; } -bool compare(SinexSolNeq& one, SinexSolNeq& two) +bool compareFreqChannels(SinexSatFreqChn& left, SinexSatFreqChn& right) { - if (one.site.compare(two.site) == 0 && one.ptype.compare(two.ptype) == 0 && - one.epoch == two.epoch) + // start by comparing SVN... + int comp = left.svn.compare(right.svn); + + // then by start time if the same space vehicle + for (int i = 0; i < 3; i++) + if (comp == 0) + comp = left.start[i] - right.start[i]; + + return (comp < 0); +} + +bool compare(SinexSatMass& one, SinexSatMass& two) +{ + if (one.svn.compare(two.svn) == 0 && one.start[0] == two.start[0] && + one.start[1] == two.start[1] && one.start[2] == two.start[2] && + one.stop[0] == two.stop[0] && one.stop[1] == two.stop[1] && one.stop[2] == two.stop[2]) { return true; } return false; } -bool compare(SinexSolMatrix& one, SinexSolMatrix& two) +bool compare(SinexSatCom& one, SinexSatCom& two) { - if (one.row == two.row && one.col == two.col) + if (one.svn.compare(two.svn) == 0 && one.start[0] == two.start[0] && + one.start[1] == two.start[1] && one.start[2] == two.start[2] && + one.stop[0] == two.stop[0] && one.stop[1] == two.stop[1] && one.stop[2] == two.stop[2]) { return true; } return false; } -template -void dedupe(list& source) +bool compareSatCom(SinexSatCom& left, SinexSatCom& right) { - list copy; + // start by comparing SVN... + int comp = left.svn.compare(right.svn); - for (auto it = source.begin(); it != source.end();) - { - bool found = false; + // then by start time if the same space vehicle + for (int i = 0; i < 3; i++) + if (comp == 0) + comp = left.start[i] - right.start[i]; + + return (comp < 0); +} + +bool compare(SinexSatEcc& one, SinexSatEcc& two) +{ + if (one.svn.compare(two.svn) == 0 && one.type == two.type) + { + return true; + } + return false; +} + +bool compareSatEcc(SinexSatEcc& left, SinexSatEcc& right) +{ + // start by comparing SVN... + int comp = left.svn.compare(right.svn); + + // then by type (P or L) + if (comp == 0) + comp = static_cast(left.type) - static_cast(right.type); + + return (comp < 0); +} + +bool compare(SinexSatPower& one, SinexSatPower& two) +{ + if (one.svn.compare(two.svn) == 0 && one.start[0] == two.start[0] && + one.start[1] == two.start[1] && one.start[2] == two.start[2] && + one.stop[0] == two.stop[0] && one.stop[1] == two.stop[1] && one.stop[2] == two.stop[2]) + { + return true; + } + return false; +} + +bool compare(SinexSolEpoch& one, SinexSolEpoch& two) +{ + if (one.sitecode.compare(two.sitecode) == 0 && one.start[0] == two.start[0] && + one.start[1] == two.start[1] && one.start[2] == two.start[2] && one.end[0] == two.end[0] && + one.end[1] == two.end[1] && one.end[2] == two.end[2]) + { + return true; + } + return false; +} + +bool compareSiteEpochs(SinexSolEpoch& left, SinexSolEpoch& right) +{ + int comp = left.sitecode.compare(right.sitecode); + int i = 0; + + while (comp == 0 && i < 3) + { + comp = left.start[i] - right.start[i]; + i++; + } + + return (comp < 0); +} + +bool compare(SinexSolStatistic& one, SinexSolStatistic& two) +{ + if (one.name.compare(two.name) == 0) + { + return true; + } + return false; +} + +bool compare(SinexSolEstimate& one, SinexSolEstimate& two) +{ + if (one.sitecode.compare(two.sitecode) == 0 && one.type.compare(two.type) == 0 && + one.refepoch == two.refepoch) + { + return true; + } + return false; +} + +bool compare(SinexSolApriori& one, SinexSolApriori& two) +{ + if (one.sitecode.compare(two.sitecode) == 0 && one.param_type.compare(two.param_type) == 0 && + one.epoch == two.epoch) + { + return true; + } + return false; +} + +bool compare(SinexSolNeq& one, SinexSolNeq& two) +{ + if (one.site.compare(two.site) == 0 && one.ptype.compare(two.ptype) == 0 && + one.epoch == two.epoch) + { + return true; + } + return false; +} + +bool compare(SinexSolMatrix& one, SinexSolMatrix& two) +{ + if (one.row == two.row && one.col == two.col) + { + return true; + } + return false; +} + +template +void dedupe(list& source) +{ + list copy; + + for (auto it = source.begin(); it != source.end();) + { + bool found = false; for (auto it2 = copy.begin(); it2 != copy.end(); it2++) { @@ -430,11 +565,11 @@ void dedupeSinex() dedupe(theSinex.listprecessions); dedupe(theSinex.listsourceids); dedupe(theSinex.listsatids); + dedupe(theSinex.listsatpcs); dedupe(theSinex.listsatprns); dedupe(theSinex.listsatfreqchns); dedupe(theSinex.listsatcoms); dedupe(theSinex.listsateccs); - dedupe(theSinex.listsatpcs); dedupe(theSinex.liststatistics); // // TODO: need to make sure sitecode & type match on index @@ -460,6 +595,8 @@ void dedupeSinex() return; } +void nullFunction(string& line) {} + // TODO; What if we are reading a second file. What wins? bool readSnxHeader(std::ifstream& in) { @@ -543,1941 +680,2495 @@ bool readSnxHeader(std::ifstream& in) return true; } -void updateSinexHeader( - string& create_agc, - string& data_agc, - UYds soln_start, - UYds soln_end, - const char obsCode, - const char constCode, - string& contents, - int numParam, - double sinexVer -) +void parseReference(string& line) +{ + theSinex.refstrings.push_back(line); +} + +void parseInputHistory(string& line) { SinexInputHistory siht; + // remaining characters indiciate properties of the history - siht.code = '+'; - siht.fmt = theSinex.ver; - siht.create_agency = theSinex.createagc; - siht.data_agency = theSinex.dataagc; - siht.obs_tech = theSinex.obsCode; - siht.constraint = theSinex.constCode; - siht.num_estimates = theSinex.numparam; - siht.contents = theSinex.solcont; - siht.create_time = theSinex.filedate; - siht.start = theSinex.solutionstartdate; - siht.stop = theSinex.solutionenddate; + if (line.length() > 5) + { + const char* buff = line.c_str(); + char create_agc[4]; + char data_agc[4]; + char solcontents[7]; + int readcount; - if (theSinex.inputHistory.empty()) - theSinex.inputHistory.push_back(siht); + siht.code = line[1]; - theSinex.ver = sinexVer; + readcount = sscanf( + buff + 6, + "%4lf %3s %2lf:%3lf:%5lf %3s %2lf:%3lf:%5lf %2lf:%3lf:%5lf %c %5d %c %c %c %c %c %c %c", + &siht.fmt, + create_agc, + &siht.create_time[0], + &siht.create_time[1], + &siht.create_time[2], + data_agc, + &siht.start[0], + &siht.start[1], + &siht.start[2], + &siht.stop[0], + &siht.stop[1], + &siht.stop[2], + &siht.obs_tech, + &siht.num_estimates, + &siht.constraint, + &solcontents[0], + &solcontents[1], + &solcontents[2], + &solcontents[3], + &solcontents[4], + &solcontents[5] + ); - if (data_agc.size() > 0) - theSinex.dataagc = data_agc; - else - theSinex.dataagc = theSinex.createagc; + if (readcount >= 15) + { + while (readcount < 21) + { + solcontents[readcount - 15] = ' '; + readcount++; + } - theSinex.createagc = create_agc; - theSinex.solcont = contents; - theSinex.filedate = timeGet(); - theSinex.solutionstartdate = soln_start; - theSinex.solutionenddate = soln_end; + solcontents[6] = '\0'; - if (obsCode != ' ') - theSinex.obsCode = obsCode; + siht.create_agency = create_agc; + siht.data_agency = data_agc; + siht.contents = solcontents; - if (constCode != ' ') - theSinex.constCode = constCode; + nearestYear(siht.create_time[0]); + nearestYear(siht.start[0]); + nearestYear(siht.stop[0]); - theSinex.numparam = numParam; + theSinex.inputHistory.push_back(siht); + } + } } -void writeSnxHeader(std::ofstream& out) +void parseInputFiles(string& line) { - char line[81]; - char c; - int i; - - int offset = 0; - offset += snprintf( - line + offset, - sizeof(line) - offset, - "%%=SNX %4.2lf %3s %2.2d:%3.3d:%5.5d %3s %2.2d:%3.3d:%5.5d %2.2d:%3.3d:%5.5d %c %5d %c", - theSinex.ver, - theSinex.createagc.c_str(), - (int)theSinex.filedate[0] % 100, - (int)theSinex.filedate[1], - (int)theSinex.filedate[2], - theSinex.dataagc.c_str(), - (int)theSinex.solutionstartdate[0] % 100, - (int)theSinex.solutionstartdate[1], - (int)theSinex.solutionstartdate[2], - (int)theSinex.solutionenddate[0] % 100, - (int)theSinex.solutionenddate[1], - (int)theSinex.solutionenddate[2], - theSinex.obsCode, - theSinex.numparam, - theSinex.constCode - ); + SinexInputFile sif; + char agency[4]; + const char* buff = line.c_str(); + sif.file = line.substr(18, 29); + sif.description = line.substr(48); - i = 0; - c = theSinex.solcont[0]; + int readcount = + sscanf(buff + 1, "%3s %2lf:%3lf:%5lf", agency, &sif.yds[0], &sif.yds[1], &sif.yds[2]); - while (c != ' ') + if (readcount == 4) { - snprintf(line + offset, sizeof(line) - offset, " %c", c); + sif.agency = agency; - i++; + nearestYear(sif.yds[0]); - if (i <= theSinex.solcont.length()) - c = theSinex.solcont[i]; - else - c = ' '; + theSinex.inputFiles.push_back(sif); } - - out << line << "\n"; } -void parseReference(string& line) +void parseAcknowledgements(string& line) { - theSinex.refstrings.push_back(line); + SinexAck sat; + + sat.description = line.substr(5); + sat.agency = line.substr(1, 3); + + theSinex.acknowledgements.push_back(sat); } -void writeAsComments(Trace& out, list& comments) +void parseNutcode(string& line) { - for (auto& comment : comments) - { - string line = comment; + SinexNutCode snt; - // just make sure it starts with * as required by format - line[0] = '*'; + snt.nutcode = line.substr(1, 8); + snt.comment = line.substr(10); - out << line << "\n"; - } + theSinex.listnutcodes.push_back(snt); } -void commentsOverride() +void parsePrecode(string& line) { - // overriding only those that can be found in IGS/CODE/GRG SINEX files - theSinex.blockComments["FILE/REFERENCE"].push_back( - "*OWN __CREATION__ ___________FILENAME__________ ___________DESCRIPTION__________" - ); // INPUT/FILES - theSinex.blockComments["INPUT/HISTORY"].push_back( - "*_VERSION_ CRE __CREATION__ OWN _DATA_START_ __DATA_END__ T PARAM S ____TYPE____" - ); // INPUT/HISTORY - theSinex.blockComments["INPUT/ACKNOWLEDGEMENTS"].push_back( - "*AGY ______________________________FULL_DESCRIPTION_____________________________" - ); // INPUT/ACKNOWLEDGEMENTS - theSinex.blockComments["SITE/ID"].push_back( - "*CODE PT __DOMES__ T _STATION DESCRIPTION__ _LONGITUDE_ _LATITUDE__ HEIGHT_" - ); // SITE/ID - theSinex.blockComments["SITE/DATA"].push_back( - "*CODE PT SOLN CODE PT SOLN T _DATA START_ _DATA END___ OWN _FILE TIME__" - ); // SITE/DATA - theSinex.blockComments["SITE/RECEIVER"].push_back( - "*CODE PT SOLN T _DATA START_ _DATA END___ _RECEIVER TYPE______ _S/N_ _FIRMWARE__" - ); // SITE/RECEIVER - theSinex.blockComments["SITE/ANTENNA"].push_back( - "*CODE PT SOLN T _DATA START_ __DATA END__ __ANTENNA TYPE______ _S/N_" - ); // SITE/ANTENNA - theSinex.blockComments["SITE/GPS_PHASE_CENTER"].push_back( - "________TYPE________ _S/N_ _L1_U_ _L1_N_ _L1_E_ _L2_U_ _L2_N_ _L2_E_ __MODEL___" - ); // SITE/GPS_PHASE_CENTER - theSinex.blockComments["SITE/ECCENTRICITY"].push_back( - "* _UP_____ _NORTH__ _EAST___\n*CODE PT SOLN T " - "_DATA START_ __DATA " - "END__ TYP __ARP-BENCHMARK (M)_______" - ); // SITE/ECCENTRICITY - theSinex.blockComments["SOLUTION/ESTIMATE"].push_back( - "*INDEX _TYPE_ CODE PT SOLN _REF_EPOCH__ UNIT S ___ESTIMATED_VALUE___ __STD_DEV__" - ); // BIAS/EPOCHS|SOLUTION/EPOCHS|SOLUTION/ESTIMATE - theSinex.blockComments["SOLUTION/STATISTICS"].push_back( - "*_STATISTICAL PARAMETER________ __VALUE(S)____________" - ); // SOLUTION/STATISTICS - theSinex.blockComments["SOLUTION/APRIORI"].push_back( - "*INDEX _TYPE_ CODE PT SOLN _REF_EPOCH__ UNIT S __APRIORI VALUE______ _STD_DEV___" - ); // SOLUTION/APRIORI - theSinex.blockComments["SOLUTION/NORMAL_EQUATION_VECTOR"].push_back( - "*INDEX TYPE__ CODE PT SOLN _REF_EPOCH__ UNIT S __RIGHT_HAND_SIDE____" - ); // SOLUTION/NORMAL_EQUATION_VECTOR - theSinex.blockComments["SOLUTION/MATRIX_ESTIMATE"].push_back( - "*PARA1 PARA2 _______PARA2+0_______ _______PARA2+1_______ _______PARA2+2_______" - ); // SOLUTION/MATRIX_ESTIMATE|SOLUTION/MATRIX_APRIORI|SOLUTION/NORMAL_EQUATION_MATRIX - theSinex.blockComments["SATELLITE/PHASE_CENTER"].push_back( - "*SITE L SATA_Z SATA_X SATA_Y L SATA_Z SATA_X SATA_Y MODEL_____ T M" - ); // SATELLITE/PHASE_CENTER - theSinex.blockComments["SAT/ID"].push_back( - "SATELLITE/ID *SITE PR COSPAR___ T DATA_START__ DATA_END____ ANTENNA_____________" - ); // SATELLITE/ID + SinexPreCode snt; + + snt.precesscode = line.substr(1, 8); + snt.comment = line.substr(10); + + theSinex.listprecessions.push_back(snt); } -void writeSnxReference(ofstream& out) +void parseSourceIds(string& line) { - Block block(out, "FILE/REFERENCE"); + SinexSourceId ssi; - for (auto& refString : theSinex.refstrings) - { - out << refString << "\n"; - } + ssi.source = line.substr(1, 4); + ssi.iers = line.substr(6, 8); + ssi.icrf = line.substr(15, 16); + ssi.comments = line.substr(32); + + theSinex.listsourceids.push_back(ssi); } -void writeSnxComments(ofstream& out) +void parseSiteIds(string& line) { - Block block(out, "FILE/COMMENT"); + const char* buff = line.c_str(); + SinexSiteId sst; + + sst.sitecode = trim(line.substr(1, 4)); + sst.ptcode = line.substr(6, 2); + sst.domes = line.substr(9, 9); + sst.typecode = line[19]; + sst.desc = line.substr(21, 22); + + int readcount = sscanf( + buff + 44, + "%3d %2d %4lf %3d %2d %4lf %7lf", + &sst.lon_deg, + &sst.lon_min, + &sst.lon_sec, + &sst.lat_deg, + &sst.lat_min, + &sst.lat_sec, + &sst.height + ); - for (auto& commentstring : theSinex.blockComments[block.blockName]) + if (readcount == 7) { - out << commentstring << "\n"; + theSinex.mapsiteids[sst.sitecode] = sst; } } -void parseInputHistory(string& line) +void parseSiteData(string& line) { - SinexInputHistory siht; - // remaining characters indiciate properties of the history + const char* buff = line.c_str(); - if (line.length() > 5) - { - const char* buff = line.c_str(); - char create_agc[4]; - char data_agc[4]; - char solcontents[7]; - int readcount; + SinexSiteData sst; - siht.code = line[1]; + sst.site = line.substr(1, 4); + sst.station_pt = line.substr(6, 2); + sst.soln_id = line.substr(9, 4); + sst.sitecode = line.substr(14, 4); + sst.site_pt = line.substr(18, 2); + sst.sitesoln = line.substr(20, 4); - readcount = sscanf( - buff + 6, - "%4lf %3s %2lf:%3lf:%5lf %3s %2lf:%3lf:%5lf %2lf:%3lf:%5lf %c %5d %c %c %c %c %c %c %c", - &siht.fmt, - create_agc, - &siht.create_time[0], - &siht.create_time[1], - &siht.create_time[2], - data_agc, - &siht.start[0], - &siht.start[1], - &siht.start[2], - &siht.stop[0], - &siht.stop[1], - &siht.stop[2], - &siht.obs_tech, - &siht.num_estimates, - &siht.constraint, - &solcontents[0], - &solcontents[1], - &solcontents[2], - &solcontents[3], - &solcontents[4], - &solcontents[5] - ); + sst.obscode = line[24]; + UYds start; + UYds end; + UYds create; + char agency[4]; - if (readcount >= 15) - { - while (readcount < 21) - { - solcontents[readcount - 15] = ' '; - readcount++; - } + int readcount; - solcontents[6] = '\0'; + readcount = sscanf( + buff + 28, + "%2lf:%3lf:%5lf %2lf:%3lf:%5lf %3s %2lf:%3lf:%5lf", + &start[0], + &start[1], + &start[2], + &end[0], + &end[1], + &end[2], + agency, + &create[0], + &create[1], + &create[2] + ); - siht.create_agency = create_agc; - siht.data_agency = data_agc; - siht.contents = solcontents; + if (readcount == 10) + { + sst.agency = agency; + sst.start = start; + sst.stop = end; + sst.create = create; - nearestYear(siht.create_time[0]); - nearestYear(siht.start[0]); - nearestYear(siht.stop[0]); + // see comment at top of file + if (sst.start[0] != 0 || sst.start[1] != 0 || sst.start[2] != 0) + { + nearestYear(sst.start[0]); + } - theSinex.inputHistory.push_back(siht); + if (sst.stop[0] != 0 || sst.stop[1] != 0 || sst.stop[2] != 0) + { + nearestYear(sst.stop[0]); } + + nearestYear(sst.create[0]); + + theSinex.listsitedata.push_back(sst); } } -void writeSnxInputHistory(ofstream& out) +void parseReceivers(string& line) { - Block block(out, "INPUT/HISTORY"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); + const char* buff = line.c_str(); - for (auto it = theSinex.inputHistory.begin(); it != theSinex.inputHistory.end(); it++) - { - char line[81] = {}; - int offset = 0; - SinexInputHistory siht = *it; - int i = 0; + SinexReceiver srt; - offset += snprintf( - line + offset, - sizeof(line) - offset, - " %cSNX %4.2lf %3s %2.2d:%3.3d:%5.5d %3s %2.2d:%3.3d:%5.5d %2.2d:%3.3d:%5.5d %c %5d %c", - siht.code, - siht.fmt, - siht.create_agency.c_str(), - (int)siht.create_time[0] % 100, - (int)siht.create_time[1], - (int)siht.create_time[2], - siht.data_agency.c_str(), - (int)siht.start[0] % 100, - (int)siht.start[1], - (int)siht.start[2], - (int)siht.stop[0] % 100, - (int)siht.stop[1], - (int)siht.stop[2], - siht.obs_tech, - siht.num_estimates, - siht.constraint - ); + srt.sitecode = trim(line.substr(1, 4)); + srt.ptcode = line.substr(6, 2); + srt.solnid = line.substr(9, 4); + srt.typecode = line[14]; + srt.type = line.substr(42, 20); + srt.sn = line.substr(63, 5); + if (line.length() > 69) + srt.firm = trim(line.substr(69)); + int readcount; - char c = siht.contents[i]; + readcount = sscanf( + buff + 16, + "%2lf:%3lf:%5lf %2lf:%3lf:%5lf", + &srt.start[0], + &srt.start[1], + &srt.start[2], + &srt.end[0], + &srt.end[1], + &srt.end[2] + ); - while (c != ' ') + if (readcount == 6) + { + // see comment at top of file + if (srt.start[0] != 0 || srt.start[1] != 0 || srt.start[2] != 0) { - offset += snprintf(line + offset, sizeof(line) - offset, " %c", c); - i++; + nearestYear(srt.start[0]); + } - if (siht.contents.length() >= i) - c = siht.contents[i]; - else - c = ' '; + if (srt.end[0] != 0 || srt.end[1] != 0 || srt.end[2] != 0) + { + nearestYear(srt.end[0]); } - out << line << "\n"; + theSinex.mapreceivers[srt.sitecode][srt.start] = srt; } } -void parseInputFiles(string& line) +void parseAntennas(string& line) { - SinexInputFile sif; - char agency[4]; - const char* buff = line.c_str(); - sif.file = line.substr(18, 29); - sif.description = line.substr(48, 32); + const char* buff = line.c_str(); - int readcount = - sscanf(buff + 1, "%3s %2lf:%3lf:%5lf", agency, &sif.yds[0], &sif.yds[1], &sif.yds[2]); + SinexAntenna ant; - if (readcount == 4) + ant.sitecode = trim(line.substr(1, 4)); + ant.ptcode = line.substr(6, 2); + ant.solnnum = line.substr(9, 4); + ant.typecode = line[14]; + ant.type = line.substr(42, 20); + if (line.length() > 63) + ant.sn = trim(line.substr(63, 5)); + + int readcount = sscanf( + buff + 16, + "%2lf:%3lf:%5lf %2lf:%3lf:%5lf", + &ant.start[0], + &ant.start[1], + &ant.start[2], + &ant.end[0], + &ant.end[1], + &ant.end[2] + ); + + if (readcount == 6) { - sif.agency = agency; + // see comment at top of file + if (ant.start[0] != 0 || ant.start[1] != 0 || ant.start[2] != 0) + { + nearestYear(ant.start[0]); + } - nearestYear(sif.yds[0]); + if (ant.end[0] != 0 || ant.end[1] != 0 || ant.end[2] != 0) + { + nearestYear(ant.end[0]); + } - theSinex.inputFiles.push_back(sif); + theSinex.mapantennas[ant.sitecode][ant.start] = ant; + // theSinex.list_antennas.push_back(ant); } } -void writeSnxInputFiles(ofstream& out) +void parseGpsPhaseCenters(string& line) { - Block block(out, "INPUT/FILES"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); + const char* buff = line.c_str(); + SinexGpsPhaseCenter sgpct; - for (auto& inputFile : theSinex.inputFiles) - { - SinexInputFile& sif = inputFile; + sgpct.antname = line.substr(1, 20); + sgpct.serialno = line.substr(22, 5); + sgpct.calib = line.substr(70); - char line[81]; - int len; - snprintf( - line, - sizeof(line), - " %3s %02d:%03d:%05d ", - sif.agency.c_str(), - (int)sif.yds[0] % 100, - (int)sif.yds[1], - (int)sif.yds[2] - ); + int readcount = sscanf( + buff + 28, + "%6lf %6lf %6lf %6lf %6lf %6lf", + &sgpct.L1[0], + &sgpct.L1[1], + &sgpct.L1[2], + &sgpct.L2[0], + &sgpct.L2[1], + &sgpct.L2[2] + ); - // if the filename length is greater than 29 (format spec limit) make into a comment line - if (sif.file.length() > 29) - line[0] = '*'; - // pad short filenames to 29 characters - if ((len = sif.file.length()) < 29) - { - for (int i = len; i < 29; i++) - sif.file += ' '; - } - out << line << sif.file << " " << sif.description << "\n"; + if (readcount == 6) + { + theSinex.listgpspcs.push_back(sgpct); } } -void parseAcknowledgements(string& line) +// Gallileo phase centers take three line each! +void parseGalPhaseCenters(string& s_x) { - SinexAck sat; + static int lineNum = 0; + static string lines[3]; + lines[lineNum] = s_x; - sat.description = line.substr(5); - sat.agency = line.substr(1, 3); + lineNum++; + if (lineNum != 3) + { + // wait for 3 lines. + return; + } - theSinex.acknowledgements.push_back(sat); -} + lineNum = 0; -void writeSnxAcknowledgements(ofstream& out) -{ - Block block(out, "INPUT/ACKNOWLEDGEMENTS"); + auto& line1 = lines[0]; + auto& line2 = lines[1]; + auto& line3 = lines[2]; - writeAsComments(out, theSinex.blockComments[block.blockName]); + SinexGalPhaseCenter sgpct; - for (auto& acknowledgement : theSinex.acknowledgements) - { - SinexAck& ack = acknowledgement; + sgpct.antname = line1.substr(1, 20); + sgpct.serialno = line1.substr(22, 5); + sgpct.calib = line1.substr(69); - char line[81]; - snprintf(line, sizeof(line), " %3s %s", ack.agency.c_str(), ack.description.c_str()); + int readcount1 = sscanf( + line1.c_str() + 28, + "%6lf %6lf %6lf %6lf %6lf %6lf", + &sgpct.L1[0], + &sgpct.L1[1], + &sgpct.L1[2], + &sgpct.L5[0], + &sgpct.L5[1], + &sgpct.L5[2] + ); - out << line << "\n"; + // Do we need to check the antenna name and serial each time? I am going to assume not + int readcount2 = sscanf( + line2.c_str() + 28, + "%6lf %6lf %6lf %6lf %6lf %6lf", + &sgpct.L6[0], + &sgpct.L6[1], + &sgpct.L6[2], + &sgpct.L7[0], + &sgpct.L7[1], + &sgpct.L7[2] + ); + int readcount3 = + sscanf(line3.c_str() + 28, "%6lf %6lf %6lf", &sgpct.L8[0], &sgpct.L8[1], &sgpct.L8[2]); + + if (readcount1 == 6 && readcount2 == 6 && readcount3 == 3) + { + theSinex.listgalpcs.push_back(sgpct); } } -void parseSiteIds(string& line) +void parseSiteEccentricity(string& line) { - const char* buff = line.c_str(); - SinexSiteId sst; + const char* buff = line.c_str(); + SinexSiteEcc sset; - sst.sitecode = trim(line.substr(1, 4)); - sst.ptcode = line.substr(6, 2); - sst.domes = line.substr(9, 9); - sst.typecode = line[19]; - sst.desc = line.substr(21, 22); + sset.sitecode = trim(line.substr(1, 4)); + sset.ptcode = line.substr(6, 2); + sset.solnnum = line.substr(9, 4); + sset.typecode = line[14]; + sset.rs = line.substr(42, 3); + char junk[4]; int readcount = sscanf( - buff + 44, - "%3d %2d %4lf %3d %2d %4lf %7lf", - &sst.lon_deg, - &sst.lon_min, - &sst.lon_sec, - &sst.lat_deg, - &sst.lat_min, - &sst.lat_sec, - &sst.height + buff + 16, + "%2lf:%3lf:%5lf %2lf:%3lf:%5lf %3s %8lf %8lf %8lf", + &sset.start[0], + &sset.start[1], + &sset.start[2], + &sset.end[0], + &sset.end[1], + &sset.end[2], + junk, + &sset.ecc.u(), + &sset.ecc.n(), + &sset.ecc.e() ); - if (readcount == 7) + if (readcount == 10) { - theSinex.mapsiteids[sst.sitecode] = sst; + // see comment at top of file + if (sset.start[0] != 0 || sset.start[1] != 0 || sset.start[2] != 0) + { + nearestYear(sset.start[0]); + } + + if (sset.end[0] != 0 || sset.end[1] != 0 || sset.end[2] != 0) + { + nearestYear(sset.end[0]); + } + + theSinex.mapeccentricities[sset.sitecode][sset.start] = sset; } } -void writeSnxSiteids(ofstream& out) +void parseSatelliteIds(string& line) { - Block block(out, "SITE/ID"); + const char* buff = line.c_str(); - writeAsComments(out, theSinex.blockComments[block.blockName]); + SinexSatId sst; + + sst.svn = line.substr(1, 4); + sst.prn = sst.svn[0] + line.substr(6, 2); + sst.cospar = line.substr(9, 9); + sst.obsCode = line[18]; + sst.antRcvType = line.substr(47); + + int readcount = sscanf( + buff + 21, + "%2lf:%3lf:%5lf %2lf:%3lf:%5lf", + &sst.timeSinceLaunch[0], + &sst.timeSinceLaunch[1], + &sst.timeSinceLaunch[2], + &sst.timeUntilDecom[0], + &sst.timeUntilDecom[1], + &sst.timeUntilDecom[2] + ); - for (auto& [id, ssi] : theSinex.mapsiteids) + if (readcount == 6) { - if (ssi.used == false) - { - continue; - } + // TODO: make the following adjustements + // TSL if 0 is Sinex file start date + // TUD if 0 is Sinex file end date - tracepdeex( - 0, - out, - " %4s %2s %9s %c %22s %3d %2d %4.1lf %3d %2d %4.1lf %7.1lf\n", - ssi.sitecode.c_str(), - ssi.ptcode.c_str(), - ssi.domes.c_str(), - ssi.typecode, - ssi.desc.c_str(), - ssi.lon_deg, - ssi.lon_min, - ssi.lon_sec, - ssi.lat_deg, - ssi.lat_min, - ssi.lat_sec, - ssi.height - ); + theSinex.listsatids.push_back(sst); } } -// compare by the 2 station ids only. -bool compareSiteData(const SinexSiteData& left, const SinexSiteData& right) +void parseSatellitePhaseCenters(string& line) { - int comp = left.site.compare(right.site); + const char* buff = line.c_str(); - if (comp == 0) - comp = left.sitecode.compare(right.sitecode); + SinexSatPc spt; - return (comp < 0); + int readcount2; + + spt.svn = line.substr(1, 4); + spt.freq = line[6]; + spt.freq2 = line[29]; + spt.antenna = line.substr(52, 10); + spt.type = line[63]; + spt.model = line[65]; + + int readcount = sscanf(buff + 6, "%6lf %6lf %6lf", &spt.zxy[0], &spt.zxy[1], &spt.zxy[2]); + + if (spt.freq2 != ' ') + { + readcount2 = sscanf(buff + 31, "%6lf %6lf %6lf", &spt.zxy2[0], &spt.zxy2[1], &spt.zxy2[2]); + } + + if (readcount == 3 && (spt.freq2 == ' ' || readcount2 == 3)) + { + theSinex.listsatpcs.push_back(spt); + } } -void parseSiteData(string& line) +void parseSatelliteIdentifiers(string& line) { const char* buff = line.c_str(); - SinexSiteData sst; + SinexSatIdentity sst; - sst.site = line.substr(1, 4); - sst.station_pt = line.substr(6, 2); - sst.soln_id = line.substr(9, 4); - sst.sitecode = line.substr(14, 4); - sst.site_pt = line.substr(18, 2); - sst.sitesoln = line.substr(20, 4); + sst.svn = line.substr(1, 4); + sst.cospar = line.substr(6, 9); + sst.category = (int)str2num(buff, 16, 6); + sst.blocktype = trim(line.substr(23, 15)); + sst.comment = line.substr(39); - sst.obscode = line[24]; - UYds start; - UYds end; - UYds create; - char agency[4]; + theSinex.satIdentityMap[sst.svn] = sst; - int readcount; + nav.blocktypeMap[sst.svn] = sst.blocktype; +} - readcount = sscanf( - buff + 28, - "%2lf:%3lf:%5lf %2lf:%3lf:%5lf %3s %2lf:%3lf:%5lf", - &start[0], - &start[1], - &start[2], - &end[0], - &end[1], - &end[2], - agency, - &create[0], - &create[1], - &create[2] - ); +void parseSatPrns(string& line) +{ + const char* buff = line.c_str(); - if (readcount == 10) - { - sst.agency = agency; - sst.start = start; - sst.stop = end; - sst.create = create; + SinexSatPrn spt; - // see comment at top of file - if (sst.start[0] != 0 || sst.start[1] != 0 || sst.start[2] != 0) - { - nearestYear(sst.start[0]); - } + spt.svn = line.substr(1, 4); + spt.prn = line.substr(36, 3); + spt.comment = line.substr(40); - if (sst.stop[0] != 0 || sst.stop[1] != 0 || sst.stop[2] != 0) - { - nearestYear(sst.stop[0]); - } + int readcount = sscanf( + buff + 6, + "%4lf:%3lf:%5lf %4lf:%3lf:%5lf", + &spt.start[0], + &spt.start[1], + &spt.start[2], + &spt.stop[0], + &spt.stop[1], + &spt.stop[2] + ); - nearestYear(sst.create[0]); + if (readcount == 6) + { + // No need to adjust years since for satellites the year is 4 digits ... + theSinex.listsatprns.push_back(spt); - theSinex.listsitedata.push_back(sst); + nav.svnMap[SatSys(spt.prn.c_str())][spt.start] = spt.svn; } } -void writeSnxSitedata(ofstream& out, list* pstns) +void parseSatFreqChannels(string& line) { - Block block(out, "SITE/DATA"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); + const char* buff = line.c_str(); - for (auto& sitedata : theSinex.listsitedata) - { - SinexSiteData& ssd = sitedata; - bool doit = false; + SinexSatFreqChn sfc; - char line[81]; - snprintf( - line, - sizeof(line), - " %4s %2s %4s %4s %2s %4s %c %2.2d:%3.3d:%5.5d %2.2d:%3.3d:%5.5d %3s %2.2d:%3.3d:%5.5d", - ssd.site.c_str(), - ssd.station_pt.c_str(), - ssd.soln_id.c_str(), - ssd.sitecode.c_str(), - ssd.site_pt.c_str(), - ssd.sitesoln.c_str(), - ssd.obscode, - (int)ssd.start[0] % 100, - (int)ssd.start[1], - (int)ssd.start[2], - (int)ssd.stop[0] % 100, - (int)ssd.stop[1], - (int)ssd.stop[2], - ssd.agency.c_str(), - (int)ssd.create[0] % 100, - (int)ssd.create[1], - (int)ssd.create[2] - ); + sfc.svn = line.substr(1, 4); + sfc.comment = line.substr(40); - if (pstns == nullptr) - doit = true; - else - { - for (auto& stn : *pstns) - { - if (ssd.site.compare(stn.id_ptr->sitecode) == 0) - { - doit = true; - break; - } - } - } + int readcount = sscanf( + buff + 6, + "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %3d", + &sfc.start[0], + &sfc.start[1], + &sfc.start[2], + &sfc.stop[0], + &sfc.stop[1], + &sfc.stop[2], + &sfc.channel + ); - if (doit) - out << line << "\n"; + if (readcount == 7) + { + // No need to adjust years since for satellites the year is 4 digits ... + theSinex.listsatfreqchns.push_back(sfc); } } -void parseReceivers(string& line) +void parseSatPlanes(string& line) { + int offset = 0; + if (line.empty() == false && line[0] == ' ') + { + offset = 1; + } + const char* buff = line.c_str(); - SinexReceiver srt; + SinexSatPlane spl; - srt.sitecode = trim(line.substr(1, 4)); - srt.ptcode = line.substr(6, 2); - srt.solnid = line.substr(9, 4); - srt.typecode = line[14]; - srt.type = line.substr(42, 20); - srt.sn = line.substr(63, 5); - srt.firm = trim(line.substr(69, 11)); - int readcount; + spl.svn = line.substr(offset, 4); - readcount = sscanf( - buff + 16, - "%2lf:%3lf:%5lf %2lf:%3lf:%5lf", - &srt.start[0], - &srt.start[1], - &srt.start[2], - &srt.end[0], - &srt.end[1], - &srt.end[2] + int readcount = sscanf( + buff + offset + 5, + "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %c", + &spl.start[0], + &spl.start[1], + &spl.start[2], + &spl.stop[0], + &spl.stop[1], + &spl.stop[2], + &spl.plane ); - if (readcount == 6) + if ((int)line.size() > offset + 37) { - // see comment at top of file - if (srt.start[0] != 0 || srt.start[1] != 0 || srt.start[2] != 0) - { - nearestYear(srt.start[0]); - } + spl.slot = trim(line.substr(offset + 37, 6)); + } - if (srt.end[0] != 0 || srt.end[1] != 0 || srt.end[2] != 0) - { - nearestYear(srt.end[0]); - } + if ((int)line.size() > offset + 44) + { + spl.comment = trim(line.substr(offset + 44)); + } - theSinex.mapreceivers[srt.sitecode][srt.start] = srt; + if (spl.slot.empty() == false && spl.slot[0] == '[') + { + spl.comment = trim(line.substr(offset + 37)); + spl.slot.clear(); + } + + if (readcount == 7) + { + theSinex.satPlaneMap[spl.svn][spl.start] = spl; } } -void writeSnxReceivers(ofstream& out) +void parseSatelliteMass(string& line) { - Block block(out, "SITE/RECEIVER"); + const char* buff = line.c_str(); - writeAsComments(out, theSinex.blockComments[block.blockName]); + SinexSatMass ssm; - for (auto& [site, timemap] : theSinex.mapreceivers) - for (auto it = timemap.rbegin(); it != timemap.rend(); it++) - { - auto& [time, receiver] = *it; + ssm.svn = line.substr(1, 4); + ssm.comment = line.substr(46); - if (receiver.used == false) - { - continue; - } + int readcount = sscanf( + buff + 6, + "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %9lf", + &ssm.start[0], + &ssm.start[1], + &ssm.start[2], + &ssm.stop[0], + &ssm.stop[1], + &ssm.stop[2], + &ssm.mass + ); - tracepdeex( - 0, - out, - " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %20s %5s %s\n", - receiver.sitecode.c_str(), - receiver.ptcode.c_str(), - receiver.solnid.c_str(), - receiver.typecode, - (int)receiver.start[0] % 100, - (int)receiver.start[1], - (int)receiver.start[2], - (int)receiver.end[0] % 100, - (int)receiver.end[1], - (int)receiver.end[2], - receiver.type.c_str(), - receiver.sn.c_str(), - receiver.firm.c_str() - ); - } + if (readcount == 7) + { + // No need to adjust years since for satellites the year is 4 digits ... + theSinex.mapsatmasses[ssm.svn][ssm.start] = ssm; + } } -void parseAntennas(string& line) +void parseSatelliteComs(string& line) { const char* buff = line.c_str(); - SinexAntenna ant; + SinexSatCom sct; - ant.sitecode = trim(line.substr(1, 4)); - ant.ptcode = line.substr(6, 2); - ant.solnnum = line.substr(9, 4); - ant.typecode = line[14]; - ant.type = line.substr(42, 20); - ant.sn = trim(line.substr(63, 5)); + sct.svn = line.substr(1, 4); + sct.comment = line.substr(66); int readcount = sscanf( - buff + 16, - "%2lf:%3lf:%5lf %2lf:%3lf:%5lf", - &ant.start[0], - &ant.start[1], - &ant.start[2], - &ant.end[0], - &ant.end[1], - &ant.end[2] + buff + 6, + "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %9lf %9lf %9lf", + &sct.start[0], + &sct.start[1], + &sct.start[2], + &sct.stop[0], + &sct.stop[1], + &sct.stop[2], + &sct.com[0], + &sct.com[1], + &sct.com[2] ); - if (readcount == 6) + if (readcount == 9) { - // see comment at top of file - if (ant.start[0] != 0 || ant.start[1] != 0 || ant.start[2] != 0) - { - nearestYear(ant.start[0]); - } - - if (ant.end[0] != 0 || ant.end[1] != 0 || ant.end[2] != 0) - { - nearestYear(ant.end[0]); - } - - theSinex.mapantennas[ant.sitecode][ant.start] = ant; - // theSinex.list_antennas.push_back(ant); + // No need to adjust years since for satellites the year is 4 digits ... + theSinex.listsatcoms.push_back(sct); } } -void writeSnxAntennas(ofstream& out) +void parseSatelliteEccentricities(string& line) { - Block block(out, "SITE/ANTENNA"); + const char* buff = line.c_str(); - writeAsComments(out, theSinex.blockComments[block.blockName]); + SinexSatEcc set; - for (auto& [site, antmap] : theSinex.mapantennas) - for (auto it = antmap.rbegin(); it != antmap.rend(); it++) - { - auto& [time, ant] = *it; + set.svn = line.substr(1, 4); + set.equip = line.substr(6, 20); + set.type = line[27]; + set.comment = line.substr(59); - if (ant.used == false) - { - continue; - } + int readcount = sscanf(buff + 29, "%9lf %9lf %9lf", &set.ecc[0], &set.ecc[1], &set.ecc[2]); - tracepdeex( - 0, - out, - " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %20s %s\n", - ant.sitecode.c_str(), - ant.ptcode.c_str(), - ant.solnnum.c_str(), - ant.typecode, - (int)ant.start[0] % 100, - (int)ant.start[1], - (int)ant.start[2], - (int)ant.end[0] % 100, - (int)ant.end[1], - (int)ant.end[2], - ant.type.c_str(), - ant.sn.c_str() - ); - } + if (readcount == 3) + { + theSinex.listsateccs.push_back(set); + } } -// compare by antenna type and serial number. -bool compareGpsPc(SinexGpsPhaseCenter& left, SinexGpsPhaseCenter& right) +void parseSatellitePowers(string& line) { - int comp = left.antname.compare(right.antname); - - if (comp == 0) - comp = left.serialno.compare(right.serialno); - - return (comp < 0); -} + const char* buff = line.c_str(); -void parseGpsPhaseCenters(string& line) -{ - const char* buff = line.c_str(); - SinexGpsPhaseCenter sgpct; + SinexSatPower spt; - sgpct.antname = line.substr(1, 20); - sgpct.serialno = line.substr(22, 5); - sgpct.calib = line.substr(70, 10); + spt.svn = line.substr(1, 4); + spt.comment = line.substr(41); int readcount = sscanf( - buff + 28, - "%6lf %6lf %6lf %6lf %6lf %6lf", - &sgpct.L1[0], - &sgpct.L1[1], - &sgpct.L1[2], - &sgpct.L2[0], - &sgpct.L2[1], - &sgpct.L2[2] + buff + 6, + "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %4d", + &spt.start[0], + &spt.start[1], + &spt.start[2], + &spt.stop[0], + &spt.stop[1], + &spt.stop[2], + &spt.power ); - if (readcount == 6) + if (readcount == 7) { - theSinex.listgpspcs.push_back(sgpct); + // No need to adjust years since for satellites the year is 4 digits ... + theSinex.mapsatpowers[spt.svn][spt.start] = spt; } } -void truncateSomething(char* buf) +void parseSinexSatYawRates(string& line) { - if (strlen(buf) == 7 && buf[1] == '0' && buf[0] == '-') + const char* buff = line.c_str(); + + SinexSatYawRate entry; + + entry.svn = line.substr(1, 4); + entry.comment = line.substr(51); + + int readCount = sscanf( + buff + 6, + "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %c %8lf", + &entry.start[0], + &entry.start[1], + &entry.start[2], + &entry.stop[0], + &entry.stop[1], + &entry.stop[2], + &entry.yawBias, + &entry.maxYawRate + ); + + entry.maxYawRate *= D2R; + + if (readCount == 8) { - for (int j = 2; j < 8; j++) - { - buf[j - 1] = buf[j]; - } + theSinex.satYawRateMap[entry.svn][entry.start] = entry; } } -void writeSnxGpsPcs(ofstream& out, list* pstns) +void parseSinexSatAttMode(string& line) { - Block block(out, "SITE/GPS_PHASE_CENTER"); + const char* buff = line.c_str(); - writeAsComments(out, theSinex.blockComments[block.blockName]); + SinexSatAttMode entry; + entry.svn = line.substr(1, 4); + int readCount = sscanf( + buff + 6, + "%4lf-%2lf-%2lf %2lf:%2lf:%2lf %4lf-%2lf-%2lf %2lf:%2lf:%2lf ", + &entry.start[0], + &entry.start[1], + &entry.start[2], + &entry.start[3], + &entry.start[4], + &entry.start[5], + &entry.stop[0], + &entry.stop[1], + &entry.stop[2], + &entry.stop[3], + &entry.stop[4], + &entry.stop[5] + ); + entry.attMode = line.substr(47); - for (auto& gps_pc : theSinex.listgpspcs) + if (readCount == 12) { - SinexGpsPhaseCenter& sgt = gps_pc; - char buf[8]; - bool doit = false; - - char line[81]; - int offset = 0; + theSinex.satAttModeMap[entry.svn][entry.start] = entry; + } +} - offset += snprintf( - line + offset, - sizeof(line) - offset, - " %20s %5s ", - sgt.antname.c_str(), - sgt.serialno.c_str() - ); +void parseEpochs(string& line) +{ + const char* buff = line.c_str(); - for (int i = 0; i < 3; i++) - { - snprintf(buf, sizeof(buf), "%6.4lf", sgt.L1[i]); - truncateSomething(buf); - offset += snprintf(line + offset, sizeof(line) - offset, "%s", buf); - } + SinexSolEpoch sst; - for (int i = 0; i < 3; i++) - { - snprintf(buf, sizeof(buf), "%6.4lf", sgt.L2[i]); - truncateSomething(buf); - offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); - } + sst.sitecode = trim(line.substr(1, 4)); + sst.ptcode = line.substr(6, 2); + sst.solnnum = line.substr(9, 4); + sst.typecode = line[14]; - offset += snprintf(line + offset, sizeof(line) - offset, "%s", sgt.calib.c_str()); + int readcount = sscanf( + buff + 16, + "%2lf:%3lf:%5lf %2lf:%3lf:%5lf %2lf:%3lf:%5lf", + &sst.start[0], + &sst.start[1], + &sst.start[2], + &sst.end[0], + &sst.end[1], + &sst.end[2], + &sst.mean[0], + &sst.mean[1], + &sst.mean[2] + ); - if (pstns == nullptr) + if (readcount == 9) + { + // see comment at top of file + if (sst.start[0] != 0 || sst.start[1] != 0 || sst.start[2] != 0) { - doit = true; + nearestYear(sst.start[0]); } - else + + if (sst.end[0] != 0 || sst.end[1] != 0 || sst.end[2] != 0) { - for (auto& stn : *pstns) - { - if (sgt.antname == stn.ant_ptr->type) - { - doit = true; - break; - } - } + nearestYear(sst.end[0]); } - if (doit) + if (sst.mean[0] != 0 || sst.mean[1] != 0 || sst.mean[2] != 0) { - out << line << "\n"; + nearestYear(sst.mean[0]); } } } -// compare by antenna type and serial number. return true0 if left < right -bool compareGalPc(SinexGalPhaseCenter& left, SinexGalPhaseCenter& right) +void parseStatistics(string& line) // todo? is this type stuff really necessary { - int comp = left.antname.compare(right.antname); + const char* buff = line.c_str(); - if (comp == 0) - comp = left.serialno.compare(right.serialno); - - return (comp < 0); -} - -// Gallileo phase centers take three line each! -void parseGalPhaseCenters(string& s_x) -{ - static int lineNum = 0; - static string lines[3]; - lines[lineNum] = s_x; + string stat = line.substr(1, 30); + double dval; + int ival; + short etype; - lineNum++; - if (lineNum != 3) + if (line.substr(33).find(".") != string::npos) { - // wait for 3 lines. - return; + dval = (double)atof(buff + 33); + etype = 1; + } + else + { + ival = atoi(buff + 33); + etype = 0; } - lineNum = 0; - - auto& line1 = lines[0]; - auto& line2 = lines[1]; - auto& line3 = lines[2]; - - SinexGalPhaseCenter sgpct; - - sgpct.antname = line1.substr(1, 20); - sgpct.serialno = line1.substr(22, 5); - sgpct.calib = line1.substr(69, 10); + SinexSolStatistic sst; + sst.name = trim(stat); + sst.etype = etype; - int readcount1 = sscanf( - line1.c_str() + 28, - "%6lf %6lf %6lf %6lf %6lf %6lf", - &sgpct.L1[0], - &sgpct.L1[1], - &sgpct.L1[2], - &sgpct.L5[0], - &sgpct.L5[1], - &sgpct.L5[2] - ); + if (etype == 0) + sst.value.ival = ival; - // Do we need to check the antenna name and serial each time? I am going to assume not - int readcount2 = sscanf( - line2.c_str() + 28, - "%6lf %6lf %6lf %6lf %6lf %6lf", - &sgpct.L6[0], - &sgpct.L6[1], - &sgpct.L6[2], - &sgpct.L7[0], - &sgpct.L7[1], - &sgpct.L7[2] - ); - int readcount3 = - sscanf(line3.c_str() + 28, "%6lf %6lf %6lf", &sgpct.L8[0], &sgpct.L8[1], &sgpct.L8[2]); + if (etype == 1) + sst.value.dval = dval; - if (readcount1 == 6 && readcount2 == 6 && readcount3 == 3) - { - theSinex.listgalpcs.push_back(sgpct); - } + theSinex.liststatistics.push_back(sst); } -void writeSnxGalPcs(ofstream& out, list* pstns) +void parseSolutionEstimates(string& line) { - Block block(out, "SITE/GAL_PHASE_CENTER"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& gal_pc : theSinex.listgalpcs) - { - SinexGalPhaseCenter& sgt = gal_pc; - char buf[8]; - bool doit = false; + const char* buff = line.c_str(); - if (pstns == nullptr) - doit = true; - else - { - for (auto& stn : *pstns) - { - if (sgt.antname == stn.ant_ptr->type) - { - doit = true; - break; - } - } - } + SinexSolEstimate sst; - if (!doit) - continue; + sst.file = theSinex.currentFile; + sst.type = line.substr(7, 6); + sst.sitecode = line.substr(14, 4); + sst.ptcode = line.substr(19, 2); + sst.solnnum = line.substr(22, 4); - { - char line[81]; - int offset = 0; + sst.index = (int)str2num(buff, 1, 5); - offset += snprintf( - line + offset, - sizeof(line) - offset, - " %20s %5s ", - sgt.antname.c_str(), - sgt.serialno.c_str() - ); + int readcount = + sscanf(buff + 27, "%2lf:%3lf:%5lf", &sst.refepoch[0], &sst.refepoch[1], &sst.refepoch[2]); - for (int i = 0; i < 3; i++) - { - snprintf(buf, sizeof(buf), "%6.4lf", sgt.L1[i]); - truncateSomething(buf); - offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); - } + sst.unit = line.substr(40, 4); - for (int i = 0; i < 3; i++) - { - snprintf(buf, sizeof(buf), "%6.4lf", sgt.L5[i]); - truncateSomething(buf); - offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); - } + sst.constraint = line[45]; - offset += snprintf(line + offset, sizeof(line) - offset, "%s", sgt.calib.c_str()); - out << line << "\n"; - } + readcount += sscanf(buff + 47, "%21lf %11lf", &sst.estimate, &sst.stddev); + if (readcount == 5) + { + // see comment at top of file + if (sst.refepoch[0] != 0 || sst.refepoch[1] != 0 || sst.refepoch[2] != 0) { - char line[81]; - int offset = 0; - - offset += snprintf( - line + offset, - sizeof(line) - offset, - " %20s %5s ", - sgt.antname.c_str(), - sgt.serialno.c_str() - ); - - for (int i = 0; i < 3; i++) - { - snprintf(buf, sizeof(buf), "%6.4lf", sgt.L6[i]); - truncateSomething(buf); - offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); - } - - for (int i = 0; i < 3; i++) - { - snprintf(buf, sizeof(buf), "%6.4lf", sgt.L7[i]); - truncateSomething(buf); - offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); - } - - offset += snprintf(line + offset, sizeof(line) - offset, "%s", sgt.calib.c_str()); - - out << line << "\n"; + nearestYear(sst.refepoch[0]); } + auto& typeMap = theSinex.estimatesMap[sst.sitecode][sst.type]; + if (!typeMap.empty()) { - char line[81]; - int offset = 0; - - offset += snprintf( - line, - sizeof(line), - " %20s %5s ", - sgt.antname.c_str(), - sgt.serialno.c_str() - ); + auto& firstEntry = typeMap.begin()->second; - for (int i = 0; i < 3; i++) + if (firstEntry.file != sst.file) { - snprintf(buf, sizeof(buf), "%6.4lf", sgt.L8[i]); - truncateSomething(buf); - offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); + BOOST_LOG_TRIVIAL(debug) + << "Clearing sinex estimates for " << firstEntry.sitecode << " type " + << sst.type << " from " << firstEntry.file << " as it is being overwritten by " + << theSinex.currentFile; + typeMap.clear(); } - - offset += snprintf(line + offset, sizeof(line) - offset, " "); - offset += snprintf(line + offset, sizeof(line) - offset, "%s", sgt.calib.c_str()); - out << line << "\n"; } + + theSinex.estimatesMap[sst.sitecode][sst.type][sst.refepoch] = sst; } } -void parseSiteEccentricity(string& line) +void parseApriori(string& line) { - const char* buff = line.c_str(); - SinexSiteEcc sset; + const char* buff = line.c_str(); - sset.sitecode = trim(line.substr(1, 4)); - sset.ptcode = line.substr(6, 2); - sset.solnnum = line.substr(9, 4); - sset.typecode = line[14]; - sset.rs = line.substr(42, 3); - char junk[4]; + SinexSolApriori sst = {}; + + sst.idx = (int)str2num(buff, 1, 5); + sst.param_type = line.substr(7, 6); + sst.sitecode = line.substr(14, 4); + sst.ptcode = line.substr(19, 2); + sst.solnnum = line.substr(22, 4); + + char unit[5]; + + unit[4] = '\0'; int readcount = sscanf( - buff + 16, - "%2lf:%3lf:%5lf %2lf:%3lf:%5lf %3s %8lf %8lf %8lf", - &sset.start[0], - &sset.start[1], - &sset.start[2], - &sset.end[0], - &sset.end[1], - &sset.end[2], - junk, - &sset.ecc.u(), - &sset.ecc.n(), - &sset.ecc.e() + buff + 27, + "%2lf:%3lf:%5lf %4s %c %21lf %11lf", + &sst.epoch[0], + &sst.epoch[1], + &sst.epoch[2], + unit, + &sst.constraint, + &sst.param, + &sst.stddev ); - if (readcount == 10) + if (readcount == 7) { - // see comment at top of file - if (sset.start[0] != 0 || sset.start[1] != 0 || sset.start[2] != 0) - { - nearestYear(sset.start[0]); - } + sst.unit = unit; - if (sset.end[0] != 0 || sset.end[1] != 0 || sset.end[2] != 0) + // see comment at top of file + if (sst.epoch[0] != 0 || sst.epoch[1] != 0 || sst.epoch[2] != 0) { - nearestYear(sset.end[0]); + nearestYear(sst.epoch[0]); } - theSinex.mapeccentricities[sset.sitecode][sset.start] = sset; + theSinex.apriorimap[sst.idx] = sst; } } -void writeSnxSiteEccs(ofstream& out) +void parseNormals(string& line) { - Block block(out, "SITE/ECCENTRICITY"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); + const char* buff = line.c_str(); - for (auto& [id, setMap] : theSinex.mapeccentricities) - for (auto it = setMap.rbegin(); it != setMap.rend(); it++) - { - auto& [time, set] = *it; + SinexSolNeq sst; - if (set.used == false) - { - continue; - } + sst.param = (int)str2num(buff, 2, 5); + sst.ptype = line.substr(7, 6); + sst.site = line.substr(14, 4); + sst.pt = line.substr(19, 2); + sst.solnnum = line.substr(22, 4); + char unit[5]; - tracepdeex( - 0, - out, - " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %3s %8.4lf %8.4lf %8.4lf\n", - set.sitecode.c_str(), - set.ptcode.c_str(), - set.solnnum.c_str(), - set.typecode, - (int)set.start[0] % 100, - (int)set.start[1], - (int)set.start[2], - (int)set.end[0] % 100, - (int)set.end[1], - (int)set.end[2], - set.rs.c_str(), - set.ecc.u(), - set.ecc.n(), - set.ecc.e() - ); - } -} + unit[4] = '\0'; -bool compareSiteEpochs(SinexSolEpoch& left, SinexSolEpoch& right) -{ - int comp = left.sitecode.compare(right.sitecode); - int i = 0; + int readcount = sscanf( + buff + 27, + "%2lf:%3lf:%5lf %4s %c %21lf", + &sst.epoch[0], + &sst.epoch[1], + &sst.epoch[2], + unit, + &sst.constraint, + &sst.normal + ); - while (comp == 0 && i < 3) + if (readcount == 6) { - comp = left.start[i] - right.start[i]; - i++; - } + sst.unit = unit; - return (comp < 0); + // see comment at top of file + if (sst.epoch[0] != 0 || sst.epoch[1] != 0 || sst.epoch[2] != 0) + { + nearestYear(sst.epoch[0]); + } + + theSinex.listnormaleqns.push_back(sst); + } } -void parseEpochs(string& line) +matrix_type mat_type; +matrix_value mat_value; + +void parseMatrix(string& line) //, matrix_type type, matrix_value value) { const char* buff = line.c_str(); - SinexSolEpoch sst; - - sst.sitecode = trim(line.substr(1, 4)); - sst.ptcode = line.substr(6, 2); - sst.solnnum = line.substr(9, 4); - sst.typecode = line[14]; + // //todo? this is only half complete, the maxrow/col arent used but should be with + // multiple input matrices. + int maxrow = 0; + int maxcol = 0; + SinexSolMatrix smt; int readcount = sscanf( - buff + 16, - "%2lf:%3lf:%5lf %2lf:%3lf:%5lf %2lf:%3lf:%5lf", - &sst.start[0], - &sst.start[1], - &sst.start[2], - &sst.end[0], - &sst.end[1], - &sst.end[2], - &sst.mean[0], - &sst.mean[1], - &sst.mean[2] + buff, + " %5d %5d %21lf %21lf %21lf", + &smt.row, + &smt.col, + &smt.value[0], + &smt.value[1], + &smt.value[2] ); - if (readcount == 9) + if (readcount > 2) { - // see comment at top of file - if (sst.start[0] != 0 || sst.start[1] != 0 || sst.start[2] != 0) - { - nearestYear(sst.start[0]); - } - - if (sst.end[0] != 0 || sst.end[1] != 0 || sst.end[2] != 0) + if (smt.row < smt.col) { - nearestYear(sst.end[0]); + // xor swap + smt.row ^= smt.col; + smt.col ^= smt.row; + smt.row ^= smt.col; } - if (sst.mean[0] != 0 || sst.mean[1] != 0 || sst.mean[2] != 0) - { - nearestYear(sst.mean[0]); - } - } -} + int covars = readcount - 2; -void writeSnxEpochs(Trace& out) -{ - string blockName; - if (theSinex.epochshavebias) - blockName = "BIAS/EPOCHS"; - else - blockName = "SOLUTION/EPOCHS"; + for (int i = readcount - 2; i < 3; i++) + smt.value[i] = -1; - Block block(out, blockName); + smt.numvals = readcount - 2; - writeAsComments(out, theSinex.blockComments[block.blockName]); + if (smt.row > maxrow) + maxrow = smt.row; + if (smt.col > maxcol) + maxcol = smt.col; - for (auto& [id, sst] : theSinex.solEpochMap) - { - tracepdeex( - 0, - out, - " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %02d:%03d:%05d\n", - sst.sitecode.c_str(), - sst.ptcode.c_str(), - sst.solnnum.c_str(), - sst.typecode, - (int)sst.start[0] % 100, - (int)sst.start[1], - (int)sst.start[2], - (int)sst.end[0] % 100, - (int)sst.end[1], - (int)sst.end[2], - (int)sst.mean[0] % 100, - (int)sst.mean[1], - (int)sst.mean[2] - ); + theSinex.matrixmap[mat_type][mat_value].push_back( + smt + ); // todo? initialise mat_type and mat_value before use } } -void parseStatistics(string& line) // todo aaron, is this type stuff really necessary +void parseDataHandling(string& line) { + SinexDataHandling sdt; + const char* buff = line.c_str(); - string stat = line.substr(1, 30); - double dval; - int ival; - short etype; + sdt.sitecode = trim(line.substr(1, 4)); // 4 - CDP ID + sdt.ptcode = line.substr(6, 2); // 2 - satellites these biases apply to (-- = all) + sdt.solnnum = line.substr(9, 4); // 4 - solution number + sdt.t = line.substr(14, 1); // 1 - if (line.substr(33).find(".") != string::npos) + int readcount = sscanf( + buff + 16, + "%2lf:%3lf:%5lf", + &sdt.epochstart[0], + &sdt.epochstart[1], + &sdt.epochstart[2] + ); + readcount += + sscanf(buff + 29, "%2lf:%3lf:%5lf", &sdt.epochend[0], &sdt.epochend[1], &sdt.epochend[2]); + + sdt.m = line.substr(42, 1); // 1 + + if (line.size() >= 79) { - dval = (double)atof(buff + 33); - etype = 1; + sdt.estimate = str2num(buff, 44, 12); + sdt.stddev = str2num(buff, 57, 7); + sdt.estrate = str2num(buff, 65, 9); + sdt.unit = line.substr(75, 4); // 4 - units of estimate } - else + if (line.size() > 82) { - ival = atoi(buff + 33); - etype = 0; + sdt.comments = line.substr(82); } - SinexSolStatistic sst; - sst.name = trim(stat); - sst.etype = etype; - - if (etype == 0) - sst.value.ival = ival; + if (readcount >= 6) // just need a start & stop time + { + // see comment at top of file + if (sdt.epochstart[0] != 0 || sdt.epochstart[1] != 0 || sdt.epochstart[2] != 0) + { + nearestYear(sdt.epochstart[0]); + } + if (sdt.epochend[0] != 0 || sdt.epochend[1] != 0 || sdt.epochend[2] != 0) + { + nearestYear(sdt.epochend[0]); + } - if (etype == 1) - sst.value.dval = dval; + GTime time = sdt.epochstart; - theSinex.liststatistics.push_back(sst); + theSinex.mapdatahandling[sdt.sitecode][sdt.ptcode][sdt.m.front()][time] = sdt; + } } -void writeSnxStatistics(ofstream& out) +bool readSinex(const string& filepath) { - Block block(out, "SOLUTION/STATISTICS"); + // BOOST_LOG_TRIVIAL(info) + // << "reading " << filepath; - writeAsComments(out, theSinex.blockComments[block.blockName]); + ifstream filestream(filepath); + if (!filestream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening sinex file" << filepath; + return false; + } - for (auto& statistic : theSinex.liststatistics) + bool pass = readSnxHeader(filestream); + if (pass == false) { - char line[81]; + BOOST_LOG_TRIVIAL(error) << "Error reading header line."; - if (statistic.etype == 0) // int - snprintf( - line, - sizeof(line), - " %-30s %22d", - statistic.name.c_str(), - statistic.value.ival - ); + return false; + } - if (statistic.etype == 1) // double - snprintf( - line, - sizeof(line), - " %-30s %22.15lf", - statistic.name.c_str(), - statistic.value.dval - ); + theSinex.currentFile = filepath; - out << line << "\n"; - } -} + bool warnedBlockSkip = false; -void parseSolutionEstimates(string& line) -{ - const char* buff = line.c_str(); + void (*parseFunction)(string&) = nullFunction; - SinexSolEstimate sst; + string closure; - sst.file = theSinex.currentFile; - sst.type = line.substr(7, 6); - sst.sitecode = line.substr(14, 4); - sst.ptcode = line.substr(19, 2); - sst.solnnum = line.substr(22, 4); + bool failure = false; - sst.index = (int)str2num(buff, 1, 5); + int lineNumber = 0; - int readcount = - sscanf(buff + 27, "%2lf:%3lf:%5lf", &sst.refepoch[0], &sst.refepoch[1], &sst.refepoch[2]); + while (filestream) + { + string line; - sst.unit = line.substr(40, 4); + getline(filestream, line); - sst.constraint = line[45]; + lineNumber++; - readcount += sscanf(buff + 47, "%21lf %11lf", &sst.estimate, &sst.stddev); + // test below empty line (ie continue if something on the line) + if (!filestream) + { + // error - did not find closure line. Report and clean up. + BOOST_LOG_TRIVIAL(error) << "Closure line not found before end."; - if (readcount == 5) - { - // see comment at top of file - if (sst.refepoch[0] != 0 || sst.refepoch[1] != 0 || sst.refepoch[2] != 0) + failure = true; + break; + } + else if (line[0] == '*') { - nearestYear(sst.refepoch[0]); + // comment } - - auto it = theSinex.estimatesMap.find(sst.sitecode); - if (it != theSinex.estimatesMap.end()) + else if (line[0] == '-') { - auto& firstEntry = it->second.begin()->second.begin()->second; + // end of block + parseFunction = nullFunction; - if (firstEntry.file != sst.file) + if (line != closure) { - BOOST_LOG_TRIVIAL(debug) - << "Clearing sinex data for " << firstEntry.sitecode << " from " - << firstEntry.file << " as it is being overwritten by " << theSinex.currentFile; - theSinex.estimatesMap[sst.sitecode].clear(); + BOOST_LOG_TRIVIAL(error) << "Incorrect section closure line encountered on line " + << lineNumber << ": " << closure << " != " << line; } } - theSinex.estimatesMap[sst.sitecode][sst.type][sst.refepoch] = sst; - } -} - -void writeSnxEstimatesFromFilter(ofstream& out, KFState& kfState) -{ - Block block(out, "SOLUTION/ESTIMATE"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - for (auto& [key, index] : kfState.kfIndexMap) - { - if (key.type != KF::REC_POS && key.type != KF::REC_POS_RATE && key.type != KF::STRAIN_RATE) + else if (line[0] == ' ') { - continue; + try + { + // this probably needs specialty parsing - use a prepared function pointer. + parseFunction(line); + } + catch (std::out_of_range& e) + { + BOOST_LOG_TRIVIAL(error) + << "Sinex line width error on line " << lineNumber << ": '" << line << "'"; + } + catch (...) + { + BOOST_LOG_TRIVIAL(error) + << "Sinex parsing error on line " << lineNumber << ": '" << line << "'"; + } } - - string type; - if (key.type == KF::REC_POS) - type = "STA?"; - else if (key.type == KF::REC_POS_RATE) - type = "VEL?"; - else if (key.type == KF::STRAIN_RATE) - type = "VEL?"; // todo aaron, scale is wrong, actually entirely untested - - if (key.num == 0) - type[3] = 'X'; - else if (key.num == 1) - type[3] = 'Y'; - else if (key.num == 2) - type[3] = 'Z'; - - string ptcode = theSinex.mapsiteids[key.str].ptcode; - - tracepdeex( - 0, - out, - " %5d %-6s %4s %2s %4d %02d:%03d:%05d %-4s %c %21.14le %11.5le\n", - index, - type.c_str(), - key.str.c_str(), - ptcode.c_str(), - 1, - (int)theSinex.solutionenddate[0] % 100, - (int)theSinex.solutionenddate[1], - (int)theSinex.solutionenddate[2], - "m", - '9', // TODO: replace with sst.constraint when fixed - kfState.x(index), - sqrt(kfState.P(index, index)) - ); - } -} - -// void write_snx_estimates( -// ofstream& out, -// std::list* pstns = nullptr) -// { -// out << "+SOLUTION/ESTIMATE" << "\n"; -// -// writeAsComments(out, theSinex.estimate_comments); -// -// for (auto& [index, sst] : theSinex.estimates_map) -// { -// bool doit = (pstns == nullptr); -// -// if (pstns != nullptr) -// { -// for (auto& stn : *pstns) -// { -// if (sst.sitecode.compare(stn.sitecode) == 0) -// { -// doit = true; -// break; -// } -// } -// } -// -// if (!doit) -// continue; -// -// char line[82]; -// -// snprintf(line, sizeof(line), " %5d %6s %4s %2s %4s %2.2d:%3.3d:%5.5d %-4s %c %21.14le -// %11.5le", sst.index, sst.type.c_str(), sst.sitecode.c_str(), -// sst.ptcode.c_str(), sst.solnnum.c_str(), sst.refepoch[0] % 100, -// sst.refepoch[1], sst.refepoch[2], sst.unit.c_str(), -// sst.constraint, sst.estimate, sst.stddev); -// -// out << line << "\n"; -// } -// -// out << "-SOLUTION/ESTIMATE" << "\n"; -// } - -void parseApriori(string& line) -{ - const char* buff = line.c_str(); - - SinexSolApriori sst = {}; - - sst.idx = (int)str2num(buff, 1, 5); - sst.param_type = line.substr(7, 6); - sst.sitecode = line.substr(14, 4); - sst.ptcode = line.substr(19, 2); - sst.solnnum = line.substr(22, 4); - - char unit[5]; - - unit[4] = '\0'; - - int readcount = sscanf( - buff + 27, - "%2lf:%3lf:%5lf %4s %c %21lf %11lf", - &sst.epoch[0], - &sst.epoch[1], - &sst.epoch[2], - unit, - &sst.constraint, - &sst.param, - &sst.stddev - ); - - if (readcount == 7) - { - sst.unit = unit; - - // see comment at top of file - if (sst.epoch[0] != 0 || sst.epoch[1] != 0 || sst.epoch[2] != 0) + else if (line[0] == '+') { - nearestYear(sst.epoch[0]); - } - - theSinex.apriorimap[sst.idx] = sst; - } -} - -void writeSnxApriori(ofstream& out, list* pstns = nullptr) -{ - Block block(out, "SOLUTION/APRIORI"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); + string mvs; - for (auto& [index, apriori] : theSinex.apriorimap) - { - SinexSolApriori& sst = apriori; - bool doit = (pstns == nullptr); + // prepare closing line for comparison + closure = line; + closure[0] = '-'; - if (pstns) - for (auto& stn : *pstns) + trimCut(line); + if (sinexBlockExcluded(line.substr(1))) { - if (sst.sitecode.compare(stn.id_ptr->sitecode) == 0) + parseFunction = nullFunction; + if (warnedBlockSkip == false) { - doit = true; - break; + BOOST_LOG_TRIVIAL(info) + << "SINEX block exclusion active, skipping one or more blocks in " + << filepath; + warnedBlockSkip = true; } } + else if (line == "+FILE/REFERENCE") + { + parseFunction = parseReference; + } + else if (line == "+FILE/COMMENT") + { + parseFunction = nullFunction; + } + else if (line == "+INPUT/HISTORY") + { + parseFunction = parseInputHistory; + } + else if (line == "+INPUT/FILES") + { + parseFunction = parseInputFiles; + } + else if (line == "+INPUT/ACKNOWLEDGEMENTS") + { + parseFunction = parseAcknowledgements; + } + else if (line == "+INPUT/ACKNOWLEDGMENTS") + { + parseFunction = parseAcknowledgements; + } + else if (line == "+NUTATION/DATA") + { + parseFunction = parseNutcode; + } + else if (line == "+PRECESSION/DATA") + { + parseFunction = parsePrecode; + } + else if (line == "+SOURCE/ID") + { + parseFunction = parseSourceIds; + } + else if (line == "+SITE/ID") + { + parseFunction = parseSiteIds; + } + else if (line == "+SITE/DATA") + { + parseFunction = parseSiteData; + } + else if (line == "+SITE/RECEIVER") + { + parseFunction = parseReceivers; + } + else if (line == "+SITE/ANTENNA") + { + parseFunction = parseAntennas; + } + else if (line == "+SITE/GPS_PHASE_CENTER") + { + parseFunction = parseGpsPhaseCenters; + } + else if (line == "+SITE/GAL_PHASE_CENTER") + { + parseFunction = parseGalPhaseCenters; + } + else if (line == "+SITE/ECCENTRICITY") + { + parseFunction = parseSiteEccentricity; + } + else if (line == "+SATELLITE/ID") + { + parseFunction = parseSatelliteIds; + } + else if (line == "+SATELLITE/PHASE_CENTER") + { + parseFunction = parseSatellitePhaseCenters; + } + else if (line == "+SATELLITE/IDENTIFIER") + { + parseFunction = parseSatelliteIdentifiers; + } + else if (line == "+SATELLITE/PRN") + { + parseFunction = parseSatPrns; + } + else if (line == "+SATELLITE/FREQUENCY_CHANNEL") + { + parseFunction = parseSatFreqChannels; + } + else if (line == "+SATELLITE/PLANE") + { + parseFunction = parseSatPlanes; + } + else if (line == "+SATELLITE/MASS") + { + parseFunction = parseSatelliteMass; + } + else if (line == "+SATELLITE/COM") + { + parseFunction = parseSatelliteComs; + } + else if (line == "+SATELLITE/ECCENTRICITY") + { + parseFunction = parseSatelliteEccentricities; + } + else if (line == "+SATELLITE/TX_POWER") + { + parseFunction = parseSatellitePowers; + } + else if (line == "+SATELLITE/YAW_BIAS_RATE") + { + parseFunction = parseSinexSatYawRates; + } + else if (line == "+SATELLITE/ATTITUDE_MODE") + { + parseFunction = parseSinexSatAttMode; + } + else if (line == "+BIAS/EPOCHS") + { + parseFunction = parseEpochs; + } + else if (line == "+SOLUTION/EPOCHS") + { + parseFunction = parseEpochs; + } + else if (line == "+SOLUTION/STATISTICS") + { + parseFunction = parseStatistics; + } + else if (line == "+SOLUTION/ESTIMATE") + { + parseFunction = parseSolutionEstimates; + } + else if (line == "+SOLUTION/APRIORI") + { + parseFunction = parseApriori; + } + else if (line == "+SOLUTION/MATRIX_ESTIMATE") + { + parseFunction = parseMatrix; + } + else if (line == "+SOLUTION/MATRIX_APRIORI") + { + parseFunction = parseMatrix; + } + else if (line == "+SOLUTION/NORMAL_EQUATION_VECTOR") + { + parseFunction = parseNormals; + } + else if (line == "+SOLUTION/NORMAL_EQUATION_MATRIX") + { + parseFunction = parseMatrix; + } + else if (line == "+SOLUTION/DATA_HANDLING") + { + parseFunction = parseDataHandling; + } + else if (line == "+MODEL/RANGE_BIAS") + { + parseFunction = parseDataHandling; + } // Same format w/ SOLUTION/DATA_HANDLING + else if (line == "+MODEL/TIME_BIAS") + { + parseFunction = parseDataHandling; + } // Same format w/ SOLUTION/DATA_HANDLING + else + { + parseFunction = nullFunction; + BOOST_LOG_TRIVIAL(warning) << "Unknown header line: " << line; + } // Skip unknown sections + + // int i; + // failure = read_snx_matrix (filestream, + // NORMAL_EQN, INFORMATION, c); break; case 15: if + // (!theSinex.epochs_have_bias + // && !theSinex.list_solepochs.empty()) + // { + // BOOST_LOG_TRIVIAL(error) + // << "Cannot combine BIAS/EPOCHS and SOLUTION/EPOCHS blocks."; + // + // failure = true; + // break; + // } + // + // theSinex.epochs_have_bias = true; + // theSinex.epochcomments.insert(theSinex.epochcomments.end(), + // comments.begin(), comments.end()); comments.clear(); + // failure = read_snx_epochs(filestream, true); break; + // + // case 16: + // if (theSinex.epochs_have_bias && !theSinex.list_solepochs.empty()) + // { + // BOOST_LOG_TRIVIAL(error) + // << "Cannot combine BIAS/EPOCHS and SOLUTION/EPOCHS blocks."; + // + // failure = true; + // break; + // } + // + // theSinex.epochs_have_bias = false; + // theSinex.epochcomments .insert(theSinex.epochcomments.end(), + // comments.begin(), comments.end()); comments.clear(); + // + // failure = read_snx_epochs(filestream, false); + // break; + // + // case 21: + // theSinex.matrix_comments.insert(theSinex.matrix_comments.end(), + // comments.begin(), comments.end()); comments.clear(); + // c = line[headers[i].length() + 2]; mvs = + // line.substr(headers[i].length() + 4, 4); + // + // if (!mvs.compare("CORR")) mv = CORRELATION; + // else if (!mvs.compare("COVA")) mv = COVARIANCE; + // else if (!mvs.compare("INFO")) mv = INFORMATION; + // + // failure = read_snx_matrix(filestream, ESTIMATE, mv, c); + // break; + // + // case 22: + // theSinex.matrix_comments.insert(theSinex.matrix_comments.end(), + // comments.begin(), comments.end()); comments.clear(); + // c = line[headers[i].length() + 2]; mvs = + // line.substr(headers[i].length() + 4, 4); + // + // if (!mvs.compare("CORR")) mv = CORRELATION; + // else if (!mvs.compare("COVA")) mv = COVARIANCE; + // else if (!mvs.compare("INFO")) mv = INFORMATION; + // + // failure = read_snx_matrix(filestream, APRIORI, mv, c); + // break; + // + // default: + // break; + // } + } + else if (line[0] == '%') + { + trimCut(line); + if (line != "%ENDSNX") + { + // error in file. report it. + BOOST_LOG_TRIVIAL(error) << "Line starting '%' met not final line" << "\n" << line; + + failure = true; + } + + break; + } + + if (failure) + break; + } + + theSinex.listsitedata.sort(compareSiteData); + theSinex.listgpspcs.sort(compareGpsPc); + theSinex.listgalpcs.sort(compareGalPc); + theSinex.listsatids.sort(compareSatIds); + theSinex.listsatpcs.sort(compareSatPc); + theSinex.listsatprns.sort(compareSatPrns); + theSinex.listsatfreqchns.sort(compareFreqChannels); + theSinex.listsatcoms.sort(compareSatCom); + theSinex.listsateccs.sort(compareSatEcc); + + // theSinex.matrix_map[type][value].sort(compare_matrix_entries); + dedupeSinex(); + + return failure == false; +} + +void updateSinexHeader( + string& create_agc, + string& data_agc, + UYds soln_start, + UYds soln_end, + const char obsCode, + const char constCode, + string& contents, + int numParam, + double sinexVer +) +{ + SinexInputHistory siht; + + siht.code = '+'; + siht.fmt = theSinex.ver; + siht.create_agency = theSinex.createagc; + siht.data_agency = theSinex.dataagc; + siht.obs_tech = theSinex.obsCode; + siht.constraint = theSinex.constCode; + siht.num_estimates = theSinex.numparam; + siht.contents = theSinex.solcont; + siht.create_time = theSinex.filedate; + siht.start = theSinex.solutionstartdate; + siht.stop = theSinex.solutionenddate; + + if (theSinex.inputHistory.empty()) + theSinex.inputHistory.push_back(siht); + + theSinex.ver = sinexVer; + + if (data_agc.size() > 0) + theSinex.dataagc = data_agc; + else + theSinex.dataagc = theSinex.createagc; + + theSinex.createagc = create_agc; + theSinex.solcont = contents; + theSinex.filedate = timeGet(); + theSinex.solutionstartdate = soln_start; + theSinex.solutionenddate = soln_end; + + if (obsCode != ' ') + theSinex.obsCode = obsCode; + + if (constCode != ' ') + theSinex.constCode = constCode; + + theSinex.numparam = numParam; +} + +void commentsOverride() +{ + // overriding only those that can be found in IGS/CODE/GRG SINEX files + theSinex.blockComments["FILE/REFERENCE"].push_back( + "*INFO_TYPE_________ INFO________________________________________________________" + ); // FILE/REFERENCE + theSinex.blockComments["FILE/COMMENT"].push_back( + "*DESCRIPTION____________________________________________________________________" + ); // FILE/COMMENT + theSinex.blockComments["INPUT/HISTORY"].push_back( + "*_VERSION_ CRE __CREATION__ OWN _DATA_START_ __DATA_END__ T PARAM S TYPE________" + ); // INPUT/HISTORY + theSinex.blockComments["INPUT/FILES"].push_back( + "*OWN __CREATION__ FILENAME_____________________ DESCRIPTION_____________________" + ); // INPUT/FILES + theSinex.blockComments["INPUT/ACKNOWLEDGEMENTS"].push_back( + "*AGY DESCRIPTION________________________________________________________________" + ); // INPUT/ACKNOWLEDGEMENTS + theSinex.blockComments["SITE/ID"].push_back( + "*CODE PT __DOMES__ T STATION_DESCRIPTION___ _LONGITUDE_ _LATITUDE__ HEIGHT_" + ); // SITE/ID + theSinex.blockComments["SITE/DATA"].push_back( + "*CODE PT SOLN CODE PT SOLN T _DATA_START_ _DATA_END___ OWN _FILE_TIME__" + ); // SITE/DATA + theSinex.blockComments["SITE/RECEIVER"].push_back( + "*CODE PT SOLN T _DATA_START_ _DATA_END___ RECEIVER_TYPE_______ S/N__ FIRMWARE___" + ); // SITE/RECEIVER + theSinex.blockComments["SITE/ANTENNA"].push_back( + "*CODE PT SOLN T _DATA_START_ __DATA_END__ ANTENNA_TYPE________ S/N__" + ); // SITE/ANTENNA + theSinex.blockComments["SITE/GPS_PHASE_CENTER"].push_back( + "*ANTENNA_TYPE________ S/N__ _L1_U_ _L1_N_ _L1_E_ _L2_U_ _L2_N_ _L2_E_ MODEL_____" + ); // SITE/GPS_PHASE_CENTER + theSinex.blockComments["SITE/ECCENTRICITY"].push_back( + "*CODE PT SOLN T _DATA_START_ __DATA_END__ REF __DX_U__ __DX_N__ __DX_E__" + ); // SITE/ECCENTRICITY + theSinex.blockComments["SATELLITE/ID"].push_back( + "*SVN_ PR COSPAR_ID T _DATA_START_ __DATA_END__ ANTENNA_____________" + ); // SATELLITE/ID + theSinex.blockComments["SATELLITE/PHASE_CENTER"].push_back( + "*SVN_ L SATA_Z SATA_X SATA_Y L SATA_Z SATA_X SATA_Y MODEL_____ T M" + ); // SATELLITE/PHASE_CENTER + theSinex.blockComments["SOLUTION/EPOCHS"].push_back( + "*CODE PT SOLN T _DATA_START_ __DATA_END__ _MEAN_EPOCH_" + ); // BIAS/EPOCHS|SOLUTION/EPOCHS + theSinex.blockComments["SOLUTION/STATISTICS"].push_back( + "*STATISTICAL_PARAMETER_________ _______VALUE(S)_______" + ); // SOLUTION/STATISTICS + theSinex.blockComments["SOLUTION/ESTIMATE"].push_back( + "*INDEX TYPE__ CODE PT SOLN _REF_EPOCH__ UNIT S ___ESTIMATED_VALUE___ __STD_DEV__" + ); // SOLUTION/ESTIMATE + theSinex.blockComments["SOLUTION/APRIORI"].push_back( + "*INDEX TYPE__ CODE PT SOLN _REF_EPOCH__ UNIT S ____APRIORI_VALUE____ __STD_DEV__" + ); // SOLUTION/APRIORI + theSinex.blockComments["SOLUTION/NORMAL_EQUATION_VECTOR"].push_back( + "*INDEX TYPE__ CODE PT SOLN _REF_EPOCH__ UNIT S ___RIGHT_HAND_SIDE___" + ); // SOLUTION/NORMAL_EQUATION_VECTOR + theSinex.blockComments["SOLUTION/MATRIX_ESTIMATE"].push_back( + "*PARA1 PARA2 _______PARA2+0_______ _______PARA2+1_______ _______PARA2+2_______" + ); // SOLUTION/MATRIX_ESTIMATE|SOLUTION/MATRIX_APRIORI|SOLUTION/NORMAL_EQUATION_MATRIX +} + +int sinexCheckAddGaReference(string solType, string peaVer, bool isTrop) +{ + // step 1: check it is not already there + for (auto it = theSinex.refstrings.begin(); it != theSinex.refstrings.end(); it++) + { + if (it->find("Geoscience Australia") != string::npos) + { + return 1; + } + } + + // step 2: remove any other provider's details + // NB we do not increment the iterator in the loop because the erase if found will do it for us + for (auto it = theSinex.refstrings.begin(); it != theSinex.refstrings.end();) + { + string line = *it; + + if (line.find("DESCRIPTION") != string::npos || line.find("OUTPUT") != string::npos || + line.find("CONTACT") != string::npos || line.find("SOFTWARE") != string::npos || + line.find("HARDWARE") != string::npos || line.find("INPUT") != string::npos) + { + it = theSinex.refstrings.erase(it); + } + else + { + it++; + } + } + + // step 3: put in the Geoscience reference + char line[81]; + + snprintf(line, sizeof(line), " %-18s %s", "DESCRIPTION", "Geoscience Australia"); + theSinex.refstrings.push_back(line); + snprintf(line, sizeof(line), " %-18s %s", "OUTPUT", solType.c_str()); + theSinex.refstrings.push_back(line); + snprintf(line, sizeof(line), " %-18s %s", "CONTACT", "npi@ga.gov.au"); + theSinex.refstrings.push_back(line); + snprintf(line, sizeof(line), " %-18s %s", "SOFTWARE", ("Ginan PEA Version " + peaVer).c_str()); + theSinex.refstrings.push_back(line); + +#ifndef _WIN32 + struct utsname buf; + int result = uname(&buf); + + if (result == 0) + { + int offset = 0; + + offset += snprintf(line + offset, sizeof(line) - offset, " %-18s ", "HARDWARE"); + + offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf.sysname); + offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf.release); + offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf.version); + + theSinex.refstrings.push_back(line); + } +#else + // Windows - provide basic hardware info + snprintf(line, sizeof(line), " %-18s %s", "HARDWARE", "Windows"); + theSinex.refstrings.push_back(line); +#endif + + snprintf(line, sizeof(line), " %-18s %s", "INPUT", "RINEX"); + theSinex.refstrings.push_back(line); + + if (isTrop) + { + snprintf( + line, + sizeof(line), + " %-18s %03d", + "VERSION NUMBER", + 1 + ); // note: increment if the processing is modified in a way that might lead to a different + // error characteristics of the product - see trop snx specs + theSinex.refstrings.push_back(line); + } + return 0; +} + +void sinexAddComment(const string what) +{ + theSinex.fileComments.push_back(what); +} + +void sinexAddFiles( + const string& who, + const GTime& time, + const vector& filenames, + const string& description +) +{ + for (auto& filename : filenames) + { + SinexInputFile sif; + + sif.yds = time; + sif.agency = who; + sif.file = filename; + sif.description = description; + + theSinex.inputFiles.push_back(sif); + } +} + +void sinexAddStatistic(const string& what, const int val) +{ + SinexSolStatistic sst; + + sst.name = what; + sst.etype = 0; + sst.value.ival = val; + + theSinex.liststatistics.push_back(sst); +} + +void sinexAddStatistic(const string& what, const double val) +{ + SinexSolStatistic sst; + + sst.name = what; + sst.etype = 1; + sst.value.dval = val; + + theSinex.liststatistics.push_back(sst); +} + +void writeSnxHeader(std::ofstream& out) +{ + char line[81]; + char c; + int i; + + int offset = 0; + offset += snprintf( + line + offset, + sizeof(line) - offset, + "%%=SNX %4.2lf %3s %02d:%03d:%05d %3s %02d:%03d:%05d %02d:%03d:%05d %c %5d %c", + theSinex.ver, + theSinex.createagc.c_str(), + (int)theSinex.filedate[0] % 100, + (int)theSinex.filedate[1], + (int)theSinex.filedate[2], + theSinex.dataagc.c_str(), + (int)theSinex.solutionstartdate[0] % 100, + (int)theSinex.solutionstartdate[1], + (int)theSinex.solutionstartdate[2], + (int)theSinex.solutionenddate[0] % 100, + (int)theSinex.solutionenddate[1], + (int)theSinex.solutionenddate[2], + theSinex.obsCode, + theSinex.numparam, + theSinex.constCode + ); + + i = 0; + c = theSinex.solcont[0]; + + while (c != ' ') + { + snprintf(line + offset, sizeof(line) - offset, " %c", c); + + i++; + + if (i <= theSinex.solcont.length()) + c = theSinex.solcont[i]; + else + c = ' '; + } + + out << line << "\n"; +} + +void writeAsComments(Trace& out, list& comments) +{ + for (auto& comment : comments) + { + string line = comment; - if (!doit) - continue; - - char line[82]; - - snprintf( - line, - sizeof(line), - " %5d %6s %4s %2s %4s %2.2d:%3.3d:%5.5d %-4s %c %21.14le %11.5le", - sst.idx, - sst.param_type.c_str(), - sst.sitecode.c_str(), - sst.ptcode.c_str(), - sst.solnnum.c_str(), - (int)sst.epoch[0] % 100, - (int)sst.epoch[1], - (int)sst.epoch[2], - sst.unit.c_str(), - sst.constraint, - sst.param, - sst.stddev - ); - - out << line << "\n"; - } -} - -void writeSnxAprioriFromReceivers(ofstream& out, map& receiverMap) -{ - Block block(out, "SOLUTION/APRIORI"); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - int index = 1; - for (auto& [id, rec] : receiverMap) - { - if (rec.invalid) - { - continue; - } - - auto& sst = rec.snx; - - for (int i = 0; i < 3; i++) - { - string type = "STA?"; - type[3] = 'X' + i; - - tracepdeex( - 0, - out, - " %5d %-6s %4s %2d %4s %02d:%03d:%05d %-4s %c %21.14le %11.5le\n", - index, - type.c_str(), - id.c_str(), - sst.id_ptr->ptcode.c_str(), - 1, // sst.solnnum.c_str(), - (int)rec.aprioriTime[0] % 100, - (int)rec.aprioriTime[1], - (int)rec.aprioriTime[2], - "m", // sst.unit.c_str(), - '3', // sst.constraint, - rec.aprioriPos(i), // sst.param, - rec.aprioriPosVar(i) - ); - - index++; + // just make sure it starts with * as required by format + line[0] = '*'; + + out << line << "\n"; + } +} + +void writeSnxReference(ofstream& out) +{ + Block block(out, "FILE/REFERENCE", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& refString : theSinex.refstrings) + { + out << refString << "\n"; + } +} + +void writeSnxComments(ofstream& out) +{ + Block block(out, "FILE/COMMENT", separator); + + for (auto& commentstring : theSinex.fileComments) + { + out << commentstring << "\n"; + } +} + +void writeSnxInputHistory(ofstream& out) +{ + Block block(out, "INPUT/HISTORY", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto it = theSinex.inputHistory.begin(); it != theSinex.inputHistory.end(); it++) + { + char line[81] = {}; + int offset = 0; + SinexInputHistory siht = *it; + int i = 0; + + offset += snprintf( + line + offset, + sizeof(line) - offset, + " %cSNX %4.2lf %3s %02d:%03d:%05d %3s %02d:%03d:%05d %02d:%03d:%05d %c %5d %c", + siht.code, + siht.fmt, + siht.create_agency.c_str(), + (int)siht.create_time[0] % 100, + (int)siht.create_time[1], + (int)siht.create_time[2], + siht.data_agency.c_str(), + (int)siht.start[0] % 100, + (int)siht.start[1], + (int)siht.start[2], + (int)siht.stop[0] % 100, + (int)siht.stop[1], + (int)siht.stop[2], + siht.obs_tech, + siht.num_estimates, + siht.constraint + ); + + char c = siht.contents[i]; + + while (c != ' ') + { + offset += snprintf(line + offset, sizeof(line) - offset, " %c", c); + i++; + + if (siht.contents.length() >= i) + c = siht.contents[i]; + else + c = ' '; } + + out << line << "\n"; } } -void parseNormals(string& line) +void writeSnxInputFiles(ofstream& out) { - const char* buff = line.c_str(); + Block block(out, "INPUT/FILES", separator); - SinexSolNeq sst; + writeAsComments(out, theSinex.blockComments[block.blockName]); - sst.param = (int)str2num(buff, 2, 5); - sst.ptype = line.substr(7, 6); - sst.site = line.substr(14, 4); - sst.pt = line.substr(19, 2); - sst.solnnum = line.substr(22, 4); - char unit[5]; + for (auto& inputFile : theSinex.inputFiles) + { + SinexInputFile& sif = inputFile; - unit[4] = '\0'; + char line[81]; + int len; + snprintf( + line, + sizeof(line), + " %3s %02d:%03d:%05d ", + sif.agency.c_str(), + (int)sif.yds[0] % 100, + (int)sif.yds[1], + (int)sif.yds[2] + ); - int readcount = sscanf( - buff + 27, - "%2lf:%3lf:%5lf %4s %c %21lf", - &sst.epoch[0], - &sst.epoch[1], - &sst.epoch[2], - unit, - &sst.constraint, - &sst.normal - ); + std::filesystem::path filepath(sif.file); + string stem = filepath.stem().string(); - if (readcount == 6) + // if the filename length is greater than 29 (format spec limit) make into a comment line + if (stem.length() > 29) + line[0] = '*'; + // pad short filenames to 29 characters + if ((len = stem.length()) < 29) + { + for (int i = len; i < 29; i++) + stem += ' '; + } + out << line << stem << " " << sif.description << "\n"; + } +} + +void writeSnxAcknowledgements(ofstream& out) +{ + Block block(out, "INPUT/ACKNOWLEDGEMENTS", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& acknowledgement : theSinex.acknowledgements) { - sst.unit = unit; + SinexAck& ack = acknowledgement; - // see comment at top of file - if (sst.epoch[0] != 0 || sst.epoch[1] != 0 || sst.epoch[2] != 0) + char line[81]; + snprintf(line, sizeof(line), " %3s %s", ack.agency.c_str(), ack.description.c_str()); + + out << line << "\n"; + } +} + +void writeSnxNutCodes(ofstream& out) +{ + Block block(out, "NUTATION/DATA", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& nutcode : theSinex.listnutcodes) + { + SinexNutCode& snt = nutcode; + + char line[81]; + + snprintf(line, sizeof(line), " %-8s %s", snt.nutcode.c_str(), snt.comment.c_str()); + + out << line << "\n"; + } +} + +void writeSnxPreCodes(ofstream& out) +{ + Block block(out, "PRECESSION/DATA", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& spt : theSinex.listprecessions) + { + char line[81]; + + snprintf(line, sizeof(line), " %-8s %s", spt.precesscode.c_str(), spt.comment.c_str()); + + out << line << "\n"; + } +} + +void writeSnxSourceIds(ofstream& out) +{ + Block block(out, "SOURCE/ID", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& source_id : theSinex.listsourceids) + { + SinexSourceId& ssi = source_id; + + char line[101]; + + snprintf( + line, + sizeof(line), + " %4s %8s %16s %s", + ssi.source.c_str(), + ssi.iers.c_str(), + ssi.icrf.c_str(), + ssi.comments.c_str() + ); + + out << line << "\n"; + } +} + +void writeSnxSiteIds(ofstream& out) +{ + Block block(out, "SITE/ID", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [id, rec] : receiverMap) + { + if (rec.invalid) + { + continue; + } + + auto ssi_it = theSinex.mapsiteids.find(id); + if (ssi_it != theSinex.mapsiteids.end() && ssi_it->second.sitecode == id) + { + auto& ssi = ssi_it->second; + + if (ssi.used == false) + continue; + + tracepdeex( + 0, + out, + " %4s %2s %9s %c %-22s %3d %2d %4.1lf %3d %2d %4.1lf %7.1lf\n", + ssi.sitecode.c_str(), + ssi.ptcode.c_str(), + ssi.domes.c_str(), + ssi.typecode, + ssi.desc.c_str(), + ssi.lon_deg, + ssi.lon_min, + ssi.lon_sec, + ssi.lat_deg, + ssi.lat_min, + ssi.lat_sec, + ssi.height + ); + } + else + { + auto recOpts = acsConfig.getRecOpts(id); + VectorPos pos = ecef2pos(rec.aprioriPos); + double lat = pos.latDeg(); + double lon = pos.lonDeg(); + double hgt = pos.hgt(); + + tracepdeex( + 0, + out, + " %4s %2s %9s %c %-22s %3d %2d %4.1lf %3d %2d %4.1lf %7.1lf\n", + id.c_str(), + "A", + recOpts.domes_number.empty() ? " M " : recOpts.domes_number, + 'P', + recOpts.site_description.empty() ? id.c_str() : recOpts.site_description, + int(lon), + fabs(int(lon * 60) % 60), + fabs(fmod(lon * 3600, 60)), + int(lat), + fabs(int(lat * 60) % 60), + fabs(fmod(lat * 3600, 60)), + hgt + ); + } + } +} + +void writeSnxSiteData(ofstream& out, list* pstns) +{ + Block block(out, "SITE/DATA", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& sitedata : theSinex.listsitedata) + { + SinexSiteData& ssd = sitedata; + bool doit = false; + + char line[81]; + snprintf( + line, + sizeof(line), + " %4s %2s %4s %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %3s %02d:%03d:%05d", + ssd.site.c_str(), + ssd.station_pt.c_str(), + ssd.soln_id.c_str(), + ssd.sitecode.c_str(), + ssd.site_pt.c_str(), + ssd.sitesoln.c_str(), + ssd.obscode, + (int)ssd.start[0] % 100, + (int)ssd.start[1], + (int)ssd.start[2], + (int)ssd.stop[0] % 100, + (int)ssd.stop[1], + (int)ssd.stop[2], + ssd.agency.c_str(), + (int)ssd.create[0] % 100, + (int)ssd.create[1], + (int)ssd.create[2] + ); + + if (pstns == nullptr) + doit = true; + else { - nearestYear(sst.epoch[0]); + for (auto& stn : *pstns) + { + if (ssd.site.compare(stn.id_ptr->sitecode) == 0) + { + doit = true; + break; + } + } } - theSinex.listnormaleqns.push_back(sst); + if (doit) + out << line << "\n"; } } -void writeSnxNormal(ofstream& out, list* pstns = nullptr) +void writeSnxReceivers(ofstream& out) { - Block block(out, "SOLUTION/NORMAL_EQUATION_VECTOR"); + Block block(out, "SITE/RECEIVER", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& sst : theSinex.listnormaleqns) - { - bool doit = (pstns == nullptr); + for (auto& [site, timemap] : theSinex.mapreceivers) + for (auto it = timemap.rbegin(); it != timemap.rend(); it++) + { + auto& [time, receiver] = *it; - if (pstns) - for (auto& stn : *pstns) + if (receiver.used == false) { - if (sst.site.compare(stn.id_ptr->sitecode) != 0) - { - doit = true; - break; - } + continue; } - if (!doit) - continue; - - char line[81]; - - snprintf( - line, - sizeof(line), - " %5d %6s %4s %2s %4s %2.2d:%3.3d:%5.5d %-4s %c %21.15lf", - sst.param, - sst.ptype.c_str(), - sst.site.c_str(), - sst.pt.c_str(), - sst.solnnum.c_str(), - (int)sst.epoch[0] % 100, - (int)sst.epoch[1], - (int)sst.epoch[2], - sst.unit.c_str(), - sst.constraint, - sst.normal - ); - - out << line << "\n"; - } + tracepdeex( + 0, + out, + " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %-20s %-5s %-11s\n", + receiver.sitecode.c_str(), + receiver.ptcode.c_str(), + receiver.solnid.c_str(), + receiver.typecode, + (int)receiver.start[0] % 100, + (int)receiver.start[1], + (int)receiver.start[2], + (int)receiver.end[0] % 100, + (int)receiver.end[1], + (int)receiver.end[2], + receiver.type.c_str(), + receiver.sn.c_str(), + receiver.firm.c_str() + ); + } } -matrix_type mat_type; -matrix_value mat_value; - -void parseMatrix(string& line) //, matrix_type type, matrix_value value) +void writeSnxReceiversFromReceivers(ofstream& out, UYds soln_start, UYds soln_end) { - const char* buff = line.c_str(); - - // //todo aaron, this is only half complete, the maxrow/col arent used but should be with - // multiple input matrices. - int maxrow = 0; - int maxcol = 0; - SinexSolMatrix smt; + Block block(out, "SITE/RECEIVER", separator); - int readcount = sscanf( - buff, - " %5d %5d %21lf %21lf %21lf", - &smt.row, - &smt.col, - &smt.value[0], - &smt.value[1], - &smt.value[2] - ); + writeAsComments(out, theSinex.blockComments[block.blockName]); - if (readcount > 2) + for (auto& [id, rec] : receiverMap) { - if (smt.row < smt.col) + if (rec.invalid) { - // xor swap - smt.row ^= smt.col; - smt.col ^= smt.row; - smt.row ^= smt.col; + continue; } - int covars = readcount - 2; - - for (int i = readcount - 2; i < 3; i++) - smt.value[i] = -1; - - smt.numvals = readcount - 2; + auto& sst = rec.snx; - if (smt.row > maxrow) - maxrow = smt.row; - if (smt.col > maxcol) - maxcol = smt.col; + string receiverType = + rec.metadata.receiverType.valid ? rec.metadata.receiverType.value : rec.receiverType; - theSinex.matrixmap[mat_type][mat_value].push_back(smt); + tracepdeex( + 0, + out, + " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %-20s %-5s %-11s\n", + id.c_str(), + sst.id_ptr == nullptr ? "A" : sst.id_ptr->ptcode.c_str(), + "----", + 'P', + (int)soln_start[0] % 100, + (int)soln_start[1], + (int)soln_start[2], + (int)soln_end[0] % 100, + (int)soln_end[1], + (int)soln_end[2], + receiverType.c_str(), + rec.metadata.receiverSerial.valid + ? rec.metadata.receiverSerial.value.c_str() + : (sst.rec_ptr == nullptr ? "-----" : sst.rec_ptr->sn.c_str()), + rec.metadata.receiverFirmware.valid + ? rec.metadata.receiverFirmware.value.c_str() + : (sst.rec_ptr == nullptr ? "-----------" : sst.rec_ptr->firm.c_str()) + ); } } -void parseSinexEstimates(string& line) {} - -void parseSinexEstimateMatrix(string& line) {} - -void writeSnxMatricesFromFilter(ofstream& out, KFState& kfState) +void writeSnxAntennas(ofstream& out) { - const char* type_strings[MAX_MATRIX_TYPE]; - const char* value_strings[MAX_MATRIX_VALUE]; - - type_strings[ESTIMATE] = "SOLUTION/MATRIX_ESTIMATE"; - type_strings[APRIORI] = "SOLUTION/MATRIX_APRIORI"; - type_strings[NORMAL_EQN] = "SOLUTION/NORMAL_EQUATION_MATRIX"; - - value_strings[CORRELATION] = "CORR"; - value_strings[COVARIANCE] = "COVA"; - value_strings[INFORMATION] = "INFO"; + Block block(out, "SITE/ANTENNA", separator); - // just check we have some values to play with first - if (kfState.P.rows() == 0) - return; + writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& mt : {ESTIMATE}) - for (auto& mv : {COVARIANCE}) + for (auto& [site, antmap] : theSinex.mapantennas) + for (auto it = antmap.rbegin(); it != antmap.rend(); it++) { - // print header - char header[128]; - snprintf( - header, - sizeof(header), - "%s %c %s", - type_strings[mt], - 'L', - mt == NORMAL_EQN ? "" : value_strings[mv] - ); - - Block block(out, header); - - writeAsComments(out, theSinex.blockComments[block.blockName]); - - MatrixXd& P = kfState.P; - - for (int i = 1; i < P.rows(); i++) - for (int j = 1; j <= i;) - { - if (P(i, j) == 0) - { - j++; - continue; - } - - // start printing a line - tracepdeex(0, out, " %5d %5d %21.14le", i, j, P(i, j)); - j++; - - for (int k = 0; k < 2; k++) - { - if ((j > i) || (P(i, j) == 0)) - { - break; - } + auto& [time, ant] = *it; - tracepdeex(0, out, " %21.14le", P(i, j)); - j++; - } + if (ant.used == false) + { + continue; + } - tracepdeex(0, out, "\n"); - } + tracepdeex( + 0, + out, + " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %-20s %-5s\n", + ant.sitecode.c_str(), + ant.ptcode.c_str(), + ant.solnnum.c_str(), + ant.typecode, + (int)ant.start[0] % 100, + (int)ant.start[1], + (int)ant.start[2], + (int)ant.end[0] % 100, + (int)ant.end[1], + (int)ant.end[2], + ant.type.c_str(), + ant.sn.c_str() + ); } } -void parseDataHandling(string& line) +void writeSnxAntennasFromReceivers(ofstream& out, UYds soln_start, UYds soln_end) { - SinexDataHandling sdt; - - const char* buff = line.c_str(); - - sdt.sitecode = trim(line.substr(1, 4)); // 4 - CDP ID - sdt.ptcode = line.substr(6, 2); // 2 - satellites these biases apply to (-- = all) - sdt.solnnum = line.substr(9, 4); // 4 - solution number - sdt.t = line.substr(14, 1); // 1 - - int readcount = sscanf( - buff + 16, - "%2lf:%3lf:%5lf", - &sdt.epochstart[0], - &sdt.epochstart[1], - &sdt.epochstart[2] - ); - readcount += - sscanf(buff + 29, "%2lf:%3lf:%5lf", &sdt.epochend[0], &sdt.epochend[1], &sdt.epochend[2]); - - sdt.m = line.substr(42, 1); // 1 + Block block(out, "SITE/ANTENNA", separator); - if (line.size() >= 79) - { - sdt.estimate = str2num(buff, 44, 12); - sdt.stddev = str2num(buff, 57, 7); - sdt.estrate = str2num(buff, 65, 9); - sdt.unit = line.substr(75, 4); // 4 - units of estimate - } - if (line.size() > 82) - { - sdt.comments = line.substr(82); - } + writeAsComments(out, theSinex.blockComments[block.blockName]); - if (readcount >= 6) // just need a start & stop time + for (auto& [id, rec] : receiverMap) { - // see comment at top of file - if (sdt.epochstart[0] != 0 || sdt.epochstart[1] != 0 || sdt.epochstart[2] != 0) - { - nearestYear(sdt.epochstart[0]); - } - if (sdt.epochend[0] != 0 || sdt.epochend[1] != 0 || sdt.epochend[2] != 0) + if (rec.invalid) { - nearestYear(sdt.epochend[0]); + continue; } - GTime time = sdt.epochstart; - - theSinex.mapdatahandling[sdt.sitecode][sdt.ptcode][sdt.m.front()][time] = sdt; - } -} - -void parsePrecode(string& line) -{ - SinexPreCode snt; + auto& sst = rec.snx; - snt.precesscode = line.substr(1, 8); - snt.comment = line.substr(10); + string antennaType = rec.metadata.antennaDescriptor.valid + ? rec.metadata.antennaDescriptor.value + : rec.antennaType; - theSinex.listprecessions.push_back(snt); + tracepdeex( + 0, + out, + " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %-20s %-5s\n", + id.c_str(), + sst.id_ptr == nullptr ? "A" : sst.id_ptr->ptcode.c_str(), + "----", + 'P', + (int)soln_start[0] % 100, + (int)soln_start[1], + (int)soln_start[2], + (int)soln_end[0] % 100, + (int)soln_end[1], + (int)soln_end[2], + antennaType.c_str(), + rec.metadata.antennaSerial.valid + ? rec.metadata.antennaSerial.value.c_str() + : (sst.ant_ptr == nullptr ? "-----" : sst.ant_ptr->sn.c_str()) + ); + } } -void writeSnxPreCodes(ofstream& out) +void writeSnxGpsPcs(ofstream& out, list* pstns) { - Block block(out, "PRECESSION/DATA"); + Block block(out, "SITE/GPS_PHASE_CENTER", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& spt : theSinex.listprecessions) + for (auto& gps_pc : theSinex.listgpspcs) { + SinexGpsPhaseCenter& sgt = gps_pc; + char buf[8]; + bool doit = false; + char line[81]; + int offset = 0; - snprintf(line, sizeof(line), " %8s %s", spt.precesscode.c_str(), spt.comment.c_str()); + offset += snprintf( + line + offset, + sizeof(line) - offset, + " %-20s %-5s ", + sgt.antname.c_str(), + sgt.serialno.c_str() + ); - out << line << "\n"; - } -} + for (int i = 0; i < 3; i++) + { + snprintf(buf, sizeof(buf), "%6.4lf", sgt.L1[i]); + truncateSomething(buf); + offset += snprintf(line + offset, sizeof(line) - offset, "%s", buf); + } -void parseNutcode(string& line) -{ - SinexNutCode snt; + for (int i = 0; i < 3; i++) + { + snprintf(buf, sizeof(buf), "%6.4lf", sgt.L2[i]); + truncateSomething(buf); + offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); + } - snt.nutcode = line.substr(1, 8); - snt.comment = line.substr(10); + offset += snprintf(line + offset, sizeof(line) - offset, "%s", sgt.calib.c_str()); - theSinex.listnutcodes.push_back(snt); + if (pstns == nullptr) + { + doit = true; + } + else + { + for (auto& stn : *pstns) + { + if (sgt.antname == stn.ant_ptr->type) + { + doit = true; + break; + } + } + } + + if (doit) + { + out << line << "\n"; + } + } } -void writeSnxNutCodes(ofstream& out) +void writeSnxGalPcs(ofstream& out, list* pstns) { - Block block(out, "NUTATION/DATA"); + Block block(out, "SITE/GAL_PHASE_CENTER", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& nutcode : theSinex.listnutcodes) + for (auto& gal_pc : theSinex.listgalpcs) { - SinexNutCode& snt = nutcode; + SinexGalPhaseCenter& sgt = gal_pc; + char buf[8]; + bool doit = false; - char line[81]; + if (pstns == nullptr) + doit = true; + else + { + for (auto& stn : *pstns) + { + if (sgt.antname == stn.ant_ptr->type) + { + doit = true; + break; + } + } + } - snprintf(line, sizeof(line), " %8s %s", snt.nutcode.c_str(), snt.comment.c_str()); + if (!doit) + continue; - out << line << "\n"; - } -} + { + char line[81]; + int offset = 0; -void parseSourceIds(string& line) -{ - SinexSourceId ssi; + offset += snprintf( + line + offset, + sizeof(line) - offset, + " %-20s %-5s ", + sgt.antname.c_str(), + sgt.serialno.c_str() + ); - ssi.source = line.substr(1, 4); - ssi.iers = line.substr(6, 8); - ssi.icrf = line.substr(15, 16); - ssi.comments = line.substr(32); + for (int i = 0; i < 3; i++) + { + snprintf(buf, sizeof(buf), "%6.4lf", sgt.L1[i]); + truncateSomething(buf); + offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); + } - theSinex.listsourceids.push_back(ssi); -} + for (int i = 0; i < 3; i++) + { + snprintf(buf, sizeof(buf), "%6.4lf", sgt.L5[i]); + truncateSomething(buf); + offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); + } -void writeSnxSourceIds(ofstream& out) -{ - Block block(out, "SOURCE/ID"); + offset += snprintf(line + offset, sizeof(line) - offset, "%s", sgt.calib.c_str()); + out << line << "\n"; + } - writeAsComments(out, theSinex.blockComments[block.blockName]); + { + char line[81]; + int offset = 0; - for (auto& source_id : theSinex.listsourceids) - { - SinexSourceId& ssi = source_id; + offset += snprintf( + line + offset, + sizeof(line) - offset, + " %-20s %-5s ", + sgt.antname.c_str(), + sgt.serialno.c_str() + ); - char line[101]; + for (int i = 0; i < 3; i++) + { + snprintf(buf, sizeof(buf), "%6.4lf", sgt.L6[i]); + truncateSomething(buf); + offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); + } - snprintf( - line, - sizeof(line), - " %4s %8s %16s %s", - ssi.source.c_str(), - ssi.iers.c_str(), - ssi.icrf.c_str(), - ssi.comments.c_str() - ); + for (int i = 0; i < 3; i++) + { + snprintf(buf, sizeof(buf), "%6.4lf", sgt.L7[i]); + truncateSomething(buf); + offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); + } - out << line << "\n"; + offset += snprintf(line + offset, sizeof(line) - offset, "%s", sgt.calib.c_str()); + + out << line << "\n"; + } + + { + char line[81]; + int offset = 0; + + offset += snprintf( + line, + sizeof(line), + " %-20s %-5s ", + sgt.antname.c_str(), + sgt.serialno.c_str() + ); + + for (int i = 0; i < 3; i++) + { + snprintf(buf, sizeof(buf), "%6.4lf", sgt.L8[i]); + truncateSomething(buf); + offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf); + } + + offset += snprintf(line + offset, sizeof(line) - offset, " "); + offset += snprintf(line + offset, sizeof(line) - offset, "%s", sgt.calib.c_str()); + out << line << "\n"; + } } } -bool compareSatIds(SinexSatId& left, SinexSatId& right) +void writeSnxSiteEccs(ofstream& out) { - int comp = left.svn.compare(right.svn); + Block block(out, "SITE/ECCENTRICITY", separator); - return (comp < 0); -} + writeAsComments(out, theSinex.blockComments[block.blockName]); -void parseSatelliteIds(string& line) -{ - const char* buff = line.c_str(); + for (auto& [id, setMap] : theSinex.mapeccentricities) + for (auto it = setMap.rbegin(); it != setMap.rend(); it++) + { + auto& [time, set] = *it; - SinexSatId sst; + if (set.used == false) + { + continue; + } - sst.svn = line.substr(1, 4); - sst.prn = sst.svn[0] + line.substr(6, 2); - sst.cospar = line.substr(9, 9); - sst.obsCode = line[18]; - sst.antRcvType = line.substr(47); + tracepdeex( + 0, + out, + " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %3s %8.4lf %8.4lf %8.4lf\n", + set.sitecode.c_str(), + set.ptcode.c_str(), + set.solnnum.c_str(), + set.typecode, + (int)set.start[0] % 100, + (int)set.start[1], + (int)set.start[2], + (int)set.end[0] % 100, + (int)set.end[1], + (int)set.end[2], + set.rs.c_str(), + set.ecc.u(), + set.ecc.n(), + set.ecc.e() + ); + } +} - int readcount = sscanf( - buff + 21, - "%2lf:%3lf:%5lf %2lf:%3lf:%5lf", - &sst.timeSinceLaunch[0], - &sst.timeSinceLaunch[1], - &sst.timeSinceLaunch[2], - &sst.timeUntilDecom[0], - &sst.timeUntilDecom[1], - &sst.timeUntilDecom[2] - ); +void writeSnxSiteEccsFromReceivers(ofstream& out, UYds soln_start, UYds soln_end) +{ + Block block(out, "SITE/ECCENTRICITY", separator); - if (readcount == 6) + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [id, rec] : receiverMap) { - // TODO: make the following adjustements - // TSL if 0 is Sinex file start date - // TUD if 0 is Sinex file end date + if (rec.invalid) + { + continue; + } + + auto& sst = rec.snx; - theSinex.listsatids.push_back(sst); + tracepdeex( + 0, + out, + " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %3s %8.4lf %8.4lf %8.4lf\n", + id.c_str(), + sst.id_ptr == nullptr ? "A" : sst.id_ptr->ptcode.c_str(), + "----", + 'P', + (int)soln_start[0] % 100, + (int)soln_start[1], + (int)soln_start[2], + (int)soln_end[0] % 100, + (int)soln_end[1], + (int)soln_end[2], + "UNE", + rec.metadata.antennaDelta.valid ? rec.metadata.antennaDelta.value[2] : rec.antDelta.u(), + rec.metadata.antennaDelta.valid ? rec.metadata.antennaDelta.value[1] : rec.antDelta.n(), + rec.metadata.antennaDelta.valid ? rec.metadata.antennaDelta.value[0] : rec.antDelta.e() + ); } } void writeSnxSatIds(ofstream& out) { - Block block(out, "SATELLITE/ID"); + Block block(out, "SATELLITE/ID", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); @@ -2488,7 +3179,7 @@ void writeSnxSatIds(ofstream& out) snprintf( line, sizeof(line), - " %4s %2s %9s %c %2.2d:%3.3d:%5.5d %2.2d:%3.3d:%5.5d %20s", + " %4s %2s %9s %c %02d:%03d:%05d %02d:%03d:%05d %-20s", ssi.svn.c_str(), ssi.prn.c_str() + 1, ssi.cospar.c_str(), @@ -2506,26 +3197,53 @@ void writeSnxSatIds(ofstream& out) } } -void parseSatelliteIdentifiers(string& line) +void writeSnxSatPc(ofstream& out) { - const char* buff = line.c_str(); + Block block(out, "SATELLITE/PHASE_CENTER", separator); - SinexSatIdentity sst; + writeAsComments(out, theSinex.blockComments[block.blockName]); - sst.svn = line.substr(1, 4); - sst.cospar = line.substr(6, 9); - sst.category = (int)str2num(buff, 16, 6); - sst.blocktype = trim(line.substr(23, 15)); - sst.comment = line.substr(39); + for (auto& spt : theSinex.listsatpcs) + { + char line[101]; + char freq2line[23]; - theSinex.satIdentityMap[sst.svn] = sst; + memset(freq2line, ' ', sizeof(freq2line)); + freq2line[22] = '\0'; - nav.blocktypeMap[sst.svn] = sst.blocktype; + if (spt.freq2 != ' ') + snprintf( + freq2line, + sizeof(freq2line), + "%c %6.4lf %6.4lf %6.4lf", + spt.freq2, + spt.zxy2[0], + spt.zxy2[1], + spt.zxy2[2] + ); + + snprintf( + line, + sizeof(line), + " %4s %c %6.4lf %6.4lf %6.4lf %22s %-10s %c %c", + spt.svn.c_str(), + spt.freq, + spt.zxy[0], + spt.zxy[1], + spt.zxy[2], + freq2line, + spt.antenna.c_str(), + spt.type, + spt.model + ); + + out << line << "\n"; + } } void writeSnxSatIdents(ofstream& out) { - Block block(out, "SATELLITE/IDENTIFIER"); + Block block(out, "SATELLITE/IDENTIFIER", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); @@ -2548,46 +3266,9 @@ void writeSnxSatIdents(ofstream& out) } } -bool compareSatPrns(SinexSatPrn& left, SinexSatPrn& right) -{ - int comp = left.prn.compare(right.prn); - - return (comp < 0); -} - -void parseSatPrns(string& line) -{ - const char* buff = line.c_str(); - - SinexSatPrn spt; - - spt.svn = line.substr(1, 4); - spt.prn = line.substr(36, 3); - spt.comment = line.substr(40); - - int readcount = sscanf( - buff + 6, - "%4lf:%3lf:%5lf %4lf:%3lf:%5lf", - &spt.start[0], - &spt.start[1], - &spt.start[2], - &spt.stop[0], - &spt.stop[1], - &spt.stop[2] - ); - - if (readcount == 6) - { - // No need to adjust years since for satellites the year is 4 digits ... - theSinex.listsatprns.push_back(spt); - - nav.svnMap[SatSys(spt.prn.c_str())][spt.start] = spt.svn; - } -} - void writeSnxSatPrns(ofstream& out) { - Block block(out, "SATELLITE/PRN"); + Block block(out, "SATELLITE/PRN", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); @@ -2598,7 +3279,7 @@ void writeSnxSatPrns(ofstream& out) snprintf( line, sizeof(line), - " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %3s %s", + " %4s %04d:%03d:%05d %04d:%03d:%05d %3s %s", spt.svn.c_str(), (int)spt.start[0], (int)spt.start[1], @@ -2614,50 +3295,9 @@ void writeSnxSatPrns(ofstream& out) } } -bool compareFreqChannels(SinexSatFreqChn& left, SinexSatFreqChn& right) -{ - // start by comparing SVN... - int comp = left.svn.compare(right.svn); - - // then by start time if the same space vehicle - for (int i = 0; i < 3; i++) - if (comp == 0) - comp = left.start[i] - right.start[i]; - - return (comp < 0); -} - -void parseSatFreqChannels(string& line) -{ - const char* buff = line.c_str(); - - SinexSatFreqChn sfc; - - sfc.svn = line.substr(1, 4); - sfc.comment = line.substr(40); - - int readcount = sscanf( - buff + 6, - "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %3d", - &sfc.start[0], - &sfc.start[1], - &sfc.start[2], - &sfc.stop[0], - &sfc.stop[1], - &sfc.stop[2], - &sfc.channel - ); - - if (readcount == 7) - { - // No need to adjust years since for satellites the year is 4 digits ... - theSinex.listsatfreqchns.push_back(sfc); - } -} - void writeSnxSatFreqChn(ofstream& out) { - Block block(out, "SATELLITE/FREQUENCY_CHANNEL"); + Block block(out, "SATELLITE/FREQUENCY_CHANNEL", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); @@ -2668,7 +3308,7 @@ void writeSnxSatFreqChn(ofstream& out) snprintf( line, sizeof(line), - " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %3d %s", + " %4s %04d:%03d:%05d %04d:%03d:%05d %3d %s", sfc.svn.c_str(), (int)sfc.start[0], (int)sfc.start[1], @@ -2684,37 +3324,9 @@ void writeSnxSatFreqChn(ofstream& out) } } -void parseSatelliteMass(string& line) -{ - const char* buff = line.c_str(); - - SinexSatMass ssm; - - ssm.svn = line.substr(1, 4); - ssm.comment = line.substr(46); - - int readcount = sscanf( - buff + 6, - "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %9lf", - &ssm.start[0], - &ssm.start[1], - &ssm.start[2], - &ssm.stop[0], - &ssm.stop[1], - &ssm.stop[2], - &ssm.mass - ); - - if (readcount == 7) - { - // No need to adjust years since for satellites the year is 4 digits ... - theSinex.mapsatmasses[ssm.svn][ssm.start] = ssm; - } -} - void writESnxSatMass(ofstream& out) { - Block block(out, "SATELLITE/MASS"); + Block block(out, "SATELLITE/MASS", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); @@ -2726,140 +3338,25 @@ void writESnxSatMass(ofstream& out) snprintf( line, sizeof(line), - " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %9.3lf %s", + " %4s %04d:%03d:%05d %04d:%03d:%05d %9.3lf %s", ssm.svn.c_str(), (int)ssm.start[0], - (int)ssm.start[1], - (int)ssm.start[2], - (int)ssm.stop[0], - (int)ssm.stop[1], - (int)ssm.stop[2], - ssm.mass, - ssm.comment.c_str() - ); - - out << line << "\n"; - } -} - -/** Get GLONASS frequency channel from SINEX data - * Returns frequency channel number for a GLONASS satellite at a given time. - * Searches SINEX satellite frequency channel blocks to find the correct channel. - */ -int getGloFreqChannel( - const SatSys& sat, ///< Satellite to query - const GTime& time, ///< Time of observation - Navigation& nav ///< Navigation data to cache result -) -{ - if (sat.sys != E_Sys::GLO) - { - return 0; - } - - // Try to get SVN from nav.svnMap first (populated from SINEX SATELLITE/PRN block) - string svn; - auto it = nav.svnMap[sat].lower_bound(time); - if (it != nav.svnMap[sat].end()) - { - svn = it->second; - } - - // Fallback to satDataMap if svnMap lookup failed - if (svn.empty()) - { - svn = sat.svn(); - } - - BOOST_LOG_TRIVIAL(debug) << "SINEX: Querying frequency channel for " << sat.id() - << " at time " << time.to_string() << " (SVN=" << svn << ")"; - - if (svn.empty()) - { - BOOST_LOG_TRIVIAL(info) - << "SINEX: No SVN available for " << sat.id() - << " at time " << time.to_string(); - return 0; - } - - // Find frequency channel for this SVN at this time - for (auto& sfc : theSinex.listsatfreqchns) - { - if (sfc.svn != svn) - continue; - - GTime startTime = sfc.start; - GTime stopTime = sfc.stop; - - // Check if stop time is 0000:000:00000 (means ongoing/no end date) - bool isOngoing = (sfc.stop[0] == 0 && sfc.stop[1] == 0 && sfc.stop[2] == 0); - - // Time must be after start, and either before stop or stop is ongoing - if (time >= startTime && (isOngoing || time <= stopTime)) - { - nav.gloFreqMap[sat] = sfc.channel; - - BOOST_LOG_TRIVIAL(debug) - << "SINEX: Found frequency channel " << sfc.channel - << " for " << sat.id() << " (SVN=" << svn << ") at time " << time.to_string(); - - return sfc.channel; - } - } - - BOOST_LOG_TRIVIAL(debug) - << "SINEX: Could not find frequency channel for " << sat.id() - << " (SVN=" << svn << ") at time " << time.to_string(); - - return 0; -} - -bool compareSatCom(SinexSatCom& left, SinexSatCom& right) -{ - // start by comparing SVN... - int comp = left.svn.compare(right.svn); - - // then by start time if the same space vehicle - for (int i = 0; i < 3; i++) - if (comp == 0) - comp = left.start[i] - right.start[i]; - - return (comp < 0); -} - -void parseSatelliteComs(string& line) -{ - const char* buff = line.c_str(); - - SinexSatCom sct; - - sct.svn = line.substr(1, 4); - sct.comment = line.substr(66); - - int readcount = sscanf( - buff + 6, - "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %9lf %9lf %9lf", - &sct.start[0], - &sct.start[1], - &sct.start[2], - &sct.stop[0], - &sct.stop[1], - &sct.stop[2], - &sct.com[0], - &sct.com[1], - &sct.com[2] - ); + (int)ssm.start[1], + (int)ssm.start[2], + (int)ssm.stop[0], + (int)ssm.stop[1], + (int)ssm.stop[2], + ssm.mass, + ssm.comment.c_str() + ); - if (readcount == 9) - { - // No need to adjust years since for satellites the year is 4 digits ... - theSinex.listsatcoms.push_back(sct); - } + out << line << "\n"; + } } void writeSnxSatCom(ofstream& out) { - Block block(out, "SATELLITE/COM"); + Block block(out, "SATELLITE/COM", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); @@ -2870,7 +3367,7 @@ void writeSnxSatCom(ofstream& out) snprintf( line, sizeof(line), - " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %9.4lf %9.4lf %9.4lf %s", + " %4s %04d:%03d:%05d %04d:%03d:%05d %9.4lf %9.4lf %9.4lf %s", sct.svn.c_str(), (int)sct.start[0], (int)sct.start[1], @@ -2888,40 +3385,9 @@ void writeSnxSatCom(ofstream& out) } } -bool compareSatEcc(SinexSatEcc& left, SinexSatEcc& right) -{ - // start by comparing SVN... - int comp = left.svn.compare(right.svn); - - // then by type (P or L) - if (comp == 0) - comp = static_cast(left.type) - static_cast(right.type); - - return (comp < 0); -} - -void parseSatelliteEccentricities(string& line) -{ - const char* buff = line.c_str(); - - SinexSatEcc set; - - set.svn = line.substr(1, 4); - set.equip = line.substr(6, 20); - set.type = line[27]; - set.comment = line.substr(59); - - int readcount = sscanf(buff + 29, "%9lf %9lf %9lf", &set.ecc[0], &set.ecc[1], &set.ecc[2]); - - if (readcount == 3) - { - theSinex.listsateccs.push_back(set); - } -} - void writeSnxSatEcc(ofstream& out) { - Block block(out, "SATELLITE/ECCENTRICITY"); + Block block(out, "SATELLITE/ECCENTRICITY", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); @@ -2946,37 +3412,9 @@ void writeSnxSatEcc(ofstream& out) } } -void parseSatellitePowers(string& line) -{ - const char* buff = line.c_str(); - - SinexSatPower spt; - - spt.svn = line.substr(1, 4); - spt.comment = line.substr(41); - - int readcount = sscanf( - buff + 6, - "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %4d", - &spt.start[0], - &spt.start[1], - &spt.start[2], - &spt.stop[0], - &spt.stop[1], - &spt.stop[2], - &spt.power - ); - - if (readcount == 7) - { - // No need to adjust years since for satellites the year is 4 digits ... - theSinex.mapsatpowers[spt.svn][spt.start] = spt; - } -} - void writeSnxSatPower(ofstream& out) { - Block block(out, "SATELLITE/TX_POWER"); + Block block(out, "SATELLITE/TX_POWER", separator); writeAsComments(out, theSinex.blockComments[block.blockName]); @@ -2988,7 +3426,7 @@ void writeSnxSatPower(ofstream& out) snprintf( line, sizeof(line), - " %4s %4.4d:%3.3d:%5.5d %4.4d:%3.3d:%5.5d %4d %s", + " %4s %04d:%03d:%05d %04d:%03d:%05d %4d %s", spt.svn.c_str(), (int)spt.start[0], (int)spt.start[1], @@ -3004,503 +3442,419 @@ void writeSnxSatPower(ofstream& out) } } -bool compareSatPc(SinexSatPc& left, SinexSatPc& right) -{ - // start by comparing SVN... - int comp = left.svn.compare(right.svn); - - // then by the first freq number - if (comp == 0) - comp = static_cast(left.freq) - static_cast(right.freq); - - return (comp < 0); -} - -void parseSatellitePhaseCenters(string& line) +void writeSnxEpochs(Trace& out) { - const char* buff = line.c_str(); - - SinexSatPc spt; - - int readcount2; - - spt.svn = line.substr(1, 4); - spt.freq = line[6]; - spt.freq2 = line[29]; - spt.antenna = line.substr(52, 10); - spt.type = line[63]; - spt.model = line[65]; - - int readcount = sscanf(buff + 6, "%6lf %6lf %6lf", &spt.zxy[0], &spt.zxy[1], &spt.zxy[2]); - - if (spt.freq2 != ' ') - { - readcount2 = sscanf(buff + 31, "%6lf %6lf %6lf", &spt.zxy2[0], &spt.zxy2[1], &spt.zxy2[2]); - } - - if (readcount == 3 && (spt.freq2 == ' ' || readcount2 == 3)) - { - theSinex.listsatpcs.push_back(spt); - } -} + string blockName; + if (theSinex.epochshavebias) + blockName = "BIAS/EPOCHS"; + else + blockName = "SOLUTION/EPOCHS"; -void writeSnxSatPc(ofstream& out) -{ - Block block(out, "SATELLITE/PHASE_CENTER"); + Block block(out, blockName, separator); writeAsComments(out, theSinex.blockComments[block.blockName]); - for (auto& spt : theSinex.listsatpcs) + for (auto& [id, sst] : theSinex.solEpochMap) { - char line[101]; - char freq2line[23]; - - memset(freq2line, ' ', sizeof(freq2line)); - freq2line[22] = '\0'; - - if (spt.freq2 != ' ') - snprintf( - freq2line, - sizeof(freq2line), - "%c %6.4lf %6.4lf %6.4lf", - spt.freq2, - spt.zxy2[0], - spt.zxy2[1], - spt.zxy2[2] - ); - - snprintf( - line, - sizeof(line), - " %4s %c %6.4lf %6.4lf %6.4lf %22s %-10s %c %c", - spt.svn.c_str(), - spt.freq, - spt.zxy[0], - spt.zxy[1], - spt.zxy[2], - freq2line, - spt.antenna.c_str(), - spt.type, - spt.model + tracepdeex( + 0, + out, + " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %02d:%03d:%05d\n", + sst.sitecode.c_str(), + sst.ptcode.c_str(), + sst.solnnum.c_str(), + sst.typecode, + (int)sst.start[0] % 100, + (int)sst.start[1], + (int)sst.start[2], + (int)sst.end[0] % 100, + (int)sst.end[1], + (int)sst.end[2], + (int)sst.mean[0] % 100, + (int)sst.mean[1], + (int)sst.mean[2] ); - - out << line << "\n"; - } -} - -void parseSinexSatYawRates(string& line) -{ - const char* buff = line.c_str(); - - SinexSatYawRate entry; - - entry.svn = line.substr(1, 4); - entry.comment = line.substr(51); - - int readCount = sscanf( - buff + 6, - "%4lf:%3lf:%5lf %4lf:%3lf:%5lf %c %8lf", - &entry.start[0], - &entry.start[1], - &entry.start[2], - &entry.stop[0], - &entry.stop[1], - &entry.stop[2], - &entry.yawBias, - &entry.maxYawRate - ); - - entry.maxYawRate *= D2R; - - if (readCount == 8) - { - theSinex.satYawRateMap[entry.svn][entry.start] = entry; - } -} - -void parseSinexSatAttMode(string& line) -{ - const char* buff = line.c_str(); - - SinexSatAttMode entry; - entry.svn = line.substr(1, 4); - int readCount = sscanf( - buff + 6, - "%4lf-%2lf-%2lf %2lf:%2lf:%2lf %4lf-%2lf-%2lf %2lf:%2lf:%2lf ", - &entry.start[0], - &entry.start[1], - &entry.start[2], - &entry.start[3], - &entry.start[4], - &entry.start[5], - &entry.stop[0], - &entry.stop[1], - &entry.stop[2], - &entry.stop[3], - &entry.stop[4], - &entry.stop[5] - ); - entry.attMode = line.substr(47); - - if (readCount == 12) - { - theSinex.satAttModeMap[entry.svn][entry.start] = entry; } } -void nullFunction(string& line) {} - -bool readSinex(const string& filepath) +void writeSnxEpochsFromReceivers(Trace& out, UYds soln_start, UYds soln_end) { - // BOOST_LOG_TRIVIAL(info) - // << "reading " << filepath; - - ifstream filestream(filepath); - if (!filestream) - { - BOOST_LOG_TRIVIAL(error) << "Error opening sinex file" << filepath; - return false; - } - - bool pass = readSnxHeader(filestream); - if (pass == false) - { - BOOST_LOG_TRIVIAL(error) << "Error reading header line."; - - return false; - } + string blockName; + if (theSinex.epochshavebias) + blockName = "BIAS/EPOCHS"; + else + blockName = "SOLUTION/EPOCHS"; - theSinex.currentFile = filepath; + Block block(out, blockName, separator); - void (*parseFunction)(string&) = nullFunction; + writeAsComments(out, theSinex.blockComments[block.blockName]); - string closure; + for (auto& [id, rec] : receiverMap) + { + if (rec.invalid) + { + continue; + } - bool failure = false; + auto& sst = rec.snx; - int lineNumber = 0; + UYds soln_mean = (GTime)soln_start + ((GTime)soln_end - (GTime)soln_start).to_double() / 2; - while (filestream) - { - string line; + tracepdeex( + 0, + out, + " %4s %2s %4s %c %02d:%03d:%05d %02d:%03d:%05d %02d:%03d:%05d\n", + id.c_str(), + sst.id_ptr == nullptr ? "A" : sst.id_ptr->ptcode.c_str(), + 1, + 'P', + (int)soln_start[0] % 100, + (int)soln_start[1], + (int)soln_start[2], + (int)soln_end[0] % 100, + (int)soln_end[1], + (int)soln_end[2], + (int)soln_mean[0] % 100, + (int)soln_mean[1], + (int)soln_mean[2] + ); + } +} - getline(filestream, line); +void writeSnxStatistics(ofstream& out) +{ + Block block(out, "SOLUTION/STATISTICS", separator); - lineNumber++; + writeAsComments(out, theSinex.blockComments[block.blockName]); - // test below empty line (ie continue if something on the line) - if (!filestream) - { - // error - did not find closure line. Report and clean up. - BOOST_LOG_TRIVIAL(error) << "Closure line not found before end."; + for (auto& statistic : theSinex.liststatistics) + { + char line[81]; - failure = true; - break; - } - else if (line[0] == '*') - { - // comment - } - else if (line[0] == '-') - { - // end of block - parseFunction = nullFunction; + if (statistic.etype == 0) // int + snprintf( + line, + sizeof(line), + " %-30s %22d", + statistic.name.c_str(), + statistic.value.ival + ); - if (line != closure) - { - BOOST_LOG_TRIVIAL(error) << "Incorrect section closure line encountered on line " - << lineNumber << ": " << closure << " != " << line; - } - } - else if (line[0] == ' ') - { - try - { - // this probably needs specialty parsing - use a prepared function pointer. - parseFunction(line); - } - catch (std::out_of_range& e) - { - BOOST_LOG_TRIVIAL(error) - << "Sinex line width error on line " << lineNumber << ": '" << line << "'"; - } - catch (...) - { - BOOST_LOG_TRIVIAL(error) - << "Sinex parsing error on line " << lineNumber << ": '" << line << "'"; - } - } - else if (line[0] == '+') - { - string mvs; + if (statistic.etype == 1) // double + snprintf( + line, + sizeof(line), + " %-30s %22.15lf", + statistic.name.c_str(), + statistic.value.dval + ); - // prepare closing line for comparison - closure = line; - closure[0] = '-'; + out << line << "\n"; + } +} - trimCut(line); - if (line == "+FILE/REFERENCE") - { - parseFunction = parseReference; - } - else if (line == "+FILE/COMMENT") - { - parseFunction = nullFunction; - } - else if (line == "+INPUT/HISTORY") - { - parseFunction = parseInputHistory; - } - else if (line == "+INPUT/FILES") - { - parseFunction = parseInputFiles; - } - else if (line == "+INPUT/ACKNOWLEDGEMENTS") - { - parseFunction = parseAcknowledgements; - } - else if (line == "+INPUT/ACKNOWLEDGMENTS") - { - parseFunction = parseAcknowledgements; - } - else if (line == "+NUTATION/DATA") - { - parseFunction = parseNutcode; - } - else if (line == "+PRECESSION/DATA") - { - parseFunction = parsePrecode; - } - else if (line == "+SOURCE/ID") - { - parseFunction = parseSourceIds; - } - else if (line == "+SITE/ID") - { - parseFunction = parseSiteIds; - } - else if (line == "+SITE/DATA") - { - parseFunction = parseSiteData; - } - else if (line == "+SITE/RECEIVER") - { - parseFunction = parseReceivers; - } - else if (line == "+SITE/ANTENNA") - { - parseFunction = parseAntennas; - } - else if (line == "+SITE/GPS_PHASE_CENTER") - { - parseFunction = parseGpsPhaseCenters; - } - else if (line == "+SITE/GAL_PHASE_CENTER") - { - parseFunction = parseGalPhaseCenters; - } - else if (line == "+SITE/ECCENTRICITY") - { - parseFunction = parseSiteEccentricity; - } - else if (line == "+BIAS/EPOCHS") - { - parseFunction = parseEpochs; - } - else if (line == "+MODEL/RANGE_BIAS") - { - parseFunction = parseDataHandling; - } // Same format w/ SOLUTION/DATA_HANDLING - else if (line == "+MODEL/TIME_BIAS") - { - parseFunction = parseDataHandling; - } // Same format w/ SOLUTION/DATA_HANDLING - else if (line == "+SOLUTION/EPOCHS") - { - parseFunction = parseEpochs; - } - else if (line == "+SOLUTION/STATISTICS") - { - parseFunction = parseStatistics; - } - else if (line == "+SOLUTION/ESTIMATE") - { - parseFunction = parseSolutionEstimates; - } - else if (line == "+SOLUTION/APRIORI") - { - parseFunction = parseApriori; - } - else if (line == "+SOLUTION/NORMAL_EQUATION_VECTOR") - { - parseFunction = parseNormals; - } - else if (line == "+SOLUTION/MATRIX_ESTIMATE") - { - parseFunction = parseMatrix; - } - else if (line == "+SOLUTION/MATRIX_APRIORI") - { - parseFunction = parseMatrix; - } - else if (line == "+SOLUTION/NORMAL_EQUATION_MATRIX") - { - parseFunction = parseMatrix; - } - else if (line == "+SOLUTION/DATA_HANDLING") - { - parseFunction = parseDataHandling; - } - else if (line == "+SATELLITE/IDENTIFIER") - { - parseFunction = parseSatelliteIdentifiers; - } - else if (line == "+SATELLITE/PRN") - { - parseFunction = parseSatPrns; - } - else if (line == "+SATELLITE/MASS") - { - parseFunction = parseSatelliteMass; - } - else if (line == "+SATELLITE/FREQUENCY_CHANNEL") - { - parseFunction = parseSatFreqChannels; - } - else if (line == "+SATELLITE/TX_POWER") - { - parseFunction = parseSatellitePowers; - } - else if (line == "+SATELLITE/COM") - { - parseFunction = parseSatelliteComs; - } - else if (line == "+SATELLITE/ECCENTRICITY") - { - parseFunction = parseSatelliteEccentricities; - } - else if (line == "+SATELLITE/PHASE_CENTER") - { - parseFunction = parseSatellitePhaseCenters; - } - else if (line == "+SATELLITE/ID") - { - parseFunction = parseSatelliteIds; - } - else if (line == "+SATELLITE/YAW_BIAS_RATE") - { - parseFunction = parseSinexSatYawRates; - } - else if (line == "+SATELLITE/ATTITUDE_MODE") +// void write_snx_estimates( +// ofstream& out, +// std::list* pstns = nullptr) +// { +// out << "+SOLUTION/ESTIMATE" << "\n"; +// +// writeAsComments(out, theSinex.estimate_comments); +// +// for (auto& [index, sst] : theSinex.estimates_map) +// { +// bool doit = (pstns == nullptr); +// +// if (pstns != nullptr) +// { +// for (auto& stn : *pstns) +// { +// if (sst.sitecode.compare(stn.sitecode) == 0) +// { +// doit = true; +// break; +// } +// } +// } +// +// if (!doit) +// continue; +// +// char line[82]; +// +// snprintf(line, sizeof(line), " %5d %6s %4s %2s %4s %02d:%03d:%05d %-4s %c %21.14le +// %11.5le", sst.index, sst.type.c_str(), sst.sitecode.c_str(), +// sst.ptcode.c_str(), sst.solnnum.c_str(), sst.refepoch[0] % 100, +// sst.refepoch[1], sst.refepoch[2], sst.unit.c_str(), +// sst.constraint, sst.estimate, sst.stddev); +// +// out << line << "\n"; +// } +// +// out << "-SOLUTION/ESTIMATE" << "\n"; +// } + +void writeSnxEstimatesFromFilter(ofstream& out, KFState& kfState) +{ + Block block(out, "SOLUTION/ESTIMATE", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [key, index] : kfState.kfIndexMap) + { + if (key.type != KF::REC_POS && key.type != KF::REC_POS_RATE && key.type != KF::STRAIN_RATE) + { + continue; + } + + string type; + if (key.type == KF::REC_POS) + type = "STA?"; + else if (key.type == KF::REC_POS_RATE) + type = "VEL?"; + else if (key.type == KF::STRAIN_RATE) + type = "VEL?"; // todo? scale is wrong, actually entirely untested + + if (key.num == 0) + type[3] = 'X'; + else if (key.num == 1) + type[3] = 'Y'; + else if (key.num == 2) + type[3] = 'Z'; + + auto& sst = receiverMap[key.str].snx; + + tracepdeex( + 0, + out, + " %5d %-6s %4s %2s %4d %02d:%03d:%05d %-4s %c %21.14le %11.5le\n", + index, + type.c_str(), + key.str.c_str(), + sst.id_ptr == nullptr ? "A" : sst.id_ptr->ptcode.c_str(), + 1, + (int)theSinex.solutionenddate[0] % 100, + (int)theSinex.solutionenddate[1], + (int)theSinex.solutionenddate[2], + "m", + '9', // TODO: replace with sst.constraint when fixed + kfState.x(index), + sqrt(kfState.P(index, index)) + ); + } +} + +void writeSnxApriori(ofstream& out, list* pstns = nullptr) +{ + Block block(out, "SOLUTION/APRIORI", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& [index, apriori] : theSinex.apriorimap) + { + SinexSolApriori& sst = apriori; + bool doit = (pstns == nullptr); + + if (pstns) + for (auto& stn : *pstns) { - parseFunction = parseSinexSatAttMode; + if (sst.sitecode.compare(stn.id_ptr->sitecode) == 0) + { + doit = true; + break; + } } - else - { - parseFunction = nullFunction; - BOOST_LOG_TRIVIAL(warning) << "Unknown header line: " << line; - } // Skip unknown sections - // int i; - // failure = read_snx_matrix (filestream, - // NORMAL_EQN, INFORMATION, c); break; case 15: if - // (!theSinex.epochs_have_bias - // && !theSinex.list_solepochs.empty()) - // { - // BOOST_LOG_TRIVIAL(error) - // << "Cannot combine BIAS/EPOCHS and SOLUTION/EPOCHS blocks."; - // - // failure = true; - // break; - // } - // - // theSinex.epochs_have_bias = true; - // theSinex.epochcomments.insert(theSinex.epochcomments.end(), - // comments.begin(), comments.end()); comments.clear(); - // failure = read_snx_epochs(filestream, true); break; - // - // case 16: - // if (theSinex.epochs_have_bias && !theSinex.list_solepochs.empty()) - // { - // BOOST_LOG_TRIVIAL(error) - // << "Cannot combine BIAS/EPOCHS and SOLUTION/EPOCHS blocks."; - // - // failure = true; - // break; - // } - // - // theSinex.epochs_have_bias = false; - // theSinex.epochcomments .insert(theSinex.epochcomments.end(), - // comments.begin(), comments.end()); comments.clear(); - // - // failure = read_snx_epochs(filestream, false); - // break; - // - // case 21: - // theSinex.matrix_comments.insert(theSinex.matrix_comments.end(), - // comments.begin(), comments.end()); comments.clear(); - // c = line[headers[i].length() + 2]; mvs = - // line.substr(headers[i].length() + 4, 4); - // - // if (!mvs.compare("CORR")) mv = CORRELATION; - // else if (!mvs.compare("COVA")) mv = COVARIANCE; - // else if (!mvs.compare("INFO")) mv = INFORMATION; - // - // failure = read_snx_matrix(filestream, ESTIMATE, mv, c); - // break; - // - // case 22: - // theSinex.matrix_comments.insert(theSinex.matrix_comments.end(), - // comments.begin(), comments.end()); comments.clear(); - // c = line[headers[i].length() + 2]; mvs = - // line.substr(headers[i].length() + 4, 4); - // - // if (!mvs.compare("CORR")) mv = CORRELATION; - // else if (!mvs.compare("COVA")) mv = COVARIANCE; - // else if (!mvs.compare("INFO")) mv = INFORMATION; - // - // failure = read_snx_matrix(filestream, APRIORI, mv, c); - // break; - // - // default: - // break; - // } + if (!doit) + continue; + + char line[82]; + + snprintf( + line, + sizeof(line), + " %5d %-6s %4s %2s %4s %02d:%03d:%05d %-4s %c %21.14le %11.5le", + sst.idx, + sst.param_type.c_str(), + sst.sitecode.c_str(), + sst.ptcode.c_str(), + sst.solnnum.c_str(), + (int)sst.epoch[0] % 100, + (int)sst.epoch[1], + (int)sst.epoch[2], + sst.unit.c_str(), + sst.constraint, + sst.param, + sst.stddev + ); + + out << line << "\n"; + } +} + +void writeSnxAprioriFromReceivers(ofstream& out, map& receiverMap) +{ + Block block(out, "SOLUTION/APRIORI", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + int index = 1; + for (auto& [id, rec] : receiverMap) + { + if (rec.invalid) + { + continue; } - else if (line[0] == '%') + + auto& sst = rec.snx; + + for (int i = 0; i < 3; i++) { - trimCut(line); - if (line != "%ENDSNX") - { - // error in file. report it. - BOOST_LOG_TRIVIAL(error) << "Line starting '%' met not final line" << "\n" << line; + string type = "STA?"; + type[3] = 'X' + i; - failure = true; + tracepdeex( + 0, + out, + " %5d %-6s %4s %2d %4s %02d:%03d:%05d %-4s %c %21.14le %11.5le\n", + index, + type.c_str(), + id.c_str(), + sst.id_ptr == nullptr ? "A" : sst.id_ptr->ptcode.c_str(), + 1, // sst.solnnum.c_str(), + (int)rec.aprioriTime[0] % 100, + (int)rec.aprioriTime[1], + (int)rec.aprioriTime[2], + "m", // sst.unit.c_str(), + '3', // sst.constraint, + rec.aprioriPos(i), // sst.param, + rec.aprioriPosVar(i) + ); + + index++; + } + } +} + +void writeSnxNormal(ofstream& out, list* pstns = nullptr) +{ + Block block(out, "SOLUTION/NORMAL_EQUATION_VECTOR", separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + for (auto& sst : theSinex.listnormaleqns) + { + bool doit = (pstns == nullptr); + + if (pstns) + for (auto& stn : *pstns) + { + if (sst.site.compare(stn.id_ptr->sitecode) != 0) + { + doit = true; + break; + } } - break; - } + if (!doit) + continue; - if (failure) - break; + char line[81]; + + snprintf( + line, + sizeof(line), + " %5d %-6s %4s %2s %4s %02d:%03d:%05d %-4s %c %21.15lf", + sst.param, + sst.ptype.c_str(), + sst.site.c_str(), + sst.pt.c_str(), + sst.solnnum.c_str(), + (int)sst.epoch[0] % 100, + (int)sst.epoch[1], + (int)sst.epoch[2], + sst.unit.c_str(), + sst.constraint, + sst.normal + ); + + out << line << "\n"; } +} - theSinex.listsatpcs.sort(compareSatPc); - theSinex.listsateccs.sort(compareSatEcc); - theSinex.listsitedata.sort(compareSiteData); - theSinex.listgpspcs.sort(compareGpsPc); - theSinex.listsatids.sort(compareSatIds); - theSinex.listsatfreqchns.sort(compareFreqChannels); - theSinex.listsatprns.sort(compareSatPrns); - theSinex.listsatcoms.sort(compareSatCom); - theSinex.listgalpcs.sort(compareGalPc); +void writeSnxMatricesFromFilter(ofstream& out, KFState& kfState) +{ + const char* type_strings[MAX_MATRIX_TYPE]; + const char* value_strings[MAX_MATRIX_VALUE]; - // theSinex.matrix_map[type][value].sort(compare_matrix_entries); - dedupeSinex(); + type_strings[ESTIMATE] = "SOLUTION/MATRIX_ESTIMATE"; + type_strings[APRIORI] = "SOLUTION/MATRIX_APRIORI"; + type_strings[NORMAL_EQN] = "SOLUTION/NORMAL_EQUATION_MATRIX"; - return failure == false; + value_strings[CORRELATION] = "CORR"; + value_strings[COVARIANCE] = "COVA"; + value_strings[INFORMATION] = "INFO"; + + // just check we have some values to play with first + if (kfState.P.rows() == 0) + return; + + for (auto& mt : {ESTIMATE}) + for (auto& mv : {COVARIANCE}) + { + // print header + char header[128]; + snprintf( + header, + sizeof(header), + "%s %c %s", + type_strings[mt], + 'L', + mt == NORMAL_EQN ? "" : value_strings[mv] + ); + + Block block(out, header, separator); + + writeAsComments(out, theSinex.blockComments[block.blockName]); + + MatrixXd& P = kfState.P; + + for (int i = 1; i < P.rows(); i++) + for (int j = 1; j <= i;) + { + if (P(i, j) == 0) + { + j++; + continue; + } + + // start printing a line + tracepdeex(0, out, " %5d %5d %21.14le", i, j, P(i, j)); + j++; + + for (int k = 0; k < 2; k++) + { + if ((j > i) || (P(i, j) == 0)) + { + break; + } + + tracepdeex(0, out, " %21.14le", P(i, j)); + j++; + } + + tracepdeex(0, out, "\n"); + } + } } -void writeSinex(string filepath, KFState& kfState, map& receiverMap) +void writeSinex( + string filepath, + KFState& kfState, + map& receiverMap, + UYds soln_start, + UYds soln_end +) { ofstream filestream(filepath); @@ -3517,200 +3871,118 @@ void writeSinex(string filepath, KFState& kfState, map& receiv { writeSnxReference(filestream); } - if (!theSinex.blockComments["FILE/COMMENT"].empty()) + if (!theSinex.fileComments.empty()) { writeSnxComments(filestream); } - if (!theSinex.inputHistory.empty()) - { - writeSnxInputHistory(filestream); - } - if (!theSinex.inputFiles.empty()) - { - writeSnxInputFiles(filestream); - } if (!theSinex.acknowledgements.empty()) { writeSnxAcknowledgements(filestream); } - - if (!theSinex.mapsiteids.empty()) - { - writeSnxSiteids(filestream); - } - // if (!theSinex.listsitedata. empty()) { writeSnxSitedata - //(filestream);} - if (!theSinex.mapreceivers.empty()) - { - writeSnxReceivers(filestream); - } - if (!theSinex.mapantennas.empty()) - { - writeSnxAntennas(filestream); - } - // if (!theSinex.listgpspcs. empty()) { writeSnxGps_pcs - //(filestream);} if (!theSinex.listgalpcs. empty()) { writeSnxGal_pcs - //(filestream);} - if (!theSinex.mapeccentricities.empty()) + if (!theSinex.inputHistory.empty()) { - writeSnxSiteEccs(filestream); + writeSnxInputHistory(filestream); } - if (!theSinex.solEpochMap.empty()) + if (!theSinex.inputFiles.empty()) { - writeSnxEpochs(filestream); + writeSnxInputFiles(filestream); } - // if (!theSinex.liststatistics. empty()) { writeSnxStatistics - //(filestream);} if (!theSinex.estimatesmap. empty()) writeSnxEstimates - //(filestream); - writeSnxEstimatesFromFilter(filestream, kfState); - // if (!theSinex.apriori_map. empty()) { writeSnxApriori - //(filestream);} - writeSnxAprioriFromReceivers(filestream, receiverMap); - // if (!theSinex.list_normal_eqns. empty()) { writeSnxNormal - // (filestream);} + // if (!theSinex.listnutcodes.empty()) + // { + // writeSnxNutCodes(filestream); + // } + // if (!theSinex.listprecessions.empty()) + // { + // writeSnxPreCodes(filestream); + // } + // if (!theSinex.listsourceids.empty()) + // { + // writeSnxSourceIds(filestream); + // } + + { + writeSnxSiteIds(filestream); + } + // if (!theSinex.listsitedata.empty()) + // { + // writeSnxSiteData(filestream); + // } + // if (!theSinex.mapreceivers.empty()) + // { + // writeSnxReceivers(filestream); + // } + { + writeSnxReceiversFromReceivers(filestream, soln_start, soln_end); + } + // if (!theSinex.mapantennas.empty()) + // { + // writeSnxAntennas(filestream); + // } + { + writeSnxAntennasFromReceivers(filestream, soln_start, soln_end); + } + // if (!theSinex.listgpspcs.empty()) + // { + // writeSnxGpsPcs(filestream); + // } + // if (!theSinex.listgalpcs.empty()) + // { + // writeSnxGalPcs(filestream); + // } + // if (!theSinex.mapeccentricities.empty()) + // { + // writeSnxSiteEccs(filestream); + // } + { + writeSnxSiteEccsFromReceivers(filestream, soln_start, soln_end); + } + + // if (!theSinex.listsatids.empty()) + // { + // writeSnxSatIds(filestream); + // } + // if (!theSinex.listsatpcs.empty()) + // { + // writeSnxSatPc(filestream); + // } + + // if (!theSinex.solEpochMap.empty()) + // { + // writeSnxEpochs(filestream); + // } + { + writeSnxEpochsFromReceivers(filestream, soln_start, soln_end); + } + // if (!theSinex.liststatistics.empty()) + // { + // writeSnxStatistics(filestream); + // } + // if (!theSinex.estimatesmap.empty()) + // { + // writeSnxEstimates(filestream); + // } + { + writeSnxEstimatesFromFilter(filestream, kfState); + } + // if (!theSinex.apriorimap.empty()) + // { + // writeSnxApriori(filestream); + // } + { + writeSnxAprioriFromReceivers(filestream, receiverMap); + } + // if (!theSinex.listnormaleqns.empty()) + // { + // writeSnxNormal(filestream); + // } { - // writeSnxMatrices - // (filestream, stationListPointer); writeSnxMatricesFromFilter(filestream, kfState); } - // if (!theSinex.listsourceids. empty()) { writeSnxSourceIds - //(filestream);} if (!theSinex.listnutcodes. empty()) { writeSnxNutCodes - //(filestream);} if (!theSinex.listprecessions. empty()) { writeSnxPreCodes - //(filestream);} - filestream << "%ENDSNX" << "\n"; } -void sinexAddStatistic(const string& what, const int val) -{ - SinexSolStatistic sst; - - sst.name = what; - sst.etype = 0; - sst.value.ival = val; - - theSinex.liststatistics.push_back(sst); -} - -void sinexAddStatistic(const string& what, const double val) -{ - SinexSolStatistic sst; - - sst.name = what; - sst.etype = 1; - sst.value.dval = val; - - theSinex.liststatistics.push_back(sst); -} - -int sinexCheckAddGaReference(string solType, string peaVer, bool isTrop) -{ - // step 1: check it is not already there - for (auto it = theSinex.refstrings.begin(); it != theSinex.refstrings.end(); it++) - { - if (it->find("Geoscience Australia") != string::npos) - { - return 1; - } - } - - // step 2: remove any other provider's details - // NB we do not increment the iterator in the loop because the erase if found will do it for us - for (auto it = theSinex.refstrings.begin(); it != theSinex.refstrings.end();) - { - string line = *it; - - if (line.find("DESCRIPTION") != string::npos || line.find("OUTPUT") != string::npos || - line.find("CONTACT") != string::npos || line.find("SOFTWARE") != string::npos || - line.find("HARDWARE") != string::npos || line.find("INPUT") != string::npos) - { - it = theSinex.refstrings.erase(it); - } - else - { - it++; - } - } - - // step 3: put in the Geoscience reference - char line[81]; - - snprintf(line, sizeof(line), " %-18s %s", "DESCRIPTION", "Geoscience Australia"); - theSinex.refstrings.push_back(line); - snprintf(line, sizeof(line), " %-18s %s", "OUTPUT", solType.c_str()); - theSinex.refstrings.push_back(line); - snprintf(line, sizeof(line), " %-18s %s", "CONTACT", "npi@ga.gov.au"); - theSinex.refstrings.push_back(line); - snprintf(line, sizeof(line), " %-18s %s", "SOFTWARE", ("Ginan PEA Version " + peaVer).c_str()); - theSinex.refstrings.push_back(line); - -#ifndef _WIN32 - struct utsname buf; - int result = uname(&buf); - - if (result == 0) - { - int offset = 0; - - offset += snprintf(line + offset, sizeof(line) - offset, " %-18s ", "HARDWARE"); - - offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf.sysname); - offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf.release); - offset += snprintf(line + offset, sizeof(line) - offset, "%s ", buf.version); - - theSinex.refstrings.push_back(line); - } -#else - // Windows - provide basic hardware info - snprintf(line, sizeof(line), " %-18s %s", "HARDWARE", "Windows"); - theSinex.refstrings.push_back(line); -#endif - - snprintf(line, sizeof(line), " %-18s %s", "INPUT", "RINEX"); - theSinex.refstrings.push_back(line); - - if (isTrop) - { - snprintf( - line, - sizeof(line), - " %-18s %03d", - "VERSION NUMBER", - 1 - ); // note: increment if the processing is modified in a way that might lead to a different - // error characteristics of the product - see trop snx specs - theSinex.refstrings.push_back(line); - } - return 0; -} - -void sinexAddComment(const string what) -{ - theSinex.blockComments["FILE/COMMENT"].push_back(what); -} - -void sinexAddFiles( - const string& who, - const GTime& time, - const vector& filenames, - const string& description -) -{ - for (auto& filename : filenames) - { - SinexInputFile sif; - - sif.yds = time; - sif.agency = who; - sif.file = filename; - sif.description = description; - - theSinex.inputFiles.push_back(sif); - } -} - void setRestrictiveEndTime(UYds& current, UYds& potential) { UYds zeros; @@ -3724,7 +3996,7 @@ void setRestrictiveEndTime(UYds& current, UYds& potential) { return; } // potential time is zero, thats not restrictive, keep the current time - if (potential == current) + if (potential < current) { current = potential; return; @@ -3765,6 +4037,14 @@ GetSnxResult getRecSnx(string id, GTime time, SinexRecData& recSnx) { auto& [dummy, receiver] = *timeRecIt; + if ((GTime)receiver.end < time && receiver.end != UYds(0, 0, 0)) + { + BOOST_LOG_TRIVIAL(warning) + << "No valid time period found for Sinex receiver type of " << id + << ", using outdated data with end time " << receiver.end[0] << ":" + << receiver.end[1] << ":" << receiver.end[2]; + } + receiver.used = true; recSnx.rec_ptr = &receiver; @@ -3780,7 +4060,7 @@ GetSnxResult getRecSnx(string id, GTime time, SinexRecData& recSnx) setRestrictiveEndTime(receiver.end, nextReceiver.start); } - setRestrictiveEndTime(recSnx.start, receiver.end); + setRestrictiveEndTime(recSnx.stop, receiver.end); } } @@ -3799,6 +4079,14 @@ GetSnxResult getRecSnx(string id, GTime time, SinexRecData& recSnx) { auto& [dummy, antenna] = *antIt2; + if ((GTime)antenna.end < time && antenna.end != UYds(0, 0, 0)) + { + BOOST_LOG_TRIVIAL(warning) + << "No valid time period found for Sinex antenna type of " << id + << ", using outdated data with end time " << antenna.end[0] << ":" + << antenna.end[1] << ":" << antenna.end[2]; + } + found = true; antenna.used = true; @@ -3813,7 +4101,7 @@ GetSnxResult getRecSnx(string id, GTime time, SinexRecData& recSnx) setRestrictiveEndTime(antenna.end, nextAntenna.start); } - setRestrictiveEndTime(recSnx.start, antenna.end); + setRestrictiveEndTime(recSnx.stop, antenna.end); } } @@ -3832,22 +4120,33 @@ GetSnxResult getRecSnx(string id, GTime time, SinexRecData& recSnx) { auto& [dummy, ecc] = *eccIt2; - found = true; + if (ecc.rs == "UNE") // Eugene: What if current one is not "UNE", but next one? + { + if ((GTime)ecc.end < time && ecc.end != UYds(0, 0, 0)) + { + BOOST_LOG_TRIVIAL(warning) + << "No valid time period found for Sinex site eccentricity of " << id + << ", using outdated data with end time " << ecc.end[0] << ":" << ecc.end[1] + << ":" << ecc.end[2]; + } + + found = true; - ecc.used = true; + ecc.used = true; - recSnx.ecc_ptr = &ecc; + recSnx.ecc_ptr = &ecc; - // get next next start time as end time for this aspect - if (eccIt2 != theSinex.mapeccentricities[id].begin()) - { - eccIt2--; - auto& [dummy, nextEcc] = *eccIt2; + // get next next start time as end time for this aspect + if (eccIt2 != theSinex.mapeccentricities[id].begin()) + { + eccIt2--; + auto& [dummy, nextEcc] = *eccIt2; - setRestrictiveEndTime(ecc.end, nextEcc.start); - } + setRestrictiveEndTime(ecc.end, nextEcc.start); + } - setRestrictiveEndTime(recSnx.stop, ecc.end); + setRestrictiveEndTime(recSnx.stop, ecc.end); + } } } @@ -3878,65 +4177,305 @@ GetSnxResult getRecSnx(string id, GTime time, SinexRecData& recSnx) found = true; + auto getEstimateValue = [&](const string& type, + double& value, + GTime* refEpoch = nullptr, + double* stddevOut = nullptr, + bool updateStop = true) + { + auto& estMap = theSinex.estimatesMap[id][type]; + + SinexSolEstimate* estimate_ptr = nullptr; + GTime selectedRefEpoch; + + auto est_it = estMap.lower_bound(time); + if (est_it != estMap.end()) + { + estimate_ptr = &est_it->second; + selectedRefEpoch = est_it->first; + + // get next next start time as end time for this aspect + if (updateStop && est_it != estMap.begin()) + { + est_it--; + auto& nextEst = est_it->second; + + setRestrictiveEndTime(recSnx.stop, nextEst.refepoch); + } + } + else + { + // just use the first chronologically, (last when sorted as they are) instead + auto est_Rit = estMap.rbegin(); + if (est_Rit == estMap.rend()) + { + return false; + } + + estimate_ptr = &est_Rit->second; + selectedRefEpoch = est_Rit->first; + } + + estimate_ptr->used = true; + value = estimate_ptr->estimate; + + if (stddevOut) + { + *stddevOut = estimate_ptr->stddev; + } + + if (refEpoch) + { + *refEpoch = selectedRefEpoch; + } + + return true; + }; + + GTime staRefEpochs[3] = {}; + GTime velRefEpochs[3] = {}; + for (string type : {"STA? ", "VEL? "}) for (int i = 0; i < 3; i++) { type[3] = 'X' + i; - auto& estMap = theSinex.estimatesMap[id][type]; + double estimateValue = 0; + GTime refEpoch = {}; + double stddev = 0; - SinexSolEstimate* estimate_ptr = nullptr; + if (getEstimateValue(type, estimateValue, &refEpoch, &stddev) == false) + { + // station coordinates are mandatory, velocity can be absent. + if (type.substr(0, 3) == "STA") + found = false; + break; + } - auto est_it = estMap.lower_bound(time); - GTime refEpoch = {}; - if (est_it != estMap.end()) + if (type.substr(0, 3) == "STA") { - estimate_ptr = &est_it->second; - refEpoch = est_it->first; + recSnx.pos(i) = estimateValue; + recSnx.var(i) = SQR(stddev); + recSnx.refEpoch = refEpoch; + staRefEpochs[i] = refEpoch; + } + else if (type.substr(0, 3) == "VEL") + { + recSnx.vel(i) = estimateValue; + velRefEpochs[i] = refEpoch; + } + } - // get next next start time as end time for this aspect - if (est_it != estMap.begin()) - { - est_it--; - auto& nextEst = est_it->second; + if (recSnx.pos.norm() > 0) + { + BOOST_LOG_TRIVIAL(debug) << "SINEX: " << id + << " raw STA (ECEF m) = " << recSnx.pos.transpose() + << " (refEpochs X/Y/Z = " << staRefEpochs[0].to_string() << " / " + << staRefEpochs[1].to_string() << " / " + << staRefEpochs[2].to_string() << ")"; + } - setRestrictiveEndTime(recSnx.stop, nextEst.refepoch); - } + if (recSnx.vel.norm() > 0) + { + BOOST_LOG_TRIVIAL(debug) << "SINEX: " << id + << " raw VEL (ECEF m/yr) = " << recSnx.vel.transpose() + << " (refEpochs X/Y/Z = " << velRefEpochs[0].to_string() << " / " + << velRefEpochs[1].to_string() << " / " + << velRefEpochs[2].to_string() << ")"; + } + + double dtYears = (time - recSnx.refEpoch).to_double() / 86400 / 365.25; + VectorEcef velContribution = recSnx.vel * dtYears; // meters per year + recSnx.pos += velContribution; + + BOOST_LOG_TRIVIAL(debug) << "SINEX: " << id + << " VEL contribution (ECEF m) = " << velContribution.transpose() + << " (dtYears=" << dtYears << ")"; + + double decYear = time.to_decYear(); + double w1 = PI2 * decYear; + double w2 = 2 * w1; + + VectorEcef posBeforeSeasonal = recSnx.pos; + VectorEcef seasonalEcef; + + for (int i = 0; i < 3; i++) + { + char axis = 'X' + i; + + double a1cos = 0; + double a1sin = 0; + double a2cos = 0; + double a2sin = 0; + + GTime a1cosRef = {}; + GTime a1sinRef = {}; + GTime a2cosRef = {}; + GTime a2sinRef = {}; + + getEstimateValue(string("A1COS") + axis, a1cos, &a1cosRef, nullptr, false); + getEstimateValue(string("A1SIN") + axis, a1sin, &a1sinRef, nullptr, false); + getEstimateValue(string("A2COS") + axis, a2cos, &a2cosRef, nullptr, false); + getEstimateValue(string("A2SIN") + axis, a2sin, &a2sinRef, nullptr, false); + + if (a1cos != 0 || a1sin != 0 || a2cos != 0 || a2sin != 0) + { + BOOST_LOG_TRIVIAL(debug) + << "SINEX: " << id << " raw seasonal axis " << axis << " A1COS=" << a1cos + << " (ref=" << a1cosRef.to_string() << ") A1SIN=" << a1sin + << " (ref=" << a1sinRef.to_string() << ") A2COS=" << a2cos + << " (ref=" << a2cosRef.to_string() << ") A2SIN=" << a2sin + << " (ref=" << a2sinRef.to_string() << ")"; + } + + double seasonal = a1cos * std::cos(w1) + a1sin * std::sin(w1) + a2cos * std::cos(w2) + + a2sin * std::sin(w2); + + recSnx.pos(i) += seasonal; + seasonalEcef(i) = seasonal; + + BOOST_LOG_TRIVIAL(debug) << "SINEX: " << id << " COS/SIN contribution axis " << axis + << " (ECEF m) = " << seasonal; + } + + if (seasonalEcef.norm() > 0) + { + VectorPos pos = ecef2pos(posBeforeSeasonal); + VectorEnu seasonalEnu = ecef2enu(pos, seasonalEcef); + + BOOST_LOG_TRIVIAL(debug) << "SINEX: " << id + << " COS/SIN contribution (ENU m) = " << seasonalEnu.transpose(); + } + + VectorEnu postSeismicEnu; + + auto accumulatePostSeismic = + [&](const string& amplitudeType, const string& timeType, bool logarithmic) + { + double correction = 0; + + auto& amplitudeMap = theSinex.estimatesMap[id][amplitudeType]; + auto& timeMap = theSinex.estimatesMap[id][timeType]; + + for (auto& [refEpoch, amplitudeEstimate] : amplitudeMap) + { + if (time < refEpoch) + { + continue; } - else + + auto timeIt = timeMap.find(refEpoch); + if (timeIt == timeMap.end()) { - // just use the first chronologically, (last when sorted as they are) instead - auto est_Rit = estMap.rbegin(); - if (est_Rit == estMap.rend()) - { - // actually theres no estimate for this thing - if (type.substr(0, 3) == "STA") - found = false; - break; - } + continue; + } + + auto& timeEstimate = timeIt->second; + if (timeEstimate.estimate <= 0) + { + continue; + } - estimate_ptr = &est_Rit->second; - refEpoch = est_Rit->first; + if (amplitudeEstimate.estimate != 0) + { + BOOST_LOG_TRIVIAL(debug) + << "SINEX: " << id << " raw post-seismic " << (logarithmic ? "LOG" : "EXP") + << " refEpoch=" << refEpoch.to_string() << " amplitude(" << amplitudeType + << ")=" << amplitudeEstimate.estimate << " tau(" << timeType + << ")=" << timeEstimate.estimate; } - auto& estimate = *estimate_ptr; + double dtYears = decYear - refEpoch.to_decYear(); - estimate.used = true; + amplitudeEstimate.used = true; + timeEstimate.used = true; - if (type.substr(0, 3) == "STA") + if (logarithmic) { - recSnx.pos(i) = estimate.estimate; - recSnx.var(i) = SQR(estimate.stddev); - recSnx.refEpoch = refEpoch; + correction += + amplitudeEstimate.estimate * std::log(1 + dtYears / timeEstimate.estimate); } - else if (type.substr(0, 3) == "VEL") + else + { + correction += + amplitudeEstimate.estimate * (1 - std::exp(-dtYears / timeEstimate.estimate)); + } + } + + return correction; + }; + + auto hasEstimates = [&](const string& type) + { + auto siteIt = theSinex.estimatesMap.find(id); + if (siteIt == theSinex.estimatesMap.end()) + { + return false; + } + + auto typeIt = siteIt->second.find(type); + if (typeIt == siteIt->second.end()) + { + return false; + } + + return !typeIt->second.empty(); + }; + + for (char axis : {'N', 'E', 'U'}) + { + double corr = 0; + + string axisStr(1, axis); + bool usedAltAxis = false; + + if (axis == 'U') + { + bool haveU = hasEstimates("AEXP_" + axisStr) || hasEstimates("TEXP_" + axisStr) || + hasEstimates("ALOG_" + axisStr) || hasEstimates("TLOG_" + axisStr); + + if (!haveU) { - recSnx.vel(i) = estimate.estimate; + axisStr = "H"; + usedAltAxis = true; } } - recSnx.pos += - recSnx.vel * (time - recSnx.refEpoch).to_double() / 86400 / 365.25; // meters per year + double expCorr = + accumulatePostSeismic(string("AEXP_") + axisStr, string("TEXP_") + axisStr, false); + double logCorr = + accumulatePostSeismic(string("ALOG_") + axisStr, string("TLOG_") + axisStr, true); + + corr += expCorr + logCorr; + + BOOST_LOG_TRIVIAL(debug) << "SINEX: " << id << " post-seismic EXP/LOG contribution axis " + << axis << (usedAltAxis ? " (source H)" : "") + << " (ENU m) = " << expCorr << " / " << logCorr; + + switch (axis) + { + case 'N': + postSeismicEnu.n() = corr; + break; + case 'E': + postSeismicEnu.e() = corr; + break; + case 'U': + postSeismicEnu.u() = corr; + break; + } + } + + if (postSeismicEnu.norm() > 0) + { + VectorPos pos = ecef2pos(recSnx.pos); + VectorEcef postSeismicEcef = enu2ecef(pos, postSeismicEnu); + recSnx.pos += postSeismicEcef; + + BOOST_LOG_TRIVIAL(debug) << "SINEX: " << id + << " post-seismic total (ENU m) = " << postSeismicEnu.transpose() + << ", applied ECEF (m) = " << postSeismicEcef.transpose(); + } if (found == false) { @@ -4049,6 +4588,110 @@ GetSnxResult getSatSnx(string prn, GTime time, SinexSatSnx& satSnx) return result; } +/** Get GLONASS frequency channel from SINEX data + * Returns frequency channel number for a GLONASS satellite at a given time. + * Searches SINEX satellite frequency channel blocks to find the correct channel. + */ +int getGloFreqChannel( + const SatSys& sat, ///< Satellite to query + const GTime& time, ///< Time of observation + Navigation& nav ///< Navigation data to cache result +) +{ + if (sat.sys != E_Sys::GLO) + { + return 0; + } + + // Try to get SVN from nav.svnMap first (populated from SINEX SATELLITE/PRN block) + string svn; + auto it = nav.svnMap[sat].lower_bound(time); + if (it != nav.svnMap[sat].end()) + { + svn = it->second; + } + + // Fallback to satDataMap if svnMap lookup failed + if (svn.empty()) + { + svn = sat.svn(); + } + + BOOST_LOG_TRIVIAL(debug) << "SINEX: Querying frequency channel for " << sat.id() << " at time " + << time.to_string() << " (SVN=" << svn << ")"; + + if (svn.empty()) + { + BOOST_LOG_TRIVIAL(info) << "SINEX: No SVN available for " << sat.id() << " at time " + << time.to_string(); + return 0; + } + + // Find frequency channel for this SVN at this time + for (auto& sfc : theSinex.listsatfreqchns) + { + if (sfc.svn != svn) + continue; + + GTime startTime = sfc.start; + GTime stopTime = sfc.stop; + + // Check if stop time is 0000:000:00000 (means ongoing/no end date) + bool isOngoing = (sfc.stop[0] == 0 && sfc.stop[1] == 0 && sfc.stop[2] == 0); + + // Time must be after start, and either before stop or stop is ongoing + if (time >= startTime && (isOngoing || time <= stopTime)) + { + nav.gloFreqMap[sat] = sfc.channel; + + BOOST_LOG_TRIVIAL(debug) + << "SINEX: Found frequency channel " << sfc.channel << " for " << sat.id() + << " (SVN=" << svn << ") at time " << time.to_string(); + + return sfc.channel; + } + } + + BOOST_LOG_TRIVIAL(debug) << "SINEX: Could not find frequency channel for " << sat.id() + << " (SVN=" << svn << ") at time " << time.to_string(); + + return 0; +} + +/** Get yaw rate sinex entry for sat + */ +bool getSnxSatMaxYawRate(string svn, GTime& time, double& maxYawRate) +{ + auto itr = theSinex.satYawRateMap[svn].lower_bound(time); + if (itr == theSinex.satYawRateMap[svn].end()) + return false; + + auto& [dummy, entry] = *itr; + maxYawRate = entry.maxYawRate; + + return true; +} + +/** Get attitude mode for sat + */ +bool getSnxSatAttMode(string svn, GTime& time, string& attMode) +{ + auto itr = theSinex.satAttModeMap[svn].lower_bound(time); + if (itr == theSinex.satAttModeMap[svn].end()) + return false; + + auto& [dummy, entry] = *itr; + attMode = entry.attMode; + GTime stop = entry.stop; + + if (stop != GTime::noTime() && stop < time) + { + return false; + } + + return true; +} + void getSlrRecBias(string id, string prn, GTime time, map& recBias) { string ptcode; @@ -4158,37 +4801,3 @@ void getSlrRecBias(string id, string prn, GTime time, map& recBias continue; } } - -/** Get yaw rate sinex entry for sat - */ -bool getSnxSatMaxYawRate(string svn, GTime& time, double& maxYawRate) -{ - auto itr = theSinex.satYawRateMap[svn].lower_bound(time); - if (itr == theSinex.satYawRateMap[svn].end()) - return false; - - auto& [dummy, entry] = *itr; - maxYawRate = entry.maxYawRate; - - return true; -} - -/** Get attitude mode for sat - */ -bool getSnxSatAttMode(string svn, GTime& time, string& attMode) -{ - auto itr = theSinex.satAttModeMap[svn].lower_bound(time); - if (itr == theSinex.satAttModeMap[svn].end()) - return false; - - auto& [dummy, entry] = *itr; - attMode = entry.attMode; - GTime stop = entry.stop; - - if (stop != GTime::noTime() && stop < time) - { - return false; - } - - return true; -} diff --git a/src/cpp/common/sinex.hpp b/src/cpp/common/sinex.hpp index 65b9c6145..77ca1dcd3 100644 --- a/src/cpp/common/sinex.hpp +++ b/src/cpp/common/sinex.hpp @@ -19,12 +19,11 @@ using std::vector; struct SatSys; struct Navigation; -//=============================================================================== /* history structure (optional but recommended) * ------------------------------------------------------------------------------ +INPUT/HISTORY *CSNX FMT_ AGC EPOCH_______ AGD START_______ STOP________ T EST__ C A B C D E F -+SNX 1.23 XXX YR:DOY:SOD.. YYY YR:DOY:SOD YR:DOY:SOD C 01234 D S O E T C A + +SNX 1.23 XXX YR:DOY:SOD.. YYY YR:DOY:SOD YR:DOY:SOD C 01234 D S O E T C A */ struct SinexInputHistory { @@ -111,18 +110,18 @@ struct SinexSourceId */ struct SinexSiteId { - string sitecode; // station (4) - string ptcode; // physical monument used at the site (2) - char typecode; // observation technique {C,D,L,M,P,or R} - string domes; // domes number unique monument num (9) - string desc; // site description eg town/city (22) - int lon_deg; // longitude degrees (uint16_t) east is positive - int lon_min; // - double lon_sec; // - int lat_deg; // latitude degrees north is positive - int lat_min; // uint8_t - double lat_sec; // float - double height; // + string sitecode; // station (4) + string ptcode = "A"; // physical monument used at the site (2) + char typecode = 'P'; // observation technique {C,D,L,M,P,or R} + string domes = " M "; // domes number unique monument num (9) + string desc; // site description eg town/city (22) + int lon_deg; // longitude degrees (uint16_t) east is positive + int lon_min; // + double lon_sec; // + int lat_deg; // latitude degrees north is positive + int lat_min; // uint8_t + double lat_sec; // float + double height; // bool used = false; }; @@ -133,13 +132,13 @@ struct SinexSiteId */ struct SinexSiteData { - string site; // 4 call sign for solved parameters - string station_pt; // 2 physical - string soln_id; // 4 solution number to which this input is referred to (int?) - string sitecode; // 4 call sign from input sinex file - string site_pt; // 2 physical from above - string sitesoln; // 4 solution number for site/pt from input sinex file - char obscode; // + string site; // 4 call sign for solved parameters + string station_pt = "A"; // 2 physical + string soln_id = "----"; // 4 solution number to which this input is referred to (int?) + string sitecode; // 4 call sign from input sinex file + string site_pt = "A"; // 2 physical from above + string sitesoln = "----"; // 4 solution number for site/pt from input sinex file + char obscode = 'P'; // UYds start; UYds stop; string agency; // 3 - code agency of creation @@ -154,15 +153,15 @@ struct SinexSiteData */ struct SinexReceiver { - string sitecode; // station (4) - string ptcode; // physical monument used at the site (2) - string solnid; // solution number (4) or '----' - char typecode; - UYds start; // receiver start time - UYds end; // receiver end time - string type; // receiver type (20) - string sn; // receiver serial number (5) - string firm; // receiver firmware (11) + string sitecode; // station (4) + string ptcode = "A"; // physical monument used at the site (2) + string solnid = "----"; // solution number (4) or '----' + char typecode = 'P'; + UYds start; // receiver start time + UYds end; // receiver end time + string type; // receiver type (20) + string sn = "-----"; // receiver serial number (5) + string firm = "-----------"; // receiver firmware (11) bool used = false; }; @@ -175,14 +174,14 @@ struct SinexReceiver struct SinexAntenna { string sitecode; - string ptcode; // physical monument used at the site (2) - string solnnum; - string calibModel; - char typecode; - UYds start; /* antenna start time */ - UYds end; /* antenna end time */ - string type; /* receiver type (20)*/ - string sn; /* receiver serial number (5)*/ + string ptcode = "A"; // physical monument used at the site (2) + string solnnum = "----"; + string calibModel = "-----"; + char typecode = 'P'; + UYds start; /* antenna start time */ + UYds end; /* antenna end time */ + string type; /* receiver type (20)*/ + string sn = "-----"; /* receiver serial number (5)*/ bool used = false; }; @@ -192,11 +191,11 @@ struct SinexAntenna */ struct SinexGpsPhaseCenter { - string antname; // 20 name and model - string serialno; // 5 - Vector3d L1; // UNE d6.4*3 - Vector3d L2; // UNE d6.4*3 - string calib; // 10 calibration model + string antname; // 20 name and model + string serialno = "-----"; // 5 + Vector3d L1; // UNE d6.4*3 + Vector3d L2; // UNE d6.4*3 + string calib = "----------"; // 10 calibration model }; /* gal phase centre block structure (mandatory for Gallileo) @@ -208,14 +207,14 @@ struct SinexGpsPhaseCenter */ struct SinexGalPhaseCenter { - string antname; // 20 name and model - string serialno; // 5 - Vector3d L1; // UNE d6.4*3 - Vector3d L5; // UNE d6.4*3 - Vector3d L6; // UNE d6.4*3 - Vector3d L7; // UNE d6.4*3 - Vector3d L8; // UNE d6.4*3 - string calib; // 10 calibration model + string antname; // 20 name and model + string serialno = "-----"; // 5 + Vector3d L1; // UNE d6.4*3 + Vector3d L5; // UNE d6.4*3 + Vector3d L6; // UNE d6.4*3 + Vector3d L7; // UNE d6.4*3 + Vector3d L8; // UNE d6.4*3 + string calib = "----------"; // 10 calibration model }; /* @@ -225,180 +224,17 @@ struct SinexGalPhaseCenter */ struct SinexSiteEcc { - string sitecode; // 4 - string ptcode; // 2 - physical monument used at the site - string solnnum; - char typecode; - UYds start; /* ecc start time */ - UYds end; /* ecc end time */ - string rs; /* 3 - reference system UNE (0) or XYZ (1) */ - VectorEnu ecc; /* eccentricity UNE or XYZ (m) d8.4*3 */ + string sitecode; // 4 + string ptcode = "A"; // 2 - physical monument used at the site + string solnnum = "----"; + char typecode = 'P'; + UYds start; /* ecc start time */ + UYds end; /* ecc end time */ + string rs = "UNE"; /* 3 - reference system UNE (0) or XYZ (1) */ + VectorEnu ecc; /* eccentricity UNE or XYZ (m) d8.4*3 */ bool used = false; }; -/* -+SOLUTION/EPOCHS (mandatory) *OR* -+BIAS/EPOCHS (mandatory when biases are included) -*CODE PT SOLN T _DATA_START_ __DATA_END__ _MEAN_EPOCH_ - ALBH A 1 C 94:002:00000 94:104:00000 94:053:00000 -*/ -struct SinexSolEpoch -{ - string sitecode; // 4 - string ptcode; // 2 - physical monument used at the site - string solnnum; - char typecode; - UYds start; - UYds end; - UYds mean; -}; - -/* -+SOLUTION/STATISTICS -*STAT_NAME (30 chars) value (22char double) -*/ -struct SinexSolStatistic -{ - string name; - short etype; // 0 = int, 1 = double - union - { - int ival; - double dval; - } value; -}; - -/* -+SOLUTION/ESTIMATE -*INDEX _TYPE_ CODE PT SOLN _REF_EPOCH__ UNIT S ___ESTIMATED_VALUE___ __STD_DEV__ - 1 STAX ALBH A 1 10:001:00000 m 2 -2.34133301687257e+06 5.58270e-04 - 2 STAY ALBH A 1 10:001:00000 m 2 -3.53904951624333e+06 7.77370e-04 - 3 STAZ ALBH A 1 10:001:00000 m 2 4.74579129951391e+06 8.98560e-04 - 4 VELX ALBH A 1 10:001:00000 m/y 2 -9.92019926884722e-03 1.67050e-05 - 5 VELY ALBH A 1 10:001:00000 m/y 2 -8.46787398931193e-04 2.12080e-05 - 6 VELZ ALBH A 1 10:001:00000 m/y 2 -4.85721729753769e-03 2.39140e-05 -*/ -struct SinexSolEstimate -{ - int index; - string type; // 6 - string sitecode; // 4 - string ptcode; // 2 - physical monument used at the site - string solnnum; - UYds refepoch; - string unit; // 4 - char constraint; - double estimate; - double stddev; - string file; - - bool used = false; -}; - -/* -+SOLUTION/APRIORI -*INDEX PARAMT SITE PT SOLN EPOCH_____ UNIT C PARAM________________ STD_DEV____ - 12345 AAAAAA XXXX YY NNNN YR:DOY:SOD UUUU A 12345.123456789ABCDEF 1234.123456 -*/ -struct SinexSolApriori -{ - int idx; - string param_type; // 6 - select from - string sitecode; // 4 - string ptcode; // 2 - string solnnum; - UYds epoch; - string unit; // 4 - select from - char constraint; // for inner constraints, choose 1 - double param; // d21.15 apriori parameter - double stddev; // std deviation of parameter -}; - -/* -+SOLUTION/NORMAL_EQUATION_VECTOR -*PARAM PTYPE_ SITE PT SOLN EPOCH_____ UNIT C NORMAL______________ - 12345 AAAAAA XXXX YY NNNN YR:DOY:SOD UUUU A 12345.123456789ABCDEF -*/ -struct SinexSolNeq -{ - int param; // 5 index of estimated parameters - string ptype; // 6 - type of parameter - string site; // 4 - station - string pt; // 2 - point code - string solnnum; // 4 solution number - UYds epoch; - string unit; // 4 - char constraint; // - double normal; // right hand side of normal equation -}; - -/* -+SOLUTION/MATRIX_ESTIMATE C TYPE (mandatory) -+SOLUTION/MATRIX_APRIORI C TYPE (recommended) -+SOLUTION/MATRIX_NORMAL_EQUATION C (mandatory for normal equations) -* (Not used until I understand it better) -* C must be L or U (matrix is always symmetric about main diagonal) -* TYPE must be one of CORR/COVA/INFO for correlation, covariance and info (covariance inverse) -* APRIORI VALUES are 21.16lf, estimates and normal_equations are 21.14lf! -*ROW__ COL__ ELEM1________________ ELEM2________________ ELEM3________________ -*/ -struct SinexSolMatrix -{ - int row; // 5 - must match the solution/estimate row - int col; // 5 - must match the solution/estimate col - int numvals; - double value[3]; // each d21.14 cols col, col+1, col+2 of the row -}; - -//============================================================================= -/* -+SOLUTION/DATA_HANDLING -*CODE PT UNIT T _DATA_START_ __DATA_END__ M __E-VALUE___ STD_DEV _E-RATE__ CMNTS - 7090 -- ms A 09:344:31560 09:345:70200 T 0.90920 - 7840 -- % A 95:358:00000 95:358:86400 H -20.00 HER - 7080 -- mB A 95:065:00000 96:026:00000 P -2.10 - 1873 -- mm A 95:001:00000 00:001:00000 R -270.00 -*/ -//============================================================================= -struct SinexDataHandling -{ - string sitecode; // 4 - CDP ID - string ptcode; // 2 - satellites these biases apply to (-- = all) - string solnnum; // 4 - solution number - string t; // 1 - UYds epochstart; // yr:doy:sod - UYds epochend; // yr:doy:sod - string m; // 1 - double estimate; - double stddev; - double estrate; - string unit; // 4 - units of estimate - string comments; // 4 -}; - -typedef enum -{ - ESTIMATE, - APRIORI, - NORMAL_EQN, - MAX_MATRIX_TYPE -} matrix_type; - -typedef enum -{ - CORRELATION, - COVARIANCE, - INFORMATION, - MAX_MATRIX_VALUE -} matrix_value; - -typedef enum -{ - P_ANT, // P: antenna //todo: check the meaning of 'P' - L_LRA // L: laser retroreflector array -} E_EccType; - -//============================================================================= // TODO: satid and satident/satprn need to be checked for consistency ... /* +SATELLITE/ID (recommmended for GNSS) @@ -418,6 +254,23 @@ struct SinexSatId string antRcvType; // 20 - satellite antenna receiver type }; +/* ++SATELLITE/PHASE_CENTER +*NB Can have more than one line if satellite transmits on more than 2 frequencies +*SVN_ C ZZZZZZ XXXXXX YYYYYY C ZZZZZZ XXXXXX YYYYYY ANTENNA___ T M +*/ +struct SinexSatPc +{ + string svn; // 4 + char freq; // 1/2/5 for GPS & GLONASS, 1/5/6/7/8 for Gallileo + Vector3d zxy; // metres offset from COM in the order given 3* d6.4 + char freq2; // as above + Vector3d zxy2; // as above + string antenna; // 10 - model of antenna + char type; // Phase Center Variation A(bsolute)/R(elative) + char model; // F(ull)/E(levation model only) +}; + /* +SATELLITE/IDENTIFIER *SVN_ COSPAR_ID SatCat Block__________ Comment__________________________________ @@ -460,6 +313,21 @@ struct SinexSatFreqChn string comment; // 40? }; +/* ++SATELLITE/PLANE +*SVN_ Valid_From____ Valid_To______ P Slot__ Comment____________________________ + G032 2000:028:00000 2004:181:00000 6 F4 [PL05] +*/ +struct SinexSatPlane +{ + string svn; // 4 + UYds start; // yr:doy:sod + UYds stop; // yr:doy:sod + char plane; // orbital plane + string slot; // 6 + string comment; // 35 +}; + /* +SATELLITE/MASS *SVN_ Valid_From____ Valid_To______ Mass_[kg] Comment___________________________ @@ -516,23 +384,6 @@ struct SinexSatPower string comment; // 40 }; -/* -+SATELLITE/PHASE_CENTER -*NB Can have more than one line if satellite transmits on more than 2 frequencies -*SVN_ C ZZZZZZ XXXXXX YYYYYY C ZZZZZZ XXXXXX YYYYYY ANTENNA___ T M -*/ -struct SinexSatPc -{ - string svn; // 4 - char freq; // 1/2/5 for GPS & GLONASS, 1/5/6/7/8 for Gallileo - Vector3d zxy; // metres offset from COM in the order given 3* d6.4 - char freq2; // as above - Vector3d zxy2; // as above - string antenna; // 10 - model of antenna - char type; // Phase Center Variation A(bsolute)/R(elative) - char model; // F(ull)/E(levation model only) -}; - /* +SATELLITE/YAW_BIAS_RATE *SVN_ Valid_From____ Valid_To______ YB Yaw Rate Comment________________________________ @@ -562,6 +413,144 @@ struct SinexSatAttMode string attMode; ///< attitude mode }; +/* ++SOLUTION/EPOCHS (mandatory) *OR* ++BIAS/EPOCHS (mandatory when biases are included) +*CODE PT SOLN T _DATA_START_ __DATA_END__ _MEAN_EPOCH_ + ALBH A 1 C 94:002:00000 94:104:00000 94:053:00000 +*/ +struct SinexSolEpoch +{ + string sitecode; // 4 + string ptcode = "A"; // 2 - physical monument used at the site + string solnnum = "1"; + char typecode = 'P'; + UYds start; + UYds end; + UYds mean; +}; + +/* ++SOLUTION/STATISTICS +*STAT_NAME (30 chars) value (22char double) +*/ +struct SinexSolStatistic +{ + string name; + short etype; // 0 = int, 1 = double + union + { + int ival; + double dval; + } value; +}; + +/* ++SOLUTION/ESTIMATE +*INDEX _TYPE_ CODE PT SOLN _REF_EPOCH__ UNIT S ___ESTIMATED_VALUE___ __STD_DEV__ + 1 STAX ALBH A 1 10:001:00000 m 2 -2.34133301687257e+06 5.58270e-04 + 2 STAY ALBH A 1 10:001:00000 m 2 -3.53904951624333e+06 7.77370e-04 + 3 STAZ ALBH A 1 10:001:00000 m 2 4.74579129951391e+06 8.98560e-04 + 4 VELX ALBH A 1 10:001:00000 m/y 2 -9.92019926884722e-03 1.67050e-05 + 5 VELY ALBH A 1 10:001:00000 m/y 2 -8.46787398931193e-04 2.12080e-05 + 6 VELZ ALBH A 1 10:001:00000 m/y 2 -4.85721729753769e-03 2.39140e-05 +*/ +struct SinexSolEstimate +{ + int index; + string type; // 6 + string sitecode; // 4 + string ptcode = "A"; // 2 - physical monument used at the site + string solnnum = "1"; + UYds refepoch; + string unit; // 4 + char constraint; + double estimate; + double stddev; + string file; + + bool used = false; +}; + +/* ++SOLUTION/APRIORI +*INDEX PARAMT SITE PT SOLN EPOCH_____ UNIT C PARAM________________ STD_DEV____ + 12345 AAAAAA XXXX YY NNNN YR:DOY:SOD UUUU A 12345.123456789ABCDEF 1234.123456 +*/ +struct SinexSolApriori +{ + int idx; + string param_type; // 6 - select from + string sitecode; // 4 + string ptcode = "A"; // 2 + string solnnum = "1"; + UYds epoch; + string unit; // 4 - select from + char constraint; // for inner constraints, choose 1 + double param; // d21.15 apriori parameter + double stddev; // std deviation of parameter +}; + +/* ++SOLUTION/NORMAL_EQUATION_VECTOR +*PARAM PTYPE_ SITE PT SOLN EPOCH_____ UNIT C NORMAL______________ + 12345 AAAAAA XXXX YY NNNN YR:DOY:SOD UUUU A 12345.123456789ABCDEF +*/ +struct SinexSolNeq +{ + int param; // 5 index of estimated parameters + string ptype; // 6 - type of parameter + string site; // 4 - station + string pt = "A"; // 2 - point code + string solnnum = "1"; // 4 solution number + UYds epoch; + string unit; // 4 + char constraint; // + double normal; // right hand side of normal equation +}; + +/* ++SOLUTION/MATRIX_ESTIMATE C TYPE (mandatory) ++SOLUTION/MATRIX_APRIORI C TYPE (recommended) ++SOLUTION/MATRIX_NORMAL_EQUATION C (mandatory for normal equations) +* (Not used until I understand it better) +* C must be L or U (matrix is always symmetric about main diagonal) +* TYPE must be one of CORR/COVA/INFO for correlation, covariance and info (covariance inverse) +* APRIORI VALUES are 21.16lf, estimates and normal_equations are 21.14lf! +*ROW__ COL__ ELEM1________________ ELEM2________________ ELEM3________________ +*/ +struct SinexSolMatrix +{ + int row; // 5 - must match the solution/estimate row + int col; // 5 - must match the solution/estimate col + int numvals; + double value[3]; // each d21.14 cols col, col+1, col+2 of the row +}; + +/* ++SOLUTION/DATA_HANDLING +*CODE PT UNIT T _DATA_START_ __DATA_END__ M __E-VALUE___ STD_DEV _E-RATE__ CMNTS + 7090 -- ms A 09:344:31560 09:345:70200 T 0.90920 + 7840 -- % A 95:358:00000 95:358:86400 H -20.00 HER + 7080 -- mB A 95:065:00000 96:026:00000 P -2.10 + 1873 -- mm A 95:001:00000 00:001:00000 R -270.00 +*/ +struct SinexDataHandling +{ + string sitecode; // 4 - CDP ID + string ptcode = "--"; // 2 - satellites these biases apply to (-- = all) + string solnnum = "1"; // 4 - solution number + string t; // 1 + UYds epochstart; // yr:doy:sod + UYds epochend; // yr:doy:sod + string m; // 1 + double estimate; + double stddev; + double estrate; + string unit; // 4 - units of estimate + string comments; // 4 +}; + /* +TROP/DESCRIPTION *_________KEYWORD_____________ __VALUE(S)_______________________________________ @@ -605,38 +594,61 @@ struct Sinex string currentFile; /* header block */ - string snxtype; /* SINEX file type */ + // string snxtype; /* SINEX file type */ double ver; /* version */ string createagc; /* file creation agency */ UYds filedate; /* file create date as yr:doy:sod */ string dataagc; /* data source agency */ UYds solutionstartdate; // start date of solution - UYds solutionenddate; + UYds solutionenddate; // end date of solution char obsCode; /* observation code */ int numparam; /* number of estimated parameters */ char constCode; /* constraint code */ string solcont; /* solution types S O E T C A */ - string markerName; + string markerName; // marker name for Troposphere Sinex map> blockComments; - list refstrings; - list inputHistory; - list inputFiles; - list acknowledgements; + + /* file stuff */ + list refstrings; + list fileComments; + + /* input stuff */ + list inputHistory; + list inputFiles; + list acknowledgements; + + /* VLBI stuff - ignored for now */ + list listnutcodes; + list listprecessions; + list listsourceids; /* site stuff */ map mapsiteids; list listsitedata; map>> mapreceivers; map>> mapantennas; - map>> mapeccentricities; list listgpspcs; list listgalpcs; + map>> mapeccentricities; + + /* satellite stuff */ + list listsatids; + list listsatpcs; + map satIdentityMap; + list listsatprns; + list listsatfreqchns; + map>> satPlaneMap; + map> mapsatmasses; + list listsatcoms; + list listsateccs; + map> mapsatpowers; + map>> satYawRateMap; + map>> satAttModeMap; /* solution stuff - tied to sites */ - bool epochshavebias; - map solEpochMap; - // list list_solepochs; + bool epochshavebias; + map solEpochMap; list liststatistics; map>>> estimatesMap; map apriorimap; @@ -645,49 +657,84 @@ struct Sinex map>>>> mapdatahandling; - /* satellite stuff */ - list listsatpcs; - list listsatids; - map satIdentityMap; - - map> mapsatmasses; - map> mapsatpowers; - - list listsatprns; - list listsatfreqchns; - list listsatcoms; - list listsateccs; - - map>> satYawRateMap; - map>> satAttModeMap; - - /* VLBI - ignored for now */ - list listsourceids; - list listnutcodes; - list listprecessions; + // Troposphere Sinex data + SinexTropDesc tropDesc = {}; + map tropSiteCoordBodyFPosMap; + map tropSiteCoordMapMap; // indexed by station ID, then axis # + map tropSolFootFPosMap; + list tropSolList; // constructor Sinex(bool epochshavebias = false) : epochshavebias(epochshavebias) { }; - - // Troposphere Sinex data - map tropSiteCoordBodyFPosMap; - map tropSolFootFPosMap; - SinexTropDesc tropDesc = {}; - map tropSiteCoordMapMap; // indexed by station ID, then axis # - list tropSolList; }; -struct Sinex_stn_soln -{ - string type; /* parameter type */ - string unit; /* parameter units */ - double pos = 0; /* real position (ecef) (m)*/ - double pstd = 0; /* position std (m) */ - UYds yds; /* epoch when valid */ -}; +extern Sinex theSinex; // the one and only sinex object. + +// struct Sinex_stn_soln +// { +// string type; /* parameter type */ +// string unit; /* parameter units */ +// double pos = 0; /* real position (ecef) (m)*/ +// double pstd = 0; /* position std (m) */ +// UYds yds; /* epoch when valid */ +// }; + +void nearestYear(double& year); + +bool readSinex(const string& filepath); + +void updateSinexHeader( + string& create_agc, + string& data_agc, /* satellite meta data */ + UYds soln_start, + UYds soln_end, + const char obsCode, + const char constCode, + string& contents, + int numParam, + double sinexVer +); + +int sinexCheckAddGaReference(string solType, string peaVer, bool isTrop); +void sinexAddComment(const string what); +void sinexAddFiles( + const string& who, + const GTime& when, + const vector& filenames, + const string& description +); +// void sinexAddAcknowledgement(const string& who, const string& description); +void sinexAddStatistic(const string& what, const int value); +void sinexAddStatistic(const string& what, const double value); + +// snx.cpp fns used in tropSinex.cpp +void writeAsComments(Trace& out, list& comments); +void writeSnxReference(std::ofstream& out); + +struct KFState; +struct Receiver; + +void writeSinex( + string filepath, + KFState& kfState, + map& receiverMap, + UYds soln_start, + UYds soln_end +); + +// Trop sinex +void outputTropSinex( + string filename, + GTime time, + KFState& netKfState, + string markerName = "MIX", + bool isSmoothed = false +); + +struct SinexRecData; extern SinexSatIdentity dummySinexSatIdentity; extern SinexSatEcc dummySinexSatEcc; @@ -695,14 +742,8 @@ extern SinexSatEcc dummySinexSatEcc; /* satellite meta data */ struct SinexSatSnx { - string svn; - string prn; - SinexSatIdentity* id_ptr = &dummySinexSatIdentity; - SinexSatEcc* ecc_ptrs[2] = {&dummySinexSatEcc, &dummySinexSatEcc}; - double mass; /* kg */ - int channel; /* GLONASS ONLY */ - Vector3d com; /* centre of mass offsets (m) */ - int power; /* Tx Power (watts); */ + SinexSatIdentity* id_ptr = + &dummySinexSatIdentity; // Eugene: should be initialised with nullptr? string antenna; int numfreqs; /* number of phase center frequencies */ @@ -711,21 +752,18 @@ struct SinexSatSnx char pctype; char pcmodel; + string svn; + string prn; + int channel; /* GLONASS ONLY */ + double mass; /* kg */ + Vector3d com; /* centre of mass offsets (m) */ + SinexSatEcc* ecc_ptrs[2] = {&dummySinexSatEcc, &dummySinexSatEcc}; + int power; /* Tx Power (watts); */ + UYds start; UYds stop; }; -void nearestYear(double& year); - -bool readSinex(const string& filepath); - -struct KFState; -struct Receiver; - -void writeSinex(string filepath, KFState& kfState, map& receiverMap); - -struct SinexRecData; - union GetSnxResult { const unsigned int failure = 0; @@ -745,57 +783,12 @@ union GetSnxResult GetSnxResult getRecSnx(string id, GTime time, SinexRecData& snx); GetSnxResult getSatSnx(string prn, GTime time, SinexSatSnx& snx); -void getSlrRecBias(string id, string prn, GTime time, map& recBias); int getGloFreqChannel(const SatSys& sat, const GTime& time, Navigation& nav); - -void sinexAddStatistic(const string& what, const int value); -void sinexAddStatistic(const string& what, const double value); -int sinexCheckAddGaReference(string solType, string peaVer, bool isTrop); -void sinexAddAcknowledgement(const string& who, const string& description); -void sinexAddComment(const string what); -void sinexAddFiles( - const string& who, - const GTime& when, - const vector& filenames, - const string& description -); - -void updateSinexHeader( - string& create_agc, - string& data_agc, /* satellite meta data */ - UYds soln_start, - UYds soln_end, - const char obsCode, - const char constCode, - string& contents, - int numParam, - double sinexVer -); +bool getSnxSatMaxYawRate(string prn, GTime& time, double& maxYawRate); +bool getSnxSatAttMode(string svn, GTime& time, string& attMode); +// bool getSnxSatBlockType(string svn, string& blockType); +void getSlrRecBias(string id, string prn, GTime time, map& recBias); void sinexPostProcessing(GTime time, map& receiverMap, KFState& netKFState); - +void updateReceiverMetadata(GTime time, Receiver& rec); void sinexPerEpochPerStation(Trace& trace, GTime time, Receiver& rec); - -// Trop sinex -void outputTropSinex( - string filename, - GTime time, - KFState& netKfState, - string markerName = "MIX", - bool isSmoothed = false -); - -// snx.cpp fns used in tropSinex.cpp -void writeAsComments(Trace& out, list& comments); - -void writeSnxReference(std::ofstream& out); - -bool getSnxSatMaxYawRate(string prn, GTime& time, double& maxYawRate); - -bool getSnxSatBlockType(string svn, string& blockType); - -bool getSnxSatAttMode(string svn, GTime& time, string& attMode); - -extern Sinex theSinex; // the one and only sinex object. - -void getReceiversFromSinex(map& receiverMap, KFState& kfState); diff --git a/src/cpp/common/sp3Write.cpp b/src/cpp/common/sp3Write.cpp index 2e9c99ffe..8453be38d 100644 --- a/src/cpp/common/sp3Write.cpp +++ b/src/cpp/common/sp3Write.cpp @@ -282,7 +282,7 @@ void updateSp3Body( { writeSp3Header(sp3Stream, entryList, time, outSys, sp3FileData); } - else // todo Eugene: update accuracy sigmas as well + else // todo? update accuracy sigmas as well { sp3FileData.numEpoch++; diff --git a/src/cpp/common/streamCustom.cpp b/src/cpp/common/streamCustom.cpp index 1d3e5da73..e525b26b1 100644 --- a/src/cpp/common/streamCustom.cpp +++ b/src/cpp/common/streamCustom.cpp @@ -63,7 +63,7 @@ void CustomParser::parse(std::istream& inputStream) inputStream.read((char*)&crcRead, 2); CLEAN_UP_AND_RETURN_ON_FAILURE; - // todo aaron calculate crcRead + // todo? calculate crcRead if (0) { checksumFailure(); diff --git a/src/cpp/common/streamFile.hpp b/src/cpp/common/streamFile.hpp index ac4585eb4..6741e309b 100644 --- a/src/cpp/common/streamFile.hpp +++ b/src/cpp/common/streamFile.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -9,43 +10,54 @@ using std::make_unique; using std::string; using std::unique_ptr; -struct FileState : std::ifstream +struct FileState : std::istream { - long int& filePos; - - FileState(string path, long int& filePos, std::ifstream::openmode mode = std::ifstream::in | std::ios::binary) - : filePos{filePos} + std::ifstream& persistentStream; + long int& filePos; + + FileState( + std::ifstream& persistentStream, + const string& path, + long int& filePos, + std::ifstream::openmode mode = std::ifstream::in | std::ios::binary + ) + : std::istream(nullptr), persistentStream{persistentStream}, filePos{filePos} { + this->persistentStream.clear(); + if (filePos < 0) { - // BOOST_LOG_TRIVIAL(error) << "Error seeking to negative position in file at " - // << path << " to " - // << filePos; - close(); + this->persistentStream.setstate(std::ios::failbit); + setstate(std::ios::failbit); return; } - open(path, mode); - - if (!*this) + if (this->persistentStream.is_open() == false) { - BOOST_LOG_TRIVIAL(error) << "Error opening file at " << path << "\n" - << " - " << strerror(errno); - filePos = -1; - return; + this->persistentStream.open(path, mode); + + if (!this->persistentStream) + { + BOOST_LOG_TRIVIAL(error) << "Error opening file at " << path; + filePos = -1; + setstate(std::ios::failbit); + return; + } } - seekg(filePos); + this->persistentStream.seekg(filePos); - if (!*this) + if (!this->persistentStream) { - BOOST_LOG_TRIVIAL(error) - << "Error seeking in file at " << filePos << " in " << path << "\n" - << " - " << strerror(errno); + BOOST_LOG_TRIVIAL(error) << "Error seeking in file at " << filePos << " in " << path; filePos = -1; + setstate(std::ios::failbit); return; } + + rdbuf(this->persistentStream.rdbuf()); + clear(); } ~FileState() { filePos = streamPos(*this); } @@ -53,37 +65,26 @@ struct FileState : std::ifstream struct FileStream : Stream { - string path; - long int filePos = 0; + string path; + long int filePos = 0; + std::ifstream persistentStream; - FileStream(string path) : path(path) {} + FileStream(const string& path) : path(path) {} unique_ptr getIStream_ptr() override { - // std::cout << "Getting FileStream" << "\n"; - - return make_unique(path, filePos); + return make_unique(persistentStream, path, filePos); } - bool isDead() override - { - if (filePos < 0) - { - return true; - } - - return false; - } + bool isDead() override { return filePos < 0; } bool isAvailable() override { - std::ifstream input(path, std::ifstream::in); - - if (input) + if (persistentStream.is_open()) { return true; } - return false; + return std::filesystem::exists(path) && std::filesystem::is_regular_file(path); } }; diff --git a/src/cpp/common/streamObs.hpp b/src/cpp/common/streamObs.hpp index 92ee10908..297bfce5f 100644 --- a/src/cpp/common/streamObs.hpp +++ b/src/cpp/common/streamObs.hpp @@ -14,6 +14,8 @@ struct ObsStream : StreamParser { E_ObsAgeCode obsAgeCode = E_ObsAgeCode::CURRENT_OBS; ///< Age code of observation retrieved from memory + GTime lastReadTime = GTime::noTime(); + double interval = 0; bool isPseudoRec; @@ -32,106 +34,98 @@ struct ObsStream : StreamParser { auto& obsLister = dynamic_cast(parser); - if (obsLister.obsListList.size() < 2) + for (auto it = obsLister.obsListList.begin(); it != obsLister.obsListList.end();) { + if (it->empty()) + { + BOOST_LOG_TRIVIAL(info) + << "Dropping empty ObsList from parser queue before getObs" + << ", parser=" << parser.parserType() << ", source=" << stream.sourceString + << ", lastReadTime=" + << (lastReadTime == GTime::noTime() ? string("noTime") + : lastReadTime.to_string(6)) + << ", queued_epochs=" << obsLister.obsListList.size(); + it = obsLister.obsListList.erase(it); + } + else + { + ++it; + } + } + + BOOST_LOG_TRIVIAL(debug) + << "obsLister.obsListList.size()=" << obsLister.obsListList.size(); + + if (obsLister.obsListList.size() < 2 && stream.isDead() == false) + { + BOOST_LOG_TRIVIAL(debug) << "Not enough data in master list, reading new obs ..."; + parse(); } + else if (obsLister.obsListList.size() >= 2) + { + BOOST_LOG_TRIVIAL(debug) + << "Plenty of data in master list, no need to read more obs"; + } + else + { + BOOST_LOG_TRIVIAL(debug) << "Input stream is dead, skip reading ..."; + } if (obsLister.obsListList.empty()) { + BOOST_LOG_TRIVIAL(debug) << "No obs"; + return ObsList(); } - ObsList& obsList = obsLister.obsListList.front(); - - for (auto& obs : only(obsList)) - for (auto& [ftype, sigsList] : obs.sigsLists) + ObsList& latestObsList = obsLister.obsListList.back(); + if (latestObsList.empty() == false) + { + if (lastReadTime != GTime::noTime()) { - E_Sys sys = obs.Sat.sys; + double newInterval = (latestObsList.front()->time - lastReadTime).to_double(); - if (sys == E_Sys::GPS) - { - double dirty_C1W_phase = 0; - for (auto& sig : sigsList) - { - if (sig.code == E_ObsCode::L1C) - dirty_C1W_phase = sig.L; - - if (sig.code == E_ObsCode::L1W && sig.P == 0) - { - sig.L = 0; - } - } - - for (auto& sig : sigsList) - if (sig.code == E_ObsCode::L1W && sig.L == 0 && sig.P != 0) - { - sig.L = dirty_C1W_phase; - break; - } - } - sigsList.remove_if( - [sys](Sig& a) - { - return std::find( - acsConfig.code_priorities[sys].begin(), - acsConfig.code_priorities[sys].end(), - a.code - ) == acsConfig.code_priorities[sys].end(); - } - ); - sigsList.sort( - [sys](Sig& a, Sig& b) - { - auto iterA = std::find( - acsConfig.code_priorities[sys].begin(), - acsConfig.code_priorities[sys].end(), - a.code - ); - auto iterB = std::find( - acsConfig.code_priorities[sys].begin(), - acsConfig.code_priorities[sys].end(), - b.code - ); - - if (a.L == 0) - return false; - if (b.L == 0) - return true; - if (a.P == 0) - return false; - if (b.P == 0) - return true; - if (iterA < iterB) - return true; - else - return false; - } - ); - - if (sigsList.empty()) + if (newInterval > 0 && (interval <= 0 || interval > newInterval)) { - continue; + interval = newInterval; } + } - Sig firstOfType = sigsList.front(); + lastReadTime = latestObsList.front()->time; + } + else + { + BOOST_LOG_TRIVIAL(info) + << "Latest ObsList is empty after parse" + << ", parser=" << parser.parserType() << ", source=" << stream.sourceString + << ", lastReadTime=" + << (lastReadTime == GTime::noTime() ? string("noTime") + : lastReadTime.to_string(6)) + << ", queued_epochs=" << obsLister.obsListList.size(); + } - // use first of type as representative if its in the priority list - auto iter = std::find( - acsConfig.code_priorities[sys].begin(), - acsConfig.code_priorities[sys].end(), - firstOfType.code - ); - if (iter != acsConfig.code_priorities[sys].end()) - { - obs.sigs[ftype] = Sig(firstOfType); - } - } + ObsList& obsList = obsLister.obsListList.front(); + if (obsList.empty()) + { + BOOST_LOG_TRIVIAL(info) + << "Front ObsList is empty after parse" + << ", parser=" << parser.parserType() << ", source=" << stream.sourceString + << ", lastReadTime=" + << (lastReadTime == GTime::noTime() ? string("noTime") + : lastReadTime.to_string(6)) + << ", queued_epochs=" << obsLister.obsListList.size(); + return ObsList(); + } + + BOOST_LOG_TRIVIAL(debug) + << "Getting front ..., obsTime=" << obsList.front()->time.to_string(6); return obsList; } catch (...) { + BOOST_LOG_TRIVIAL(debug) << "Error getting obs"; } return ObsList(); @@ -149,7 +143,7 @@ struct ObsStream : StreamParser * NOTE: This function may be overridden by objects that use this interface */ ObsList getObs( - GTime time, ///< Timestamp to get observations for + GTime& time, ///< Timestamp to get observations for double delta = 0.5 ///< Acceptable tolerance around requested time ) { @@ -166,10 +160,10 @@ struct ObsStream : StreamParser } else if (time == GTime::noTime()) { - // Start epoch not given, use time of first obs as start time - foundGoodObs = true; - dropObs(); - bigObsList += obsList; + // Start epoch not given, get first obs time + obsAgeCode = E_ObsAgeCode::UNKNOWN; + time = obsList.front()->time; + BOOST_LOG_TRIVIAL(debug) << "obsAgeCode=" << obsAgeCode << ", dropping front"; break; } else if (obsList.front()->time < time - delta) @@ -177,6 +171,7 @@ struct ObsStream : StreamParser // Save earlier data to preprocess in case preprocess_all_data is on obsAgeCode = E_ObsAgeCode::PAST_OBS; dropObs(); + BOOST_LOG_TRIVIAL(debug) << "obsAgeCode=" << obsAgeCode << ", dropping front"; if (foundGoodObs == false) { // Only push past obs when good obs not found yet, i.e. drop past obs coming @@ -190,6 +185,7 @@ struct ObsStream : StreamParser { // Future obs, do nothing and leave the data to read later obsAgeCode = E_ObsAgeCode::FUTURE_OBS; + BOOST_LOG_TRIVIAL(debug) << "obsAgeCode=" << obsAgeCode << ", checking next epoch"; break; } else @@ -198,6 +194,8 @@ struct ObsStream : StreamParser foundGoodObs = true; dropObs(); bigObsList += obsList; + BOOST_LOG_TRIVIAL(debug) + << "obsAgeCode=" << E_ObsAgeCode::CURRENT_OBS << ", dropping front"; } } @@ -207,10 +205,6 @@ struct ObsStream : StreamParser // obs (obsAgeCode is now NO_OBS) even good obs found, reset obsAgeCode to CURRENT_OBS obsAgeCode = E_ObsAgeCode::CURRENT_OBS; } - else if (obsAgeCode == E_ObsAgeCode::FUTURE_OBS) - { - return ObsList(); - } return bigObsList; } diff --git a/src/cpp/common/streamParser.cpp b/src/cpp/common/streamParser.cpp index aac4ddeb4..13d8a6ed1 100644 --- a/src/cpp/common/streamParser.cpp +++ b/src/cpp/common/streamParser.cpp @@ -9,36 +9,30 @@ map streamDOAMap; long int streamPos(std::istream& stream) { // std::cout << "Closed" << "\n"; - if (stream) - { - long int filePos = stream.tellg(); - - if (!stream) - { - BOOST_LOG_TRIVIAL(error) << "Error telling in file at " << filePos << "\n" - << " - " << strerror(errno); - - return -1; - } - - if (filePos < 0) - { - BOOST_LOG_TRIVIAL(error) << "Negative file pos in file at " << filePos << "\n" - << " - " << strerror(errno); - return -1; - } - - return filePos; + if (stream.bad()) + { + BOOST_LOG_TRIVIAL(debug) << "Bad input stream"; + return -1; } - else + if (stream.eof()) { - // BOOST_LOG_TRIVIAL(error) << "InputStream is dead before destruction "; + BOOST_LOG_TRIVIAL(debug) << "Input stream has reached the end of file"; + return -1; + } + if (stream.fail()) + { + BOOST_LOG_TRIVIAL(debug) << "Failed to read input stream"; + return -1; + } - if (stream.eof()) - { - // BOOST_LOG_TRIVIAL(error) << "InputStream has end of file "; - } + long int streamPos = stream.tellg(); + + if (streamPos < 0) + { + BOOST_LOG_TRIVIAL(error) << "Error telling in stream"; return -1; } + + return streamPos; } diff --git a/src/cpp/common/streamRinex.hpp b/src/cpp/common/streamRinex.hpp index dffb75f29..29721c480 100644 --- a/src/cpp/common/streamRinex.hpp +++ b/src/cpp/common/streamRinex.hpp @@ -41,6 +41,13 @@ struct RinexParser : Parser, ObsLister if (tempObsList.size() > 0) { obsListList.push_back(std::move(tempObsList)); + + BOOST_LOG_TRIVIAL(debug) << "Parsed " << tempObsList.size() + << " obs, obsTime=" << tempObsList.front()->time.to_string(6); + } + else + { + BOOST_LOG_TRIVIAL(debug) << "No obs parsed"; } } diff --git a/src/cpp/common/streamRtcm.hpp b/src/cpp/common/streamRtcm.hpp index df291e8e8..3a78932d5 100644 --- a/src/cpp/common/streamRtcm.hpp +++ b/src/cpp/common/streamRtcm.hpp @@ -7,13 +7,27 @@ #include "common/streamParser.hpp" #include "other_ssr/otherSSR.hpp" -#define CLEAN_UP_AND_RETURN_ON_FAILURE \ - \ - if (inputStream.fail()) \ - { \ - inputStream.clear(); \ - inputStream.seekg(pos); \ - return; \ +#define CLEAN_UP_AND_RETURN_ON_FAILURE \ + \ + { \ + if (inputStream.bad()) \ + { \ + /* Unrecoverable */ \ + return; \ + } \ + if (inputStream.eof()) \ + { \ + /* End of file, keep eofbit and reset other bits of error state */ \ + inputStream.clear(inputStream.rdstate() & ~std::ios::failbit); \ + return; \ + } \ + if (inputStream.fail()) \ + { \ + /* Logical read failure, clear error state to allow future reads */ \ + inputStream.clear(); \ + inputStream.seekg(pos); /* Eugene: seekg(pos) may be harmful? */ \ + return; \ + } \ } struct RtcmParser : Parser, RtcmDecoder @@ -26,7 +40,10 @@ struct RtcmParser : Parser, RtcmDecoder { // std::cout << "Parsing rtcm" << "\n"; - if (qzssL6) // todo aaron move to own decoder type + // Eugene: Should clear here? + // inputStream.clear(); + + if (qzssL6) // todo? move to own decoder type { int pos; @@ -42,7 +59,7 @@ struct RtcmParser : Parser, RtcmDecoder if (mess_state == 0) pos = inputStream.tellg(); - if (mess_state < 4) // todo aaron, change to fifo for preamble + if (mess_state < 4) // todo? change to fifo for preamble { unsigned char c; inputStream.read((char*)&c, 1); @@ -88,7 +105,7 @@ struct RtcmParser : Parser, RtcmDecoder int i = 0; int messType = getbituInc(buf, i, 12); - if (messType == 4073) // todo aaron enum + if (messType == 4073) // todo? enum { frameBits = decodecompactSSR(qzsL6buff, rtcmTime()); } @@ -105,7 +122,7 @@ struct RtcmParser : Parser, RtcmDecoder frameBits++; int j = frameBits; messType = getbituInc(buf, j, 12); - if (messType == 4073) // todo aaron enum + if (messType == 4073) // todo? enum { int subtype = getbituInc(buf, j, 4); if (subtype != 6 && subtype != 12) @@ -248,6 +265,8 @@ struct RtcmParser : Parser, RtcmDecoder auto rtcmReturnType = decode(message); + BOOST_LOG_TRIVIAL(debug) << "rtcmReturnType=" << enum_to_string(rtcmReturnType); + if (rtcmReturnType == E_ReturnType::GOT_OBS) { return; diff --git a/src/cpp/common/streamUbx.cpp b/src/cpp/common/streamUbx.cpp index 2042eca8e..d13b63eb7 100644 --- a/src/cpp/common/streamUbx.cpp +++ b/src/cpp/common/streamUbx.cpp @@ -63,7 +63,7 @@ void UbxParser::parse(std::istream& inputStream) inputStream.read((char*)&crcRead, 2); CLEAN_UP_AND_RETURN_ON_FAILURE; - // todo aaron calculate crcRead + // todo? calculate crcRead if (0) { checksumFailure(); diff --git a/src/cpp/common/tcpSocket.cpp b/src/cpp/common/tcpSocket.cpp index 56942b42d..d65ed10d2 100644 --- a/src/cpp/common/tcpSocket.cpp +++ b/src/cpp/common/tcpSocket.cpp @@ -3,6 +3,7 @@ // #define BSONCXX_POLY_USE_SYSTEM_MNMLSTC #include "common/tcpSocket.hpp" +#include #include #include #include @@ -31,8 +32,7 @@ void TcpSocket::logChunkError() std::cout << message.str() << "\n"; messageChunkLog(message.str()); - // todo aaron - // std::ofstream outStream(rtcmTraceFilename, std::ios::app); + // todo? // std::ofstream outStream(rtcmTraceFilename, std::ios::app); // if (!outStream) // { // std::cout << "Error opening " << rtcmTraceFilename << " in " << __FUNCTION__ << @@ -294,9 +294,13 @@ void TcpSocket::delayedReconnect() // Delay and attempt reconnect, this prevents server abuse. timer.expires_from_now(boost::posix_time::seconds((int)reconnectDelay)); + nextReconnectAttemptUnixTime.store( + std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()) + + (long long)reconnectDelay + ); // wait a little longer next time; - reconnectDelay *= 2; + reconnectDelay = std::min(reconnectDelay * 2, maxReconnectDelay); timer.async_wait(boost::bind(&TcpSocket::reconnectTimerHandler, this, bp::error)); } @@ -375,6 +379,7 @@ void NtripResponder::requestResponseHandler(const boost::system::error_code& err // conneccted, turn the delay back down. reconnectDelay = 1; + nextReconnectAttemptUnixTime.store(0); isConnected = true; @@ -532,6 +537,7 @@ void TcpSocket::connect() _resolver = std::make_shared(ioContext); BOOST_LOG_TRIVIAL(debug) << "(Re)connecting " << url.sanitised(); + nextReconnectAttemptUnixTime.store(0); // The socket_ptr reduces some code, although the async_read and async_right // must be called using _sslsocket in order to work correctly. @@ -570,6 +576,33 @@ void TcpSocket::disconnect() downloadBuf.consume(downloadBuf.size()); } +void TcpSocket::shutdown() +{ + nextReconnectAttemptUnixTime.store(0); + isConnected = false; + + try + { + timer.cancel(); + } + catch (...) + { + } + + try + { + if (_resolver) + { + _resolver->cancel(); + } + } + catch (...) + { + } + + disconnect(); +} + void TcpSocket::connectionError(const boost::system::error_code& err, string operation) { if (acsConfig.output_ntrip_log == false) diff --git a/src/cpp/common/tcpSocket.hpp b/src/cpp/common/tcpSocket.hpp index 0bc97e3c8..1227b1f27 100644 --- a/src/cpp/common/tcpSocket.hpp +++ b/src/cpp/common/tcpSocket.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -248,9 +250,11 @@ struct TcpSocket : NetworkStatistics, SerialStream public: URL url; - double reconnectDelay = 1; - int disconnectionCount = 0; - bool isConnected = false; + double reconnectDelay = 1; + double maxReconnectDelay = 900; + int disconnectionCount = 0; + bool isConnected = false; + std::atomic nextReconnectAttemptUnixTime = 0; int numberErroredChunks = 0; bool logHttpSentReceived = false; @@ -272,6 +276,7 @@ struct TcpSocket : NetworkStatistics, SerialStream void connect(); void disconnect(); + void shutdown(); void startRead(bool chunked); @@ -309,6 +314,14 @@ struct TcpSocket : NetworkStatistics, SerialStream void dataChunkDownloaded(vector& dataChunk); + bool reconnectDueAfter(const std::chrono::system_clock::time_point& timePoint) const + { + long long nextReconnectUnixTime = nextReconnectAttemptUnixTime.load(); + return nextReconnectUnixTime > std::chrono::system_clock::to_time_t(timePoint); + } + + long long nextReconnectUnixTime() const { return nextReconnectAttemptUnixTime.load(); } + // content from a one-shot request has been received virtual void readContentDownloaded(vector content) {} diff --git a/src/cpp/common/tides.cpp b/src/cpp/common/tides.cpp index 0a7f1fe10..e0ac2a056 100644 --- a/src/cpp/common/tides.cpp +++ b/src/cpp/common/tides.cpp @@ -648,8 +648,8 @@ VectorEnu tideOceanLoadHardisp( for (auto [wave, disp] : otlDisplacement) { tamp[0][i] = otlDisplacement[wave].amplitude.u(); - tph[0][i] = -otlDisplacement[wave].phase.u( - ); // HARDISP.F: Change sign for phase, to be negative for lags + tph[0][i] = -otlDisplacement[wave] + .phase.u(); // HARDISP.F: Change sign for phase, to be negative for lags tamp[1][i] = otlDisplacement[wave].amplitude.e(); tph[1][i] = -otlDisplacement[wave].phase.e(); tamp[2][i] = otlDisplacement[wave].amplitude.n(); diff --git a/src/cpp/common/trace.cpp b/src/cpp/common/trace.cpp index ba2347305..877309bf9 100644 --- a/src/cpp/common/trace.cpp +++ b/src/cpp/common/trace.cpp @@ -22,7 +22,9 @@ using std::unordered_map; -boost::iostreams::stream nullStream((boost::iostreams::null_sink())); +thread_local boost::iostreams::stream nullStream{ + boost::iostreams::null_sink{} +}; /** Semi-formatted text-based outputs. * Trace files are the best record of the processing that occurs within the Pea. diff --git a/src/cpp/common/trace.hpp b/src/cpp/common/trace.hpp index 4016d6e91..914c09dc7 100644 --- a/src/cpp/common/trace.hpp +++ b/src/cpp/common/trace.hpp @@ -9,7 +9,11 @@ #include #include #include +#include +#include #include +#include +#include #include #include "common/eigenIncluder.hpp" @@ -21,7 +25,7 @@ using Trace = std::ostream; struct GTime; -extern boost::iostreams::stream nullStream; +extern thread_local boost::iostreams::stream nullStream; struct ConsoleLog : public sinks::basic_formatted_sink_backend { @@ -65,28 +69,165 @@ void traceTrivialTrace_(string const& fmt, Args&&... args) BOOST_LOG_TRIVIAL(trace) << boost::str(f); } -template -std::ofstream getTraceFile(T& thing, bool json = false) +struct PooledTraceFile { - string traceFilename; - if (json) - traceFilename = thing.jsonTraceFilename; - else - traceFilename = thing.traceFilename; + Trace* trace = &nullStream; + std::shared_ptr file; + + PooledTraceFile() = default; + explicit PooledTraceFile(std::shared_ptr fileStream) + : file{std::move(fileStream)} + { + if (file && *file) + { + trace = file.get(); + } + } + + operator Trace&() { return *trace; } + operator const Trace&() const { return *trace; } + + template + PooledTraceFile& operator<<(T&& value) + { + (*trace) << std::forward(value); + return *this; + } + + PooledTraceFile& operator<<(std::ostream& (*manip)(std::ostream&)) + { + (*trace) << manip; + return *this; + } + + PooledTraceFile& operator<<(std::ios& (*manip)(std::ios&)) + { + (*trace) << manip; + return *this; + } + PooledTraceFile& operator<<(std::ios_base& (*manip)(std::ios_base&)) + { + (*trace) << manip; + return *this; + } + + void flush() { trace->flush(); } +}; + +inline std::unordered_map>& traceFileCache() +{ + thread_local std::unordered_map> cache; + return cache; +} + +inline std::mutex& retainedTraceFilesMutex() +{ + static std::mutex mutex; + return mutex; +} + +inline std::unordered_set& retainedTraceFileNames() +{ + static std::unordered_set activeFilenames; + return activeFilenames; +} + +inline size_t& retainedTraceFilesGeneration() +{ + static size_t generation = 0; + return generation; +} + +inline void pruneTraceFileCache(const std::unordered_set& activeFilenames) +{ + auto& cache = traceFileCache(); + for (auto it = cache.begin(); it != cache.end();) + { + if (activeFilenames.find(it->first) != activeFilenames.end()) + { + it++; + continue; + } + + if (it->second) + { + it->second->flush(); + } + it = cache.erase(it); + } +} + +inline void pruneTraceFileCacheIfNeeded() +{ + thread_local size_t localGeneration = 0; + + std::unordered_set activeFilenames; + + { + std::lock_guard lock(retainedTraceFilesMutex()); + if (localGeneration == retainedTraceFilesGeneration()) + { + return; + } + + localGeneration = retainedTraceFilesGeneration(); + activeFilenames = retainedTraceFileNames(); + } + + pruneTraceFileCache(activeFilenames); +} + +inline void retainTraceFiles(const std::unordered_set& activeFilenames) +{ + { + std::lock_guard lock(retainedTraceFilesMutex()); + retainedTraceFileNames() = activeFilenames; + retainedTraceFilesGeneration()++; + } + + pruneTraceFileCache(activeFilenames); +} + +inline PooledTraceFile getTraceFile(const string& traceFilename, const string& id) +{ if (traceFilename.empty()) { - return std::ofstream(); + return PooledTraceFile(); + } + + pruneTraceFileCacheIfNeeded(); + + auto& cache = traceFileCache(); + + auto it = cache.find(traceFilename); + if (it != cache.end()) + { + return PooledTraceFile(it->second); } - std::ofstream trace(traceFilename, std::ios::app); - if (!trace) + auto traceFile = std::make_shared(traceFilename, std::ios::app); + if (!*traceFile) { - BOOST_LOG_TRIVIAL(error) << "Could not open trace file for " << thing.id << " at " + BOOST_LOG_TRIVIAL(error) << "Could not open trace file for " << id << " at " << traceFilename; + return PooledTraceFile(); } - return trace; + cache.emplace(traceFilename, traceFile); + return PooledTraceFile(std::move(traceFile)); +} + +template +PooledTraceFile getTraceFile(T& thing, bool json = false) +{ + string traceFilename; + if (json) + traceFilename = thing.jsonTraceFilename; + else + traceFilename = thing.traceFilename; + + return getTraceFile(traceFilename, thing.id); } void printHex(Trace& trace, vector& chunk); @@ -97,11 +238,15 @@ struct Block { Trace& trace; string blockName; + string separator; - Block(Trace& trace, string blockName) : trace{trace}, blockName{blockName} + Block(Trace& trace, string blockName, string separator = "") + : trace{trace}, blockName{blockName}, separator{separator} { - trace << "\n" - << "+" << blockName << "\n"; + trace << "\n"; + if (separator.empty() == false) + trace << separator << "\n"; + trace << "+" << blockName << "\n"; } ~Block() { trace << "-" << blockName << "\n"; } diff --git a/src/cpp/common/tropSinex.cpp b/src/cpp/common/tropSinex.cpp index 50f493538..cfb8360fe 100644 --- a/src/cpp/common/tropSinex.cpp +++ b/src/cpp/common/tropSinex.cpp @@ -203,7 +203,7 @@ void writeTropSiteId( if (result.failureSiteId) continue; // Receiver not found in sinex file if (result.failureEstimate) - continue; // Position not found in sinex file //todo aaron, remove this, use + continue; // Position not found in sinex file //todo? remove this, use // other function VectorEnu& antdel = stationSinex.ecc_ptr->ecc; @@ -632,10 +632,8 @@ void setTropSolFromFilter(KFState& kfState) ///< KF state kfState.getKFValue(key, x, &var); double oldVar = tropSumMap[id][typeWet].var; - double newVar = - var + - oldVar; // Ref: - // https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulae + double newVar = var + oldVar; // Ref: + // https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulae // Add on filter estimates if (key.type == KF::TROP) @@ -694,7 +692,8 @@ void setTropSolFromFilter(KFState& kfState) ///< KF state entry.x -= modelledZhd; } - stationEntry.solutions.push_back({type, entry.x, units, 8} + stationEntry.solutions.push_back( + {type, entry.x, units, 8} ); // type, value, units (multiplier), printing width stationEntry.solutions.push_back({"STDDEV", sqrt(entry.var), 1e3, 8}); } diff --git a/src/cpp/common/ubxDecoder.cpp b/src/cpp/common/ubxDecoder.cpp index 2ab8d54c8..dab259add 100644 --- a/src/cpp/common/ubxDecoder.cpp +++ b/src/cpp/common/ubxDecoder.cpp @@ -52,6 +52,9 @@ void UbxDecoder::decodeRAWX(vector& payload) // std::cout << "\n" << "Recieved RAWX message has " << numMeas << " measurements" << "\n"; + lastTimeTag = 0; + lastTime = gpst2time(week, rcvTow); + map obsMap; for (int i = 0; i < numMeas; i++) @@ -85,7 +88,7 @@ void UbxDecoder::decodeRAWX(vector& payload) SatSys Sat(sys, satId); auto& obs = obsMap[Sat]; obs.Sat = Sat; - obs.time = gpst2time(week, rcvTow); + obs.time = lastTime; printf( "meas %s %s %s %14.3lf %14.3lf\n", @@ -106,10 +109,15 @@ void UbxDecoder::decodeRAWX(vector& payload) obsList.push_back((shared_ptr)obs); } - obsListList.push_back(obsList); - - lastTimeTag = 0; - lastTime = gpst2time(week, rcvTow); + if (obsList.empty() == false) + { + obsListList.push_back(obsList); + } + else + { + BOOST_LOG_TRIVIAL(info) << "UBX decoder produced empty ObsList at epoch flush" + << ", week=" << week << ", rcvTow=" << rcvTow; + } } void UbxDecoder::decodeMEAS(vector& payload) @@ -309,10 +317,10 @@ void UbxDecoder::decodeSFRBX(vector& payload) // printf("\n preamble : %02x - subFrameId : %02x - ", preamble, subFrameId); - if (subFrameId <= 0 && subFrameId >= 4) - { - return; - } + // if (subFrameId <= 0 && subFrameId >= 4) + // { + // return; + // } // vector subFrame; // int byteBits = 0; diff --git a/src/cpp/configurator/htmlFooterTemplate.hpp b/src/cpp/configurator/htmlFooterTemplate.hpp index f2bc80680..da825b240 100644 --- a/src/cpp/configurator/htmlFooterTemplate.hpp +++ b/src/cpp/configurator/htmlFooterTemplate.hpp @@ -1,11 +1,17 @@ #pragma once // clang-format off static const char* htmlFooterTemplate = R"HTMLTEMPLATE( -

- -
- -
+

+
+ +
+
+ + +
+
+ + + + + -

Ginan YAML Inspector

+
+

Ginan YAML Inspector

+
+

Use the checkboxes to enable editing and modification of options.

Existing yaml files and their configuration can be loaded by importing them below.

diff --git a/src/cpp/inertial/posProp.cpp b/src/cpp/inertial/posProp.cpp index 72a8a3d14..d96585dec 100644 --- a/src/cpp/inertial/posProp.cpp +++ b/src/cpp/inertial/posProp.cpp @@ -47,7 +47,7 @@ void propLinear( // other.z() = temp; // Vector3d acclEcef = (accl - inertialInit.acclBias)[2] * rRec.normalized(); - // //todo aaron, rotate to ecef + // //todo? rotate to ecef } Vector3d accCF = accelCentralForce(r, GM_values[E_ThirdBody::EARTH]); @@ -78,7 +78,7 @@ void propLinear( Vector3d vPlus = v + a * dt; ; - Quaterniond qPlus = Q * qBody; // todo aaron, check ordering of this + Quaterniond qPlus = Q * qBody; // todo? check ordering of this r = rPlus; v = vPlus; diff --git a/src/cpp/iono/ionex.cpp b/src/cpp/iono/ionex.cpp index f2c6f09e3..7611eab1d 100644 --- a/src/cpp/iono/ionex.cpp +++ b/src/cpp/iono/ionex.cpp @@ -38,7 +38,7 @@ int nitem(const double* range) /* data index (i:lat,j:lon,k:hgt) */ int dataindex(int i, int j, int k, - const int* ndata) // todo aaron, convert to maps + const int* ndata) // todo? convert to maps { if (i < 0 || ndata[0] <= i || j < 0 || ndata[1] <= j || k < 0 || ndata[2] <= k) { @@ -285,8 +285,9 @@ double readionexh( { nexp = str2num(buff, 0, 6); } - else if (strstr(label, "START OF AUX DATA") == label && - strstr(buff, "DIFFERENTIAL CODE BIASES")) + else if ( + strstr(label, "START OF AUX DATA") == label && strstr(buff, "DIFFERENTIAL CODE BIASES") + ) { readionexdcb(in, navi); } diff --git a/src/cpp/iono/ionoLocalSTEC.cpp b/src/cpp/iono/ionoLocalSTEC.cpp index 48f666d0b..95b1f87d5 100644 --- a/src/cpp/iono/ionoLocalSTEC.cpp +++ b/src/cpp/iono/ionoLocalSTEC.cpp @@ -265,8 +265,7 @@ double ionCoefLocal( ); return (1 - dlatDeg / atmReg.intLatDeg) * - (1 - dlonDeg / atmReg.intLonDeg - ); // todo aaron use bilinear interpolation function? + (1 - dlonDeg / atmReg.intLonDeg); // todo? use bilinear interpolation function? } default: { diff --git a/src/cpp/iono/ionoMeas.cpp b/src/cpp/iono/ionoMeas.cpp index 7312d1d9b..2e3fa2bbb 100644 --- a/src/cpp/iono/ionoMeas.cpp +++ b/src/cpp/iono/ionoMeas.cpp @@ -180,7 +180,7 @@ void obsIonoData(Trace& trace, Receiver& rec) satStat.gf_amb += SmtG * (amb - satStat.gf_amb); satStat.ambvar = SmtG * (varP); } - obs.stecType = 1; // todo aaron magic numbers + obs.stecType = 1; // todo? magic numbers obs.stecVal = (satStat.gf_amb + lc.GF_Phas_m) / obs.stecToDelay; obs.stecVar = ((satStat.ambvar + 2 * varL) + SQR(PHASE_BIAS_STD)) / SQR(obs.stecToDelay); diff --git a/src/cpp/iono/ionoModel.cpp b/src/cpp/iono/ionoModel.cpp index 66ac3a38a..06ba12cdf 100644 --- a/src/cpp/iono/ionoModel.cpp +++ b/src/cpp/iono/ionoModel.cpp @@ -281,8 +281,8 @@ void filterIonosphere( meas.addDsgnEntry(recDCBKey, 1, init); /************ satellite DCB ************/ /* We may need to change this for multi-code - solutions */ - if (acsConfig.ionModelOpts.estimate_sat_dcb /// todo aaron, ew.. + solutions */ + if (acsConfig.ionModelOpts.estimate_sat_dcb /// todo? ew.. || mainObsCombo[sys] != obs.stecCodeCombo) { InitialState init = initialStateFromConfig(satOpts.code_bias); diff --git a/src/cpp/iono/ionoModel.hpp b/src/cpp/iono/ionoModel.hpp index 696c92e84..1b86f5541 100644 --- a/src/cpp/iono/ionoModel.hpp +++ b/src/cpp/iono/ionoModel.hpp @@ -64,14 +64,14 @@ void ionOutputLocal(Trace& trace, KFState& kfState); double getSSRIono(Trace& trace, GTime time, Vector3d& rRec, AzEl& azel, double& var, SatSys& Sat); bool getIGSSSRIono( - Trace& trace, - GTime time, - SSRAtm& ssrAtm, - Vector3d& rRec, - AzEl& azel, - double& iono, - double& var - ); + Trace& trace, + GTime time, + SSRAtm& ssrAtm, + Vector3d& rRec, + AzEl& azel, + double& iono, + double& var +); bool getCmpSSRIono( Trace& trace, GTime time, diff --git a/src/cpp/iono/ionoSBAS.cpp b/src/cpp/iono/ionoSBAS.cpp index 070a5eb9f..33b2145ea 100644 --- a/src/cpp/iono/ionoSBAS.cpp +++ b/src/cpp/iono/ionoSBAS.cpp @@ -12,102 +12,106 @@ using std::map; #define IONO_DEBUG_TRACE_LEVEL 4 map> latiVects = { - {0, { -55, -50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}}, - {1, { -75, -65, -55, -50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 65, 75}}, - {2, {-85, -75, -65, -55, -50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 65, 75}}, - {3, { -75, -65, -55, -50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 65, 75, 85}} + {0, {-55, -50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 0, + 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}}, + {1, {-75, -65, -55, -50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 0, + 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 65, 75}}, + {2, {-85, -75, -65, -55, -50, -45, -40, -35, -30, -25, -20, -15, -10, -5, + 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 65, 75}}, + {3, {-75, -65, -55, -50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 0, + 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 65, 75, 85}} }; map>> Iono_Bands = { {0, - {{0, { 1, 28, -180, 3}}, - {1, { 29, 51, -175, 0}}, - {2, { 52, 78, -170, 1}}, - {3, { 79, 101, -165, 0}}, + {{0, {1, 28, -180, 3}}, + {1, {29, 51, -175, 0}}, + {2, {52, 78, -170, 1}}, + {3, {79, 101, -165, 0}}, {4, {102, 128, -160, 1}}, {5, {129, 151, -155, 0}}, {6, {152, 178, -150, 1}}, {7, {179, 201, -145, 0}}}}, {1, - {{0, { 1, 28, -140, 2}}, - {1, { 29, 51, -135, 0}}, - {2, { 52, 78, -130, 1}}, - {3, { 79, 101, -125, 0}}, + {{0, {1, 28, -140, 2}}, + {1, {29, 51, -135, 0}}, + {2, {52, 78, -130, 1}}, + {3, {79, 101, -125, 0}}, {4, {102, 128, -120, 1}}, {5, {129, 151, -115, 0}}, {6, {152, 178, -110, 1}}, {7, {179, 201, -105, 0}}}}, {2, - {{0, { 1, 27, -100, 1}}, - {1, { 28, 50, -95, 0}}, - {2, { 51, 78, -90, 3}}, - {3, { 79, 101, -85, 0}}, - {4, {102, 128, -80, 1}}, - {5, {129, 151, -75, 0}}, - {6, {152, 178, -70, 1}}, - {7, {179, 201, -65, 0}}}}, + {{0, {1, 27, -100, 1}}, + {1, {28, 50, -95, 0}}, + {2, {51, 78, -90, 3}}, + {3, {79, 101, -85, 0}}, + {4, {102, 128, -80, 1}}, + {5, {129, 151, -75, 0}}, + {6, {152, 178, -70, 1}}, + {7, {179, 201, -65, 0}}}}, {3, - {{0, { 1, 27, -60, 1}}, - {1, { 28, 50, -55, 0}}, - {2, { 51, 78, -50, 2}}, - {3, { 79, 101, -45, 0}}, + {{0, {1, 27, -60, 1}}, + {1, {28, 50, -55, 0}}, + {2, {51, 78, -50, 2}}, + {3, {79, 101, -45, 0}}, {4, {102, 128, -40, 1}}, {5, {129, 151, -35, 0}}, {6, {152, 178, -30, 1}}, {7, {179, 201, -25, 0}}}}, {4, - {{0, { 1, 27, -20, 1}}, - {1, { 28, 50, -15, 0}}, - {2, { 51, 77, -10, 1}}, - {3, { 78, 100, -5, 0}}, - {4, {101, 128, 0, 3}}, - {5, {129, 151, 5, 0}}, - {6, {152, 178, 10, 1}}, - {7, {179, 201, 15, 0}}}}, + {{0, {1, 27, -20, 1}}, + {1, {28, 50, -15, 0}}, + {2, {51, 77, -10, 1}}, + {3, {78, 100, -5, 0}}, + {4, {101, 128, 0, 3}}, + {5, {129, 151, 5, 0}}, + {6, {152, 178, 10, 1}}, + {7, {179, 201, 15, 0}}}}, {5, - {{0, { 1, 27, 20, 1}}, - {1, { 28, 50, 25, 0}}, - {2, { 51, 77, 30, 1}}, - {3, { 78, 100, 35, 0}}, + {{0, {1, 27, 20, 1}}, + {1, {28, 50, 25, 0}}, + {2, {51, 77, 30, 1}}, + {3, {78, 100, 35, 0}}, {4, {101, 128, 40, 2}}, {5, {129, 151, 45, 0}}, {6, {152, 178, 50, 1}}, {7, {179, 201, 55, 0}}}}, {6, - {{0, { 1, 27, 60, 1}}, - {1, { 28, 50, 65, 0}}, - {2, { 51, 77, 70, 1}}, - {3, { 78, 100, 75, 0}}, + {{0, {1, 27, 60, 1}}, + {1, {28, 50, 65, 0}}, + {2, {51, 77, 70, 1}}, + {3, {78, 100, 75, 0}}, {4, {101, 127, 80, 1}}, {5, {128, 150, 85, 0}}, {6, {151, 178, 90, 3}}, {7, {179, 201, 95, 0}}}}, {7, - {{0, { 1, 27, 100, 1}}, - {1, { 28, 50, 105, 0}}, - {2, { 51, 77, 110, 1}}, - {3, { 78, 100, 115, 0}}, + {{0, {1, 27, 100, 1}}, + {1, {28, 50, 105, 0}}, + {2, {51, 77, 110, 1}}, + {3, {78, 100, 115, 0}}, {4, {101, 127, 120, 1}}, {5, {128, 150, 125, 0}}, {6, {151, 178, 130, 2}}, {7, {179, 201, 135, 0}}}}, {8, - {{0, { 1, 27, 140, 1}}, - {1, { 28, 50, 145, 0}}, - {2, { 51, 77, 150, 1}}, - {3, { 78, 100, 155, 0}}, + {{0, {1, 27, 140, 1}}, + {1, {28, 50, 145, 0}}, + {2, {51, 77, 150, 1}}, + {3, {78, 100, 155, 0}}, {4, {101, 127, 160, 1}}, {5, {128, 150, 165, 0}}, {6, {151, 177, 170, 1}}, {7, {178, 200, 175, 0}}}}, {9, - {{0, { 1, 72, 60, -180, 5, 170}}, - {1, { 73, 108, 65, -180, 10, 170}}, - {2, {143, 144, 70, -180, 10, 170}}, - {3, {179, 180, 75, -180, 10, 170}}, - {4, {181, 192, 85, -180, 30, 150}}}}, + {{0, {1, 72, 60, -180, 5, 170}}, + {1, {73, 108, 65, -180, 10, 170}}, + {2, {143, 144, 70, -180, 10, 170}}, + {3, {179, 180, 75, -180, 10, 170}}, + {4, {181, 192, 85, -180, 30, 150}}}}, {10, - {{0, { 1, 72, -60, -180, 5, 170}}, - {1, { 73, 108, -65, -180, 10, 170}}, + {{0, {1, 72, -60, -180, 5, 170}}, + {1, {73, 108, -65, -180, 10, 170}}, {2, {143, 144, -70, -180, 10, 170}}, {3, {179, 180, -75, -180, 10, 170}}, {4, {181, 192, -85, -170, 30, 160}}}} diff --git a/src/cpp/iono/ionoSphericalCaps.cpp b/src/cpp/iono/ionoSphericalCaps.cpp index 61a02d668..db3b426fd 100644 --- a/src/cpp/iono/ionoSphericalCaps.cpp +++ b/src/cpp/iono/ionoSphericalCaps.cpp @@ -45,7 +45,7 @@ double legendre_function(int m, double n, double x) double e2 = (1 + 3 / pow(p, 2) + 4 / pow(p, 3)) / (360 * pow(m, 3)); Kmn = pow(2, -m) * pow((n + m) / (n - m), (n + 2) / 4) * pow(p, m / 2.0) * exp(e1 + e2) / sqrt(m * PI); - A = Kmn * pow(sin(x), m); + A = Kmn * pow(sin(x), m); } P = A; @@ -87,7 +87,7 @@ double legendre_derivatv(int m, double n, double x) e2 = (1 + 3 / pow(p, 2) + 4 / pow(p, 3)) / (360 * pow(m, 3)); Kmn = pow(2, -m) * pow((n + m) / (n - m), (n + 2) / 4) * pow(p, m / 2.0) * exp(e1 + e2) / sqrt(m * PI); - A = Kmn * pow(sin(x), m); + A = Kmn * pow(sin(x), m); } P = A; @@ -227,7 +227,7 @@ double ionCoefSphcap(Trace& trace, int ind, IonoObs& obs, bool slant) double out; if (basis.parity) - out = legr * sin(basis.order * obs.ippMap[basis.ind].lonDeg * D2R); // todo aaron use enum + out = legr * sin(basis.order * obs.ippMap[basis.ind].lonDeg * D2R); // todo? use enum else out = legr * cos(basis.order * obs.ippMap[basis.ind].lonDeg * D2R); diff --git a/src/cpp/loading/interpolate_loading.cpp b/src/cpp/loading/interpolate_loading.cpp index 9e568e271..74d08b8ba 100644 --- a/src/cpp/loading/interpolate_loading.cpp +++ b/src/cpp/loading/interpolate_loading.cpp @@ -28,7 +28,9 @@ #include "loading/loading.h" #include "loading/tide.h" #include "loading/utils.h" +#if defined(ENABLE_PARALLELISATION) || defined(_OPENMP) #include "omp.h" +#endif namespace po = boost::program_options; @@ -45,18 +47,7 @@ void program_options(int argc, char* argv[], otl_input& input) po::options_description desc{"interpolate_loading "}; // Do not set default values here, as this will overide the configuration file opitions!!! - desc.add_options() - ("help", "This help message") - ("quiet", "Less output") - ("verbose", "More output") - ("type", po::value(), "loading type: (o) ocean loading, or (a) atmospheric loading") - ("grid", po::value(), "Loading grid netCDF file") - ("location", po::value>()->multitoken(), "location: lon (decimal degrees) lat (decimal degrees)") - ("xyz", po::bool_switch()->default_value(false), "set if the coordinates are in XYZ format") - ("code", po::value(), "Station Code with or without DOMES number (ALIC 50137M0014)") - ("input", po::value(), "input file containing list of stations CSV format name, lon, lat") - ("output", po::value(), "Output BLQ file") - ; + desc.add_options()("help", "This help message")("quiet", "Less output")("verbose", "More output")("type", po::value(), "loading type: (o) ocean loading, or (a) atmospheric loading")("grid", po::value(), "Loading grid netCDF file")("location", po::value>()->multitoken(), "location: lon (decimal degrees) lat (decimal degrees)")("xyz", po::bool_switch()->default_value(false), "set if the coordinates are in XYZ format")("code", po::value(), "Station Code with or without DOMES number (ALIC 50137M0014)")("input", po::value(), "input file containing list of stations CSV format name, lon, lat")("output", po::value(), "Output BLQ file"); po::variables_map vm; // This is to be able to parse negative numbers with boost. @@ -225,8 +216,8 @@ void program_options(int argc, char* argv[], otl_input& input) ecef[1] = input.xyz_coords[i][1]; ecef[2] = input.xyz_coords[i][2]; ecef2pos(ecef, tmp); - input.lon.push_back(tmp[1] * 180.0 / M_PI); - input.lat.push_back(tmp[0] * 180.0 / M_PI); + input.lon.push_back(tmp[1] * 180.0 / PI); + input.lat.push_back(tmp[0] * 180.0 / PI); } } }; @@ -271,7 +262,14 @@ int main(int argc, char* argv[]) input.out_disp.data() + input.out_disp.num_elements(), std::complex(0, 0) ); - + // Normalize longitudes before parallel processing + for (auto& lon : input.lon) + { + if (lon < 0) + { + lon += 360.0; + } + } for (int i_sta = 0; i_sta < input.lat.size(); i_sta++) for (int i_wave = 0; i_wave < tideinfo.get_nwave(); i_wave++) for (int i_dir = 0; i_dir < 3; i_dir++) diff --git a/src/cpp/loading/make_otl_blq.cpp b/src/cpp/loading/make_otl_blq.cpp index 7ea1dd2e3..c4933d05b 100644 --- a/src/cpp/loading/make_otl_blq.cpp +++ b/src/cpp/loading/make_otl_blq.cpp @@ -26,7 +26,9 @@ #include "loading/loading.h" #include "loading/tide.h" #include "loading/utils.h" +#if defined(ENABLE_PARALLELISATION) || defined(_OPENMP) #include "omp.h" +#endif namespace po = boost::program_options; @@ -252,8 +254,8 @@ void program_options(int argc, char* argv[], otl_input& input) ecef[1] = input.xyz_coords[i][1]; ecef[2] = input.xyz_coords[i][2]; ecef2pos(ecef, tmp); - input.lon.push_back(tmp[1] * 180.0 / M_PI); - input.lat.push_back(tmp[0] * 180.0 / M_PI); + input.lon.push_back(tmp[1] * 180.0 / PI); + input.lat.push_back(tmp[0] * 180.0 / PI); } } }; @@ -341,11 +343,7 @@ int main(int argc, char* argv[]) #pragma omp parallel for for (unsigned int i_poi = 0; i_poi < input.lat.size(); i_poi++) { - // BOOST_LOG_TRIVIAL(info) << " Processing coordinates # " << i_poi << " \n\t" << - // timer.format() ; load_1_point(tideinfo, &input, load, i_poi); - // BOOST_LOG_TRIVIAL(info) << " end pt " << i_poi << " \n\t" << - // timer.format() ; } write_BLQ(&input); diff --git a/src/cpp/loading/tide.cpp b/src/cpp/loading/tide.cpp index dae09afbf..41831379d 100644 --- a/src/cpp/loading/tide.cpp +++ b/src/cpp/loading/tide.cpp @@ -9,6 +9,7 @@ #include #include #include +#include "loading/utils.h" using namespace std; using namespace netCDF; @@ -34,10 +35,10 @@ void tide::set_name(std::string name) void tide::fill_ReIm() { - double scale = (double)1030. / 100.0 * (double)6371e3 * (double)6371e3 * (0.0625 * M_PI / 180) * - (0.0625 * M_PI / 180); - auto phase_ptr = phase.origin(); - auto& ma_shape = + double scale = (double)1030. / 100.0 * (double)6371e3 * (double)6371e3 * (0.0625 * PI / 180) * + (0.0625 * PI / 180); + auto phase_ptr = phase.origin(); + auto& ma_shape = reinterpret_cast const&>(*amplitude.shape()); in_phase.resize(ma_shape); @@ -55,11 +56,11 @@ void tide::fill_ReIm() { if (*amp_ptr != fillNan) { - *in_phase_ptr = static_cast(*amp_ptr * cos(*phase_ptr * M_PI / 180.0) * scale); - *out_phase_ptr = static_cast(*amp_ptr * sin(*phase_ptr * M_PI / 180.0) * scale); + *in_phase_ptr = static_cast(*amp_ptr * cos(*phase_ptr * PI / 180.0) * scale); + *out_phase_ptr = static_cast(*amp_ptr * sin(*phase_ptr * PI / 180.0) * scale); // cout << Re[ilat][ilon] << "\n"; - *in_phase_ptr *= sin(M_PI / 2 - *lat_ptr * M_PI / 180); - *out_phase_ptr *= sin(M_PI / 2 - *lat_ptr * M_PI / 180); + *in_phase_ptr *= sin(PI / 2 - *lat_ptr * PI / 180); + *out_phase_ptr *= sin(PI / 2 - *lat_ptr * PI / 180); } else { diff --git a/src/cpp/loading/utils.cpp b/src/cpp/loading/utils.cpp index d40092664..c27262d0d 100644 --- a/src/cpp/loading/utils.cpp +++ b/src/cpp/loading/utils.cpp @@ -59,12 +59,12 @@ void calcDistanceBearing( double deltalon = lon2_r - lon1_r; double deltalat = lat2_r - lat1_r; double a = sin(deltalat / 2) * sin(deltalat / 2) + - cos(lat1_r) * cos(lat2_r) * sin(deltalon / 2) * sin(deltalon / 2); - double y = sin(deltalon) * cos(lat2_r); - double x = cos(lat1_r) * sin(lat2_r) - sin(lat1_r) * cos(lat2_r) * cos(deltalon); + cos(lat1_r) * cos(lat2_r) * sin(deltalon / 2) * sin(deltalon / 2); + double y = sin(deltalon) * cos(lat2_r); + double x = cos(lat1_r) * sin(lat2_r) - sin(lat1_r) * cos(lat2_r) * cos(deltalon); *brng = atan2(y, x); *dist = 2 * atan2(sqrt(a), sqrt(1 - a)); if (*dist != *dist) - *dist = M_PI; + *dist = PI; } diff --git a/src/cpp/loading/utils.h b/src/cpp/loading/utils.h index 291ca6e02..917f7cd44 100644 --- a/src/cpp/loading/utils.h +++ b/src/cpp/loading/utils.h @@ -18,11 +18,11 @@ constexpr double PI = 3.1415926535897932384626433832795028841971693993751058209 constexpr double D2R = (PI / 180.0); /* deg to rad */ constexpr double R2D = (180.0 / PI); /* rad to deg */ void calcDistanceBearing( - float* lat1, - float* lon1, - float* lat2, - float* lon2, - double* dist, - double* brng - ); + float* lat1, + float* lon1, + float* lat2, + float* lon2, + double* dist, + double* brng +); void ecef2pos(const double* r, double* pos); \ No newline at end of file diff --git a/src/cpp/orbprop/acceleration.cpp b/src/cpp/orbprop/acceleration.cpp index b09fefc90..b81ca770c 100644 --- a/src/cpp/orbprop/acceleration.cpp +++ b/src/cpp/orbprop/acceleration.cpp @@ -138,7 +138,7 @@ Vector3d accelSPH( const Vector3d r, ///< Vector of the position of the satelite (ECEF) const MatrixXd C, ///< Matrix of the "C" spherical harmonic coefficient const MatrixXd S, ///< Matrix of the "S" spherical harmonic coefficient - const int maxDeg, ///< Maximum degree use for the summation of the harmonics //todo aaron, + const int maxDeg, ///< Maximum degree use for the summation of the harmonics //todo? ///< ///< limit this to max found in file/struct const double GM ///< Value of GM constant of the body in question. ) diff --git a/src/cpp/orbprop/centerMassCorrections.cpp b/src/cpp/orbprop/centerMassCorrections.cpp index d3c44bb77..bc947c678 100644 --- a/src/cpp/orbprop/centerMassCorrections.cpp +++ b/src/cpp/orbprop/centerMassCorrections.cpp @@ -67,11 +67,7 @@ Vector3d CenterMassCorrections::estimate(Array6d& dood) double theta = (dood * doodsonNumbers[wave]).sum(); for (int i = 0; i < 3; i++) { - cmcEstimate(i) += - coeff[i * 2] * cos(theta) + - coeff[i * 2 + 1] * - sin(theta - ); // todo aaron this would be better with 2 arrays or a matrix for cos/si + cmcEstimate(i) += coeff[i * 2] * cos(theta) + coeff[i * 2 + 1] * sin(theta); } } return cmcEstimate; diff --git a/src/cpp/orbprop/coordinates.cpp b/src/cpp/orbprop/coordinates.cpp index d457d53d5..feb57cbd4 100644 --- a/src/cpp/orbprop/coordinates.cpp +++ b/src/cpp/orbprop/coordinates.cpp @@ -173,7 +173,7 @@ VectorPos ecef2pos(const VectorEcef& r) void pos2enu( const VectorPos& pos, double* E -) // todo aaron, convert to return Matrix3d, check orientation +) // todo? convert to return Matrix3d, check orientation { double sinp = sin(pos.lat()); double cosp = cos(pos.lat()); diff --git a/src/cpp/orbprop/iers2010.cpp b/src/cpp/orbprop/iers2010.cpp index d7b9ccedc..b9cb69006 100644 --- a/src/cpp/orbprop/iers2010.cpp +++ b/src/cpp/orbprop/iers2010.cpp @@ -8,7 +8,9 @@ #include #include #include +#if defined(ENABLE_PARALLELISATION) || defined(_OPENMP) #include +#endif #include #include "3rdparty/sofa/src/sofa.h" #include "common/common.hpp" @@ -198,8 +200,7 @@ Array6d IERS2010::doodson(GTime time, double ut1_utc) FundamentalArgs fundArgs(time, ut1_utc); Array6d Doodson; - Doodson(4) = - -1 * fundArgs(5); // todo aaron, change to use named parameters, remove setBeta function + Doodson(4) = -1 * fundArgs(5); // todo? change to use named parameters, remove setBeta function Doodson(1) = fundArgs(3) + fundArgs(5); Doodson(0) = fundArgs(0) - Doodson(1); Doodson(2) = Doodson(1) - fundArgs(4); @@ -572,4 +573,4 @@ void HfOceanEop::compute(Array6d& fundamentalArgs, double& x, double& y, double& lod += thread_lod; } } -} \ No newline at end of file +} diff --git a/src/cpp/orbprop/orbitProp.cpp b/src/cpp/orbprop/orbitProp.cpp index 3317d2e78..ca0d603af 100644 --- a/src/cpp/orbprop/orbitProp.cpp +++ b/src/cpp/orbprop/orbitProp.cpp @@ -15,7 +15,9 @@ #include "common/navigation.hpp" #include "common/sinex.hpp" #include "common/ubxDecoder.hpp" +#if defined(ENABLE_PARALLELISATION) || defined(_OPENMP) #include "omp.h" +#endif #include "orbprop/acceleration.hpp" #include "orbprop/aod.hpp" #include "orbprop/boxwing.hpp" @@ -57,7 +59,7 @@ void OrbitIntegrator::computeCommon(GTime time) FrameSwapper frameSwapper(time, erpv); eci2ecf = frameSwapper.i2t_mat; - deci2ecf = frameSwapper.di2t_mat; // todo aaron, just fs this instead of matrices? + deci2ecf = frameSwapper.di2t_mat; // todo? just fs this instead of matrices? for (auto body : enum_values()) { @@ -1100,62 +1102,72 @@ Orbits prepareOrbits(Trace& trace, const KFState& kfState) case KF::EMP_P_0: { - orbit.empInput.push_back({false, 0, E_EmpAxis::P, trigType, stateValue, subKey} + orbit.empInput.push_back( + {false, 0, E_EmpAxis::P, trigType, stateValue, subKey} ); break; } case KF::EMP_P_1: { - orbit.empInput.push_back({false, 1, E_EmpAxis::P, trigType, stateValue, subKey} + orbit.empInput.push_back( + {false, 1, E_EmpAxis::P, trigType, stateValue, subKey} ); break; } case KF::EMP_P_2: { - orbit.empInput.push_back({false, 2, E_EmpAxis::P, trigType, stateValue, subKey} + orbit.empInput.push_back( + {false, 2, E_EmpAxis::P, trigType, stateValue, subKey} ); break; } case KF::EMP_P_3: { - orbit.empInput.push_back({false, 3, E_EmpAxis::P, trigType, stateValue, subKey} + orbit.empInput.push_back( + {false, 3, E_EmpAxis::P, trigType, stateValue, subKey} ); break; } case KF::EMP_P_4: { - orbit.empInput.push_back({false, 4, E_EmpAxis::P, trigType, stateValue, subKey} + orbit.empInput.push_back( + {false, 4, E_EmpAxis::P, trigType, stateValue, subKey} ); break; } case KF::EMP_Q_0: { - orbit.empInput.push_back({false, 0, E_EmpAxis::Q, trigType, stateValue, subKey} + orbit.empInput.push_back( + {false, 0, E_EmpAxis::Q, trigType, stateValue, subKey} ); break; } case KF::EMP_Q_1: { - orbit.empInput.push_back({false, 1, E_EmpAxis::Q, trigType, stateValue, subKey} + orbit.empInput.push_back( + {false, 1, E_EmpAxis::Q, trigType, stateValue, subKey} ); break; } case KF::EMP_Q_2: { - orbit.empInput.push_back({false, 2, E_EmpAxis::Q, trigType, stateValue, subKey} + orbit.empInput.push_back( + {false, 2, E_EmpAxis::Q, trigType, stateValue, subKey} ); break; } case KF::EMP_Q_3: { - orbit.empInput.push_back({false, 3, E_EmpAxis::Q, trigType, stateValue, subKey} + orbit.empInput.push_back( + {false, 3, E_EmpAxis::Q, trigType, stateValue, subKey} ); break; } case KF::EMP_Q_4: { - orbit.empInput.push_back({false, 4, E_EmpAxis::Q, trigType, stateValue, subKey} + orbit.empInput.push_back( + {false, 4, E_EmpAxis::Q, trigType, stateValue, subKey} ); break; } diff --git a/src/cpp/orbprop/spaceWeather.cpp b/src/cpp/orbprop/spaceWeather.cpp index 56df35401..7badb1103 100644 --- a/src/cpp/orbprop/spaceWeather.cpp +++ b/src/cpp/orbprop/spaceWeather.cpp @@ -66,14 +66,18 @@ void SpaceWeather::read(std::string filepath) ///< File path to Space Weather f std::string ignore; iss >> ignore >> numMonthlyPredictedPoints; } - else if (line == "BEGIN OBSERVED" || line == "BEGIN DAILY_PREDICTED" || - line == "BEGIN MONTHLY_PREDICTED") + else if ( + line == "BEGIN OBSERVED" || line == "BEGIN DAILY_PREDICTED" || + line == "BEGIN MONTHLY_PREDICTED" + ) { // Skip section headers continue; } - else if (line == "END OBSERVED" || line == "END DAILY_PREDICTED" || - line == "END MONTHLY_PREDICTED") + else if ( + line == "END OBSERVED" || line == "END DAILY_PREDICTED" || + line == "END MONTHLY_PREDICTED" + ) { // Skip section endings continue; diff --git a/src/cpp/orbprop/staticField.cpp b/src/cpp/orbprop/staticField.cpp index 6ef1634b5..3d4101afd 100644 --- a/src/cpp/orbprop/staticField.cpp +++ b/src/cpp/orbprop/staticField.cpp @@ -9,7 +9,7 @@ using std::vector; -// todo aaron global +// todo? global StaticField egm; /** Read the static gravity field diff --git a/src/cpp/orbprop/tideCoeff.cpp b/src/cpp/orbprop/tideCoeff.cpp index 97192928f..026f2f4dd 100644 --- a/src/cpp/orbprop/tideCoeff.cpp +++ b/src/cpp/orbprop/tideCoeff.cpp @@ -120,7 +120,7 @@ void Tide::setBeta(GTime time, double ut1_utc) { FundamentalArgs fundArgs(time, ut1_utc); - beta(0) = fundArgs.gmst - fundArgs.f - fundArgs.omega; // todo aaron, swap with doodson? + beta(0) = fundArgs.gmst - fundArgs.f - fundArgs.omega; // todo? swap with doodson? beta(1) = fundArgs.f + fundArgs.omega; beta(2) = beta(1) - fundArgs.d; beta(3) = beta(1) - fundArgs.l; diff --git a/src/cpp/other_ssr/prototypeCmpSSRDecode.cpp b/src/cpp/other_ssr/prototypeCmpSSRDecode.cpp index ae43ae934..8d8a9f40c 100644 --- a/src/cpp/other_ssr/prototypeCmpSSRDecode.cpp +++ b/src/cpp/other_ssr/prototypeCmpSSRDecode.cpp @@ -121,8 +121,9 @@ double checkDisc(SatSys Sat, E_ObsCode code, double bias, int disc, int regionID newBias = true; else if (compactSsrPhaseDisc[regionID].find(Sat) == compactSsrPhaseDisc[regionID].end()) newBias = true; - else if (compactSsrPhaseDisc[regionID][Sat].find(code) == - compactSsrPhaseDisc[regionID][Sat].end()) + else if ( + compactSsrPhaseDisc[regionID][Sat].find(code) == compactSsrPhaseDisc[regionID][Sat].end() + ) newBias = true; auto& storedDisc = compactSsrPhaseDisc[regionID][Sat][code]; @@ -791,7 +792,7 @@ int decodeSSR_clock(vector& data, GTime now) if (ssrMeta.receivedTime > compactSsrLastTime) compactSsrLastTime = - ssrMeta.receivedTime; // todo aaron, this is all copy-paste in every function + ssrMeta.receivedTime; // todo? this is all copy-paste in every function if (now < compactSsrLastTime) return E_ReturnType::WAIT; diff --git a/src/cpp/other_ssr/prototypeCmpSSREncode.cpp b/src/cpp/other_ssr/prototypeCmpSSREncode.cpp index 4c82b6eba..cb0fd7e78 100644 --- a/src/cpp/other_ssr/prototypeCmpSSREncode.cpp +++ b/src/cpp/other_ssr/prototypeCmpSSREncode.cpp @@ -823,12 +823,12 @@ vector encodecompactBIA( map> stecPolyCommonMode; vector encodecompactTEC( - SSRMeta& ssrMeta, - int regId, - SSRAtmRegion& ssrAtmReg, - int updateIntIndex, - bool last - ) + SSRMeta& ssrMeta, + int regId, + SSRAtmRegion& ssrAtmReg, + int updateIntIndex, + bool last +) { vector buffer; @@ -1497,11 +1497,11 @@ vector encodeGridInfo(SSRAtm& ssrAtm) double thisLatDeg = regData.gridLatDeg[0]; double thisLonDeg = regData.gridLonDeg[0]; tmp = (int)round(regData.gridLatDeg[0] * 16284 / PI * D2R); - i = setbitsInc(buf, i, 15, tmp); // todo aaron, check scaling facotr - tmp = (int)round(regData.gridLonDeg[0] * 16284 / PI * D2R); - i = setbitsInc(buf, i, 16, tmp); - tmp = regData.gridLatDeg.size(); - i = setbituInc(buf, i, 6, tmp); + i = setbitsInc(buf, i, 15, tmp); // todo? check scaling facotr + tmp = (int)round(regData.gridLonDeg[0] * 16284 / PI * D2R); + i = setbitsInc(buf, i, 16, tmp); + tmp = regData.gridLatDeg.size(); + i = setbituInc(buf, i, 6, tmp); for (auto& [ind, lat] : regData.gridLatDeg) { diff --git a/src/cpp/other_ssr/prototypeIgsSSRDecode.cpp b/src/cpp/other_ssr/prototypeIgsSSRDecode.cpp index aa8a82dfd..1d9658c97 100644 --- a/src/cpp/other_ssr/prototypeIgsSSRDecode.cpp +++ b/src/cpp/other_ssr/prototypeIgsSSRDecode.cpp @@ -652,7 +652,7 @@ void decodeigsSSR_type8(vector& data, GTime now) return; SSRAtmGlobal ssrAtmGlobal; - ssrAtmGlobal.numberLayers = ssrHead.numLayers; // todo aaron, can these be the same thing? + ssrAtmGlobal.numberLayers = ssrHead.numLayers; // todo? can these be the same thing? ssrAtmGlobal.vtecQuality = ssrHead.vtecQuality; ssrAtmGlobal.time = ssrHead.time; @@ -667,7 +667,7 @@ void decodeigsSSR_type8(vector& data, GTime now) int nind = 0; for (int ord = 0; ord < layer.maxOrder; ord++) for (int deg = ord; deg < layer.maxDegree && i + 16 <= data.size() * 8; - deg++) // todo aaron duplicate size checks redundant? + deg++) // todo? duplicate size checks redundant? { layer.sphHarmonic[nind].layer = layerNum; diff --git a/src/cpp/other_ssr/prototypeIgsSSREncode.cpp b/src/cpp/other_ssr/prototypeIgsSSREncode.cpp index d1e832c98..a009a4988 100644 --- a/src/cpp/other_ssr/prototypeIgsSSREncode.cpp +++ b/src/cpp/other_ssr/prototypeIgsSSREncode.cpp @@ -632,8 +632,9 @@ vector encodeIGS_ATM(SSRAtm& ssrAtm, bool last) for (auto& [ibas, b] : vteclay.sphHarmonic) { basisMaps[ilay][b.degree][b.order][b.trigType] = b.value; - bitLen += 16 * (SQR(b.degree + 1) - (b.degree - b.order) * (b.degree - b.order + 1) - ); // todo aaron, check should be +1,0? + bitLen += 16 * (SQR(b.degree + 1) - + (b.degree - b.order) * + (b.degree - b.order + 1)); // todo? check should be +1,0? } int byteLen = ceil(bitLen / 8.0); diff --git a/src/cpp/pea/inputs.cpp b/src/cpp/pea/inputs.cpp index 49940c29f..1cbfa90b4 100644 --- a/src/cpp/pea/inputs.cpp +++ b/src/cpp/pea/inputs.cpp @@ -276,19 +276,24 @@ void addReceiverData( shared_ptr streamParser_ptr; if (dataType == "OBS") + { streamParser_ptr = make_shared(std::move(stream_ptr), std::move(parser_ptr)); + + auto& rec = receiverMap[id]; + rec.id = id; + } else if (dataType == "PSEUDO") + { streamParser_ptr = make_shared(std::move(stream_ptr), std::move(parser_ptr), true); + + auto& rec = receiverMap[id]; + rec.isPseudoRec = true; + } else + { streamParser_ptr = make_shared(std::move(stream_ptr), std::move(parser_ptr)); - - if (dataType == "OBS") - { - auto& rec = receiverMap[id]; - - rec.id = id; } streamParser_ptr->stream.sourceString = inputName; diff --git a/src/cpp/pea/main.cpp b/src/cpp/pea/main.cpp index e8f750848..3215eb339 100644 --- a/src/cpp/pea/main.cpp +++ b/src/cpp/pea/main.cpp @@ -1,17 +1,22 @@ // #pragma GCC optimize ("O0") #include +#include #include #include #include #include +#include #include #include #include #include +#include #include +#include #include #include +#include #include "architectureDocs.hpp" #include "common/algebraTrace.hpp" #include "common/api.hpp" @@ -36,13 +41,16 @@ #include "common/testUtils.hpp" #include "inertial/posProp.hpp" #include "iono/ionoModel.hpp" +#if defined(ENABLE_PARALLELISATION) || defined(_OPENMP) #include "omp.h" +#endif #include "orbprop/coordinates.hpp" #include "orbprop/orbitProp.hpp" #include "pea/inputsOutputs.hpp" #include "pea/minimumConstraints.hpp" #include "pea/peaCommitStrings.hpp" #include "pea/preprocessor.hpp" +#include "slr/slr.hpp" using namespace std::literals::chrono_literals; using std::make_shared; @@ -85,6 +93,90 @@ Navigation nav = {}; int epoch = 0; GTime tsync = GTime::noTime(); map satIdMap; +map latestMissingObsStatusByReceiver; + +struct ConfiguredStreamState +{ + std::set configuredSources; + std::set configuredReceivers; +}; + +std::vector retiredStreamParsers; + +string streamDisplayName(const string& sourceString) +{ + URL url(sourceString); + + if (url.host.empty() == false && url.path.empty() == false) + { + if (url.path.front() == '/') + { + return url.path.substr(1); + } + + return url.path; + } + + return sourceString; +} + +void retireStreamParser(StreamParserPtr streamParser_ptr) +{ + if (streamParser_ptr == nullptr) + { + return; + } + + try + { + if (auto* tcpSocket = dynamic_cast(&streamParser_ptr->stream)) + { + tcpSocket->shutdown(); + } + } + catch (...) + { + } + + retiredStreamParsers.push_back(std::move(streamParser_ptr)); +} + +ConfiguredStreamState getConfiguredStreamState() +{ + ConfiguredStreamState state; + + auto addSources = [&](const vector& inputNames) + { state.configuredSources.insert(inputNames.begin(), inputNames.end()); }; + + auto addReceiverSources = [&](const auto& inputMap) + { + for (const auto& [id, inputNames] : inputMap) + { + state.configuredReceivers.insert(id); + addSources(inputNames); + } + }; + + for (const auto& [id, slrInputs] : slrObsFiles) + { + state.configuredReceivers.insert(id); + addSources(slrInputs); + } + + addReceiverSources(acsConfig.ubx_inputs); + addReceiverSources(acsConfig.sbf_inputs); + addReceiverSources(acsConfig.custom_inputs); + addReceiverSources(acsConfig.rnx_inputs); + addReceiverSources(acsConfig.obs_rtcm_inputs); + addReceiverSources(acsConfig.pseudo_sp3_inputs); + addReceiverSources(acsConfig.pseudo_snx_inputs); + + addSources(acsConfig.nav_rtcm_inputs); + addSources(acsConfig.qzs_rtcm_inputs); + addSources(acsConfig.sisnet_inputs); + + return state; +} void avoidCollisions(ReceiverMap& receiverMap) { @@ -126,9 +218,22 @@ void mainOncePerEpochPerStation(Receiver& rec, Network& net, bool& emptyEpoch, K if (rec.ready == false) { + string missingStatus; + if (auto it = latestMissingObsStatusByReceiver.find(rec.id); + it != latestMissingObsStatusByReceiver.end()) + { + missingStatus = it->second; + } + trace << "\n" << "Receiver " << rec.id << " has no data for this epoch"; - BOOST_LOG_TRIVIAL(info) << "Receiver " << rec.id << " has no data for this epoch"; + if (missingStatus.empty() == false) + { + trace << " (" << missingStatus << ")"; + } + + BOOST_LOG_TRIVIAL(info) << "Receiver " << rec.id << " has no data for this epoch" + << (missingStatus.empty() ? "" : " (" + missingStatus + ")"); return; } @@ -179,10 +284,30 @@ void mainOncePerEpochPerStation(Receiver& rec, Network& net, bool& emptyEpoch, K bool sppUsed; selectAprioriSource(trace, rec, tsync, sppUsed, net.kfState, &remoteState); - if (missingWarnInvalidate("Apriori position1", sppUsed, acsConfig.require_apriori_positions)) - return; + if (sppUsed) + { + string message = + "fixed receiver apriori position not found for " + rec.id + + "; using SPP fallback. Check receiver_options." + rec.id + + ".apriori_position, receiver_options." + rec.id + + ".models.pos.sources, and loaded SINEX station coordinates."; + + if (acsConfig.require_apriori_positions) + { + trace << "\n" + << "Warning: Receiver " << rec.id << " rejected because " << message; + BOOST_LOG_TRIVIAL(warning) << "Receiver " << rec.id << " rejected because " << message; + + rec.invalid = true; + + return; + } + + BOOST_LOG_TRIVIAL(warning) << message; + } + if (missingWarnInvalidate( - "Apriori position2", + "Receiver metadata apriori position", rec.failureAprioriPos, acsConfig.require_apriori_positions )) @@ -208,6 +333,7 @@ void mainOncePerEpochPerStation(Receiver& rec, Network& net, bool& emptyEpoch, K << rec.id; // calculate statistics + if (rec.obsList.empty() == false) { if ((GTime)rec.firstEpoch == GTime::noTime()) { @@ -302,7 +428,7 @@ void mainOncePerEpochPerSatellite( // reinitialise the options with the updated values satOpts._initialised = - false; // todo aaron, this is insufficient since the opts are inherited from the other + false; // todo? this is insufficient since the opts are inherited from the other // initialised ones per file which are not reset } @@ -601,12 +727,28 @@ int main(int argc, char** argv) bool pass = configure(argc, argv); if (pass == false) { - BOOST_LOG_TRIVIAL(error) << "Incorrect configuration"; + if (acsConfig.dry_run) + { + BOOST_LOG_TRIVIAL(error) << "Dry-run failed: configuration is invalid"; + } + else + { + BOOST_LOG_TRIVIAL(error) << "Incorrect configuration"; + } + BOOST_LOG_TRIVIAL(info) << "PEA finished"; TcpSocket::ioContext.stop(); return EXIT_FAILURE; } + if (acsConfig.dry_run) + { + BOOST_LOG_TRIVIAL(info) << "Dry-run successful: configuration and sanity checks passed"; + BOOST_LOG_TRIVIAL(info) << "PEA finished"; + TcpSocket::ioContext.stop(); + return EXIT_SUCCESS; + } + if (acsConfig.output_log) { addFileLog(acsConfig.log_json); @@ -678,7 +820,7 @@ int main(int argc, char** argv) pppNet.kfState.stateRejectCallbacks.push_back( rejectWorstMeasByState ); // Assume the state error is caused by a single measurement error and try removing it - // first + // first pppNet.kfState.stateRejectCallbacks.push_back(relaxState); pppNet.kfState.stateRejectCallbacks.push_back(rejectAllMeasByState); } @@ -695,12 +837,12 @@ int main(int argc, char** argv) ionNet.kfState.measRejectCallbacks.push_back(deweightMeas); - pppNet.kfState.stateRejectCallbacks.push_back(incrementStateErrors); + ionNet.kfState.stateRejectCallbacks.push_back(incrementStateErrors); ionNet.kfState.stateRejectCallbacks.push_back( rejectWorstMeasByState ); // Assume the state error is caused by a single measurement error and try removing it - // first - pppNet.kfState.stateRejectCallbacks.push_back(relaxState); + // first + ionNet.kfState.stateRejectCallbacks.push_back(relaxState); ionNet.kfState.stateRejectCallbacks.push_back(rejectAllMeasByState); } @@ -725,16 +867,29 @@ int main(int argc, char** argv) reloadInputFiles(); + auto hasRealtimeObservationInput = []() + { + for (auto& [id, streamParser_ptr] : streamParserMultimap) + { + auto* obsStream = dynamic_cast(streamParser_ptr.get()); + if (obsStream != nullptr && dynamic_cast(&obsStream->stream) != nullptr) + { + return true; + } + } + + return false; + }; + + bool hasRealtimeObsInput = hasRealtimeObservationInput(); + addDefaultBias(); TcpSocket::startClients(); if (acsConfig.start_epoch.is_not_a_date_time() == false) { - PTime startTime; - startTime.bigTime = boost::posix_time::to_time_t(acsConfig.start_epoch); - - GTime startGTime = startTime; + GTime startGTime = GTime(acsConfig.start_epoch); tsync = startGTime.floorTime(acsConfig.epoch_interval); if (tsync != startGTime) @@ -744,7 +899,7 @@ int main(int argc, char** argv) << acsConfig.epoch_interval << ", rounding down to " << tsync; } - acsConfig.start_epoch = boost::posix_time::from_time_t((time_t)((PTime)tsync).bigTime); + acsConfig.start_epoch = tsync.to_posixTime(); } createTracefiles(receiverMap, pppNet, ionNet); @@ -778,6 +933,7 @@ int main(int argc, char** argv) //============================================================================ GTime lastEpochStartTime; + GTime lastProcStartTime; GTime lastEpochStopTime; auto atLastEpoch = [&](bool processed = false) -> bool @@ -797,25 +953,25 @@ int main(int argc, char** argv) return false; } - int fractionalMilliseconds = (tsync.bigTime - (long int)tsync.bigTime) * 1000; - auto boostTime = boost::posix_time::from_time_t((time_t)((PTime)tsync).bigTime) + - boost::posix_time::millisec(fractionalMilliseconds); - GWeek week = tsync; GTow tow = tsync; if (processed) { - BOOST_LOG_TRIVIAL(info) << "Processed epoch #" << epoch << " - " << "GPS time: " << week - << " " << std::setw(6) << tow << " - " << boostTime << " (took " - << (lastEpochStopTime - lastEpochStartTime) << ")"; + BOOST_LOG_TRIVIAL(info) + << "Processed epoch #" << epoch << " - " << "GPS time: " << week << " " + << std::setw(6) << tow << " - " << tsync << " (data handling took " + << (lastProcStartTime - lastEpochStartTime) << ", processing took " + << (lastEpochStopTime - lastProcStartTime) << ", epoch took " + << (lastEpochStopTime - lastEpochStartTime) << ")"; } // Check end epoch - if (acsConfig.end_epoch.is_not_a_date_time() == false && boostTime >= acsConfig.end_epoch) + if (acsConfig.end_epoch.is_not_a_date_time() == false && + tsync.to_posixTime() >= acsConfig.end_epoch) { BOOST_LOG_TRIVIAL(info) - << "Exiting at epoch " << epoch << " (" << boostTime << ") as end epoch " + << "Exiting at epoch " << epoch << " (" << tsync << ") as end epoch " << acsConfig.end_epoch << " has been reached"; return true; @@ -835,8 +991,15 @@ int main(int argc, char** argv) auto nominalLoopStartTime = system_clock::now(); // The time the next loop is expected to start - if it doesnt start // until after this, it may be skipped + GTime firstObsTime = GTime::noTime(); + bool holdingReconnectOutage = false; + bool hasProcessedRealtimeEpoch = false; + auto reconnectOutageStart = system_clock::time_point{}; + auto lastOutageStatusLog = system_clock::time_point{}; + while (complete == false) { + // Increment epoch and tsync if (nextEpoch) { nextEpoch = false; @@ -847,6 +1010,8 @@ int main(int argc, char** argv) nominalLoopStartTime += std::chrono::milliseconds((int)(acsConfig.wait_next_epoch * 1000)); + lastEpochStartTime = timeGet(); + if (tsync != GTime::noTime()) { // dont obliterate the freshly configured tsync before the first epoch @@ -855,10 +1020,12 @@ int main(int argc, char** argv) tsync.bigTime += acsConfig.epoch_interval; } - if (fabs(tsync.bigTime - round(tsync.bigTime)) < acsConfig.epoch_tolerance) - { - tsync.bigTime = round(tsync.bigTime); - } + // Eugene: This will try forcing align tsync to integeral epochs, which can make + // processing epochs something like: 0.0, 0.3, 0.6, 1.0, ... + // if (fabs(tsync.bigTime - round(tsync.bigTime)) < acsConfig.epoch_tolerance) + // { + // tsync.bigTime = round(tsync.bigTime); + // } } for (auto& [id, rec] : receiverMap) @@ -893,7 +1060,7 @@ int main(int argc, char** argv) auto breakTime = nominalLoopStartTime + std::chrono::milliseconds((int)(acsConfig.max_rec_latency * 1000)); - if (loopEpochs) + if (loopEpochs) // Eugene: This doesn't do anything at all? { BOOST_LOG_TRIVIAL(info) << "Starting epoch #" << epoch; @@ -918,37 +1085,45 @@ int main(int argc, char** argv) // get observations from streams (allow some delay between stations, and retry, to ensure // all messages for the epoch have arrived) - map dataAvailableMap; - bool repeat = true; + map dataAvailableMap; + map missingObsReasons; + map missingObsSources; + map missingObsStates; + vector readyLatencySeconds; + bool repeat = true; + int attempt = 0; while (repeat && system_clock::now() < breakTime) { + attempt++; + repeat = false; // load any changes from the config bool newConfig = acsConfig.parse(); // make any changes to streams. + ConfiguredStreamState configuredStreamState; if (newConfig) { + reloadInputFiles(); configureUploadingStreams(); + configuredStreamState = getConfiguredStreamState(); } // remove any dead streams for (auto iter = streamParserMultimap.begin(); iter != streamParserMultimap.end();) { auto& [id, streamParser_ptr] = *iter; - auto& stream = streamParser_ptr->stream; + auto& stream = streamParser_ptr->stream; + string recId = id; auto& recOpts = acsConfig.getRecOpts(id); - if (recOpts.kill) + auto removeReceiver = [&]() { - BOOST_LOG_TRIVIAL(info) - << "Removing " << stream.sourceString << " due to kill config" << "\n"; - for (auto& [key, index] : pppNet.kfState.kfIndexMap) { - if (key.str == id) + if (key.str == recId) { pppNet.kfState.removeState(key); @@ -958,9 +1133,42 @@ int main(int argc, char** argv) } } - receiverMap.erase(id); + receiverMap.erase(recId); + }; + + if (newConfig && + configuredStreamState.configuredSources.find(stream.sourceString) == + configuredStreamState.configuredSources.end()) + { + auto retiredStreamParser = streamParser_ptr; + + BOOST_LOG_TRIVIAL(info) << "Removing " << stream.sourceString + << " because it is no longer configured"; + + streamDOAMap.erase(stream.sourceString); + + if (configuredStreamState.configuredReceivers.find(recId) == + configuredStreamState.configuredReceivers.end()) + { + removeReceiver(); + } iter = streamParserMultimap.erase(iter); + retireStreamParser(std::move(retiredStreamParser)); + + continue; + } + + if (recOpts.kill) + { + BOOST_LOG_TRIVIAL(info) + << "Removing " << stream.sourceString << " due to kill config"; + auto retiredStreamParser = streamParser_ptr; + + removeReceiver(); + + iter = streamParserMultimap.erase(iter); + retireStreamParser(std::move(retiredStreamParser)); continue; } @@ -981,8 +1189,7 @@ int main(int argc, char** argv) if (stream.isAvailable() && stream.isDead()) { - BOOST_LOG_TRIVIAL(info) - << "No more data available on " << stream.sourceString << "\n"; + BOOST_LOG_TRIVIAL(info) << "No more data available on " << stream.sourceString; // record as dead and erase streamDOAMap[stream.sourceString] = true; @@ -995,6 +1202,12 @@ int main(int argc, char** argv) iter++; } + if (newConfig) + { + hasRealtimeObsInput = hasRealtimeObservationInput(); + } + + // Check if all streams are dead if (streamParserMultimap.empty()) { static bool once = true; @@ -1012,6 +1225,10 @@ int main(int argc, char** argv) break; } + BOOST_LOG_TRIVIAL(debug) << "\n"; + BOOST_LOG_TRIVIAL(debug) << "tsync: " << tsync.to_string(6) << " - attempt " << attempt; + BOOST_LOG_TRIVIAL(debug) << "Parsing non-observation streams ..."; + // parse all non-observation streams for (auto& [id, streamParser_ptr] : streamParserMultimap) try @@ -1023,166 +1240,327 @@ int main(int argc, char** argv) streamParser_ptr->parse(); } - for (auto& [id, streamParser_ptr] : streamParserMultimap) + BOOST_LOG_TRIVIAL(debug) << "Parsing observation streams ..."; + + // Parse observation streams (including pseudo observations) for all receivers + for (auto& [id, rec] : receiverMap) { - ObsStream* obsStream_ptr; + auto& recOpts = acsConfig.getRecOpts(id); - try - { - obsStream_ptr = &dynamic_cast(*streamParser_ptr); - } - catch (std::bad_cast& e) + if (recOpts.exclude) { continue; } - auto& obsStream = *obsStream_ptr; - - if (obsStream.stream.isAvailable() == false) + if (rec.ready) { + BOOST_LOG_TRIVIAL(debug) << "Receiver " << id << " is ready, skipping ..."; continue; } - auto& recOpts = acsConfig.getRecOpts(id); + missingObsReasons[id] = "no_obs_stream"; - if (recOpts.exclude) + auto trace = getTraceFile(rec); + + // Search suitable data from all files from this receiver (for real-time data, there + // should be only one unique stream per receiver) + auto [begin, end] = streamParserMultimap.equal_range(id); + for (auto it = begin; it != end; it++) { - continue; - } + auto& streamParser_ptr = it->second; - auto& rec = receiverMap[id]; + ObsStream* obsStream_ptr; - auto trace = getTraceFile(rec); + try + { + obsStream_ptr = &dynamic_cast(*streamParser_ptr); + } + catch (std::bad_cast& e) + { + continue; + } - if (obsStream.isPseudoRec) - { - rec.isPseudoRec = true; - } + auto& obsStream = *obsStream_ptr; - // try to get some data (again) - if (rec.ready) - { - continue; - } + missingObsSources[id] = streamDisplayName(obsStream.stream.sourceString); - bool moreData = true; - while (moreData) - { - if (acsConfig.assign_closest_epoch) - rec.obsList = obsStream.getObs(tsync, acsConfig.epoch_interval / 2); - else - rec.obsList = obsStream.getObs(tsync, acsConfig.epoch_tolerance); + auto describeStreamState = [&](const string& reason) + { + std::ostringstream state; + state << "reason=" << reason + << ", source=" << streamDisplayName(obsStream.stream.sourceString) + << ", available=" << (obsStream.stream.isAvailable() ? "yes" : "no") + << ", dead=" << (obsStream.stream.isDead() ? "yes" : "no"); + + if (auto* tcpSocket = dynamic_cast(&obsStream.stream)) + { + long long nextReconnectUnixTime = tcpSocket->nextReconnectUnixTime(); + if (nextReconnectUnixTime > 0) + { + auto nowUnixTime = + std::chrono::system_clock::to_time_t(system_clock::now()); + long long reconnectInSeconds = + std::max(0LL, nextReconnectUnixTime - (long long)nowUnixTime); + state << ", reconnect_in_s=" << reconnectInSeconds; + } + else + { + state << ", reconnect_in_s=0"; + } + } + else + { + state << ", reconnect_in_s=n/a"; + } + + if (obsStream.obsAgeCode != E_ObsAgeCode::UNKNOWN) + { + state << ", obs_age=" << obsStream.obsAgeCode; + } + + return state.str(); + }; + + BOOST_LOG_TRIVIAL(debug) << "Reading stream '" << obsStream.stream.sourceString + << "', isAvailable=" << obsStream.stream.isAvailable() + << ", isDead=" << obsStream.stream.isDead(); - switch (obsStream.obsAgeCode) + if (obsStream.stream.isAvailable() == false) { - case E_ObsAgeCode::NO_OBS: - moreData = false; - break; - case E_ObsAgeCode::PAST_OBS: - preprocessor(trace, rec); - break; - case E_ObsAgeCode::CURRENT_OBS: - moreData = false; - preprocessor(trace, rec); - break; - case E_ObsAgeCode::FUTURE_OBS: - moreData = false; - break; + missingObsReasons[id] = "stream_unavailable"; + missingObsStates[id] = describeStreamState("stream_unavailable"); + BOOST_LOG_TRIVIAL(debug) << "Skipping unavailable stream '" + << obsStream.stream.sourceString << "'"; + continue; } - } - if (rec.obsList.empty()) - { - // failed to get observations - if (obsStream.obsAgeCode == E_ObsAgeCode::NO_OBS) + if (auto* tcpSocket = dynamic_cast(&obsStream.stream)) { - // try again later - repeat = true; - sleep_for(std::chrono::milliseconds(acsConfig.sleep_milliseconds)); + if (tcpSocket->reconnectDueAfter(breakTime)) + { + missingObsReasons[id] = "reconnect_after_breaktime"; + missingObsStates[id] = describeStreamState("reconnect_after_breaktime"); + BOOST_LOG_TRIVIAL( + debug + ) << "Skipping stream '" + << obsStream.stream.sourceString + << "' for this epoch because reconnect is scheduled after breakTime"; + continue; + } } - continue; - } + double epochTolerance = + DTTOL; // Use a very small tolerance if data interval is unknown (0) yet, + // otherwise can take in multiple-epoch data - if (tsync == GTime::noTime()) - { - tsync = rec.obsList.front()->time.floorTime(acsConfig.epoch_interval); + // try to get some data (again) + bool moreData = true; + while (moreData) + { + if (acsConfig.assign_closest_epoch) + { + BOOST_LOG_TRIVIAL(warning) + << "'assign_closest_epoch' is on, observations that fall between " + "processing epochs will be grouped to nearest epochs despite " + "'epoch_tolerance'"; - acsConfig.start_epoch = - boost::posix_time::from_time_t((time_t)((PTime)tsync).bigTime); + epochTolerance = acsConfig.epoch_interval / 2; + } + else if (obsStream.interval > 0) + { + epochTolerance = + std::min(obsStream.interval / 2, acsConfig.epoch_tolerance); + } + + BOOST_LOG_TRIVIAL(debug) + << "Getting obs, id=" << id << ", targetTime=" << tsync.to_string(6) + << ", epochTolerance=" << epochTolerance; + + GTime obsTime = tsync; + rec.obsList = obsStream.getObs(obsTime, epochTolerance); + + cleanSignals(rec.obsList); + + switch (obsStream.obsAgeCode) + { + case E_ObsAgeCode::UNKNOWN: + moreData = false; + if (firstObsTime == GTime::noTime() || obsTime < firstObsTime) + { + firstObsTime = obsTime; + } + break; + case E_ObsAgeCode::NO_OBS: + moreData = false; + break; + case E_ObsAgeCode::PAST_OBS: + preprocessor(trace, rec); + break; + case E_ObsAgeCode::CURRENT_OBS: + moreData = false; // Eugene to fix: May have more current obs from + // next file + preprocessor(trace, rec); + break; + case E_ObsAgeCode::FUTURE_OBS: + moreData = false; + break; + } - if (tsync + acsConfig.epoch_tolerance < rec.obsList.front()->time) + BOOST_LOG_TRIVIAL(debug) + << "Checking obs age and preprocessing data if needed" + << ", obsAgeCode=" << obsStream.obsAgeCode + << ", moreData=" << (moreData ? "true" : "false"); + } + + if (rec.obsList.empty()) { - repeat = true; - continue; + // Can only be NO_OBS or FUTURE_OBS + if (obsStream.obsAgeCode == E_ObsAgeCode::NO_OBS) + { + missingObsReasons[id] = "no_obs"; + missingObsStates[id] = describeStreamState("no_obs"); + // Failed to get observations, try again later + repeat = true; // Only need to retry on NO_OBS, for FUTURE_OBS, just + // move to next receiver + + BOOST_LOG_TRIVIAL(debug) + << "Failed to get observations, try again later ..."; + } + else if (obsStream.obsAgeCode == E_ObsAgeCode::FUTURE_OBS) + { + missingObsReasons[id] = "future_obs"; + missingObsStates[id] = describeStreamState("future_obs"); + } + else if (obsStream.obsAgeCode == E_ObsAgeCode::UNKNOWN) + { + missingObsReasons[id] = "unknown_obs_age"; + missingObsStates[id] = describeStreamState("unknown_obs_age"); + } + + break; } - } - dataAvailableMap[rec.id] = true; - rec.ready = true; - rec.source = obsStream.stream.sourceString; + dataAvailableMap[rec.id] = true; + missingObsReasons.erase(rec.id); + missingObsSources.erase(rec.id); + missingObsStates.erase(rec.id); + rec.ready = true; + rec.source = obsStream.stream.sourceString; - extractTrackedSignals(rec, obsStream.parser, &rec.obsList); + extractReceiverMetadata(rec, obsStream.parser, &rec.obsList); + extractTrackedSignals(rec, obsStream.parser, &rec.obsList); - auto now = system_clock::now(); + GTime readyTime = timeGet(); + if (tsync != GTime::noTime()) + { + double readyLatencySecondsValue = (readyTime - tsync).to_double(); + readyLatencySeconds.push_back(readyLatencySecondsValue); + + BOOST_LOG_TRIVIAL(debug) + << "Receiver " << rec.id << " ready diagnostic: attempt=" << attempt + << ", ready_time=" << readyTime << ", ready_latency_s=" << std::fixed + << std::setprecision(2) << readyLatencySecondsValue + << ", obs_count=" << rec.obsList.size() + << ", source=" << streamDisplayName(rec.source); + } + else + { + BOOST_LOG_TRIVIAL(debug) + << "Receiver " << rec.id << " ready diagnostic: attempt=" << attempt + << ", ready_time=" << readyTime + << ", ready_latency_s=startup_pending_tsync" + << ", obs_count=" << rec.obsList.size() + << ", source=" << streamDisplayName(rec.source); + } - if (now >= nominalLoopStartTime) - { - auto nominalLatency = now - nominalLoopStartTime; - - trace << "\n" - << std::chrono::duration_cast( - nominalLoopStartTime - peaStartTimeChrono - ) - .count() - << "ms" << " " - << std::chrono::duration_cast( - now - peaStartTimeChrono - ) - .count() - << "ms" << " " << rec.id << " nominal latency : " - << std::chrono::duration_cast(nominalLatency) - .count() - << "ms"; - } - else - { - // this observation is earlier than expected - // only shorten waiting periods, never extend - - auto nominalLatency = nominalLoopStartTime - now; - - trace << "\n" - << std::chrono::duration_cast( - nominalLoopStartTime - peaStartTimeChrono - ) - .count() - << "ms" << " " - << std::chrono::duration_cast( - now - peaStartTimeChrono - ) - .count() - << "ms" << " " << rec.id << " nominal latency : -" - << std::chrono::duration_cast(nominalLatency) - .count() - << "ms" << " Advancing start time"; - - auto alternateBreakTime = - now + std::chrono::milliseconds((int)(acsConfig.max_rec_latency * 1000)); - auto alternateStartTime = now; - - if (alternateBreakTime < breakTime) + auto now = system_clock::now(); + + if (now >= nominalLoopStartTime) { - breakTime = alternateBreakTime; + auto nominalLatency = now - nominalLoopStartTime; + + trace << "\n" + << std::chrono::duration_cast( + nominalLoopStartTime - peaStartTimeChrono + ) + .count() + << "ms" << " " + << std::chrono::duration_cast( + now - peaStartTimeChrono + ) + .count() + << "ms" << " " << rec.id << " nominal latency : " + << std::chrono::duration_cast( + nominalLatency + ) + .count() + << "ms"; } - if (alternateStartTime < nominalLoopStartTime) + else { - nominalLoopStartTime = alternateStartTime; + // this observation is earlier than expected + // only shorten waiting periods, never extend + + auto nominalLatency = nominalLoopStartTime - now; + + trace << "\n" + << std::chrono::duration_cast( + nominalLoopStartTime - peaStartTimeChrono + ) + .count() + << "ms" << " " + << std::chrono::duration_cast( + now - peaStartTimeChrono + ) + .count() + << "ms" << " " << rec.id << " nominal latency : -" + << std::chrono::duration_cast( + nominalLatency + ) + .count() + << "ms" << " Advancing start time"; + + auto alternateBreakTime = + now + + std::chrono::milliseconds((int)(acsConfig.max_rec_latency * 1000)); + auto alternateStartTime = now; + + if (alternateBreakTime < breakTime) + { + breakTime = alternateBreakTime; + } + if (alternateStartTime < nominalLoopStartTime) + { + nominalLoopStartTime = alternateStartTime; + } } + + break; } } + // Determine start time by earliest obs time and try getting obs again + if (tsync == GTime::noTime()) + { + tsync = firstObsTime.floorTime(acsConfig.epoch_interval); + + acsConfig.start_epoch = tsync.to_posixTime(); + + BOOST_LOG_TRIVIAL(warning) + << "Start epoch not configured, rounding down first obs time " << firstObsTime + << ", epoch_interval=" << acsConfig.epoch_interval + << ", start_epoch=" << tsync.to_string(6); + + repeat = true; + } + + if (repeat) + { + sleep_for(std::chrono::milliseconds(acsConfig.sleep_milliseconds)); + } } + BOOST_LOG_TRIVIAL(debug) << "Epoch data handling done with " << attempt << " attempt(s)\n"; + if (complete) { break; @@ -1200,6 +1578,107 @@ int main(int argc, char** argv) continue; } + if (dataAvailableMap.empty() && acsConfig.require_obs && hasProcessedRealtimeEpoch && + epoch > 1) + { + bool sawRealtimeObsStream = false; + long long earliestReconnectUnixTime = 0; + + for (auto& [id, streamParser_ptr] : streamParserMultimap) + { + ObsStream* obsStream_ptr = nullptr; + + try + { + obsStream_ptr = &dynamic_cast(*streamParser_ptr); + } + catch (std::bad_cast&) + { + continue; + } + + auto& obsStream = *obsStream_ptr; + + if (obsStream.isPseudoRec) + { + continue; + } + + auto* tcpSocket = dynamic_cast(&obsStream.stream); + if (tcpSocket == nullptr) + { + continue; + } + + sawRealtimeObsStream = true; + + long long nextReconnectUnixTime = tcpSocket->nextReconnectUnixTime(); + if (nextReconnectUnixTime > 0 && + (earliestReconnectUnixTime == 0 || + nextReconnectUnixTime < earliestReconnectUnixTime)) + { + earliestReconnectUnixTime = nextReconnectUnixTime; + } + } + + if (sawRealtimeObsStream) + { + if (holdingReconnectOutage == false) + { + holdingReconnectOutage = true; + reconnectOutageStart = system_clock::now(); + lastOutageStatusLog = reconnectOutageStart - 1s; + BOOST_LOG_TRIVIAL(info) << "Realtime outage hold active, keeping tsync at " + << tsync << " until realtime observations resume"; + } + + auto now = system_clock::now(); + + if (now - lastOutageStatusLog >= 1s) + { + auto outageElapsed = std::chrono::duration_cast( + now - reconnectOutageStart + ); + BOOST_LOG_TRIVIAL(info) << "Realtime outage hold ongoing, tsync=" << tsync + << ", outage_elapsed=" << outageElapsed.count() << "s"; + lastOutageStatusLog = now; + } + + auto holdSleep = std::chrono::milliseconds(acsConfig.sleep_milliseconds); + + if (earliestReconnectUnixTime > 0) + { + auto earliestReconnectTime = + system_clock::from_time_t((time_t)earliestReconnectUnixTime); + + if (earliestReconnectTime > now) + { + holdSleep = std::min( + std::chrono::duration_cast( + earliestReconnectTime - now + ), + std::chrono::milliseconds(1000) + ); + } + } + + sleep_for(holdSleep); + nominalLoopStartTime = system_clock::now(); + lastEpochStartTime = timeGet(); + + continue; + } + } + else if (holdingReconnectOutage) + { + holdingReconnectOutage = false; + auto outageElapsed = std::chrono::duration_cast( + system_clock::now() - reconnectOutageStart + ); + BOOST_LOG_TRIVIAL(info) << "Realtime outage hold released at tsync " << tsync + << " after " << outageElapsed.count() << "s"; + } + if (tsync == GTime::noTime()) { if (acsConfig.require_obs) @@ -1207,15 +1686,97 @@ int main(int argc, char** argv) tsync = timeGet().floorTime(acsConfig.epoch_interval); - acsConfig.start_epoch = boost::posix_time::from_time_t((time_t)((PTime)tsync).bigTime); + acsConfig.start_epoch = tsync.to_posixTime(); } - BOOST_LOG_TRIVIAL(info) << "Synced " << dataAvailableMap.size() << " receivers..."; + lastProcStartTime = timeGet(); + + bool realtimeProcessing = acsConfig.simulate_real_time || hasRealtimeObsInput; + + std::ostringstream syncSummary; + syncSummary << "Synced " << dataAvailableMap.size() << "/" << receiverMap.size() + << " receivers after " << attempt << " attempt(s)"; + + if (realtimeProcessing) + { + syncSummary << ", sync_now=" << lastProcStartTime + << ", sync_latency=" << (lastProcStartTime - tsync); + + if (readyLatencySeconds.empty() == false) + { + std::sort(readyLatencySeconds.begin(), readyLatencySeconds.end()); + + auto percentile = [&](double p) + { + size_t index = (size_t)std::floor(p * (readyLatencySeconds.size() - 1)); + return readyLatencySeconds[index]; + }; + + syncSummary << ", ready_latency_s[min/p25/p50/p75/max]=" << std::fixed + << std::setprecision(2) << readyLatencySeconds.front() << "/" + << percentile(0.25) << "/" << percentile(0.50) << "/" + << percentile(0.75) << "/" << readyLatencySeconds.back(); + } + + BOOST_LOG_TRIVIAL(info) << syncSummary.str(); + } + else if (attempt > 1 || dataAvailableMap.size() < receiverMap.size()) + { + BOOST_LOG_TRIVIAL(info) << syncSummary.str(); + } + else + { + BOOST_LOG_TRIVIAL(debug) << syncSummary.str(); + } + + for (auto& [id, rec] : receiverMap) + { + if (dataAvailableMap.find(id) != dataAvailableMap.end()) + { + latestMissingObsStatusByReceiver.erase(id); + continue; + } + + auto it = missingObsReasons.find(id); + if (it == missingObsReasons.end()) + { + latestMissingObsStatusByReceiver.erase(id); + continue; + } + + std::ostringstream missingDiagnostic; + missingDiagnostic << "Receiver " << id << " not-ready diagnostic: attempts=" << attempt + << ", sync_reason=" << it->second + << ", sync_latency=" << (lastProcStartTime - tsync); + + auto sourceIt = missingObsSources.find(id); + if (sourceIt != missingObsSources.end() && sourceIt->second.empty() == false) + { + missingDiagnostic << ", source=" << sourceIt->second; + } + + auto stateIt = missingObsStates.find(id); + if (stateIt != missingObsStates.end() && stateIt->second.empty() == false) + { + latestMissingObsStatusByReceiver[id] = stateIt->second; + missingDiagnostic << ", state={" << stateIt->second << "}"; + } + else + { + latestMissingObsStatusByReceiver.erase(id); + } + + BOOST_LOG_TRIVIAL(debug) << missingDiagnostic.str(); + } - lastEpochStartTime = timeGet(); if (acsConfig.require_obs == false || dataAvailableMap.empty() == false) { mainOncePerEpoch(pppNet, ionNet, receiverMap, tsync); + + if (acsConfig.require_obs && dataAvailableMap.empty() == false) + { + hasProcessedRealtimeEpoch = true; + } } lastEpochStopTime = timeGet(); diff --git a/src/cpp/pea/minimumConstraints.cpp b/src/cpp/pea/minimumConstraints.cpp index 38349175e..e7c24575d 100644 --- a/src/cpp/pea/minimumConstraints.cpp +++ b/src/cpp/pea/minimumConstraints.cpp @@ -219,7 +219,7 @@ void mincon( if (acsConfig.mincon_only) { - long int startPos = -1; + std::streamoff startPos = -1; E_SerialObject type = getFilterTypeFromFile(startPos, acsConfig.mincon_filename); } @@ -309,8 +309,8 @@ void mincon( aprioriPos = rec.minconApriori; aprioriPosVar = rec.aprioriPosVar * SQR(recOpts.mincon_scale_apriori_sigma); filterVar = kfStateStations.P.block(index, index, 3, 3) * - SQR(recOpts.mincon_scale_filter_sigma); - str = rec.id; + SQR(recOpts.mincon_scale_filter_sigma); + str = rec.id; hasStations = true; } @@ -329,8 +329,8 @@ void mincon( aprioriPos = satNav.aprioriPos; aprioriPosVar = Matrix3d::Identity() * SQR(satOpts.mincon_scale_apriori_sigma); filterVar = kfStateStations.P.block(index, index, 3, 3) * - SQR(satOpts.mincon_scale_filter_sigma); - str = key.Sat.id(); + SQR(satOpts.mincon_scale_filter_sigma); + str = key.Sat.id(); hasSatellites = true; } @@ -508,7 +508,7 @@ void mincon( double value = deltaR(xyz); meas.setValue(value); - // todo Eugene: set noise + // todo? set noise // Add null measurement and continue, its needed for inverse later @@ -1029,7 +1029,7 @@ void outputMinconStatistics(Trace& trace, MinconStatistics& minconStatistics, co KFState minconOnly(Trace& trace, ReceiverMap& receiverMap) { - long int startPos = -1; + std::streamoff startPos = -1; E_SerialObject type = getFilterTypeFromFile(startPos, acsConfig.mincon_filename); if (type != E_SerialObject::FILTER_PLUS) { diff --git a/src/cpp/pea/outputs.cpp b/src/cpp/pea/outputs.cpp index 26da9c84b..e21c04e1e 100644 --- a/src/cpp/pea/outputs.cpp +++ b/src/cpp/pea/outputs.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "architectureDocs.hpp" #include "common/acsConfig.hpp" #include "common/algebraTrace.hpp" @@ -38,6 +39,7 @@ using boost::date_time::not_a_date_time; using std::max; +using std::unordered_set; using std::this_thread::sleep_for; Output Outputs__() @@ -192,6 +194,15 @@ void createTracefiles(ReceiverMap& receiverMap, Network& pppNet, Network& ionNet { boost::posix_time::ptime logptime = currentLogptime(); createDirectories(logptime); + unordered_set activeTraceFilenames; + + auto addActiveTraceFilename = [&](const string& filename) + { + if (filename.empty() == false) + { + activeTraceFilenames.insert(filename); + } + }; startNewMongoDb( "PRIMARY", @@ -603,13 +614,16 @@ void createTracefiles(ReceiverMap& receiverMap, Network& pppNet, Network& ionNet if (acsConfig.output_decoded_rtcm_json) { - createNewTraceFile( + bool changed = createNewTraceFile( id, rtcmParser.rtcmMountpoint, logptime, acsConfig.decoded_rtcm_json_filename, rtcmParser.rtcmTraceFilename ); + + if (changed) + rtcmParser.openTraceFile(); } for (auto nav : {false, true}) @@ -635,13 +649,20 @@ void createTracefiles(ReceiverMap& receiverMap, Network& pppNet, Network& ionNet else filename = acsConfig.rtcm_obs_filename; - createNewTraceFile( + string nextRecordFilename = rtcmParser.recordFilename; + + bool changed = createNewTraceFile( id, rtcmParser.rtcmMountpoint, logptime, filename, - rtcmParser.recordFilename + nextRecordFilename ); + + if (changed) + { + rtcmParser.setRecordFilename(nextRecordFilename); + } } } } @@ -708,6 +729,39 @@ void createTracefiles(ReceiverMap& receiverMap, Network& pppNet, Network& ionNet catch (std::bad_cast& e) { /* Ignore expected bad casts for different types */ } + + for (auto& [Sat, satNav] : nav.satNavMap) + { + if (acsConfig.output_satellite_trace) + { + addActiveTraceFilename(satNav.traceFilename); + } + } + + for (auto& [id, rec] : receiverMap) + { + if (acsConfig.output_receiver_trace) + { + addActiveTraceFilename(rec.traceFilename); + } + + if (acsConfig.output_json_trace) + { + addActiveTraceFilename(rec.jsonTraceFilename); + } + } + + if (acsConfig.output_network_trace) + { + addActiveTraceFilename(pppNet.traceFilename); + } + + if (acsConfig.output_ionosphere_trace) + { + addActiveTraceFilename(ionNet.traceFilename); + } + + retainTraceFiles(activeTraceFilenames); } void outputPredictedStates(Trace& trace, KFState& kfState) @@ -1025,11 +1079,8 @@ void perEpochPostProcessingAndOutputs( MinconStatistics minconStatistics; - mincon( - pppTrace, - augmentedKF, - &minconStatistics - ); // todo aaron, orbits apriori need etting + mincon(pppTrace, augmentedKF, + &minconStatistics); // todo? orbits apriori need etting augmentedKF.outputStates(pppTrace, "/CONSTRAINED" + _RTS); diff --git a/src/cpp/pea/pea_snx.cpp b/src/cpp/pea/pea_snx.cpp index 7e041919e..d9dcb6ea9 100644 --- a/src/cpp/pea/pea_snx.cpp +++ b/src/cpp/pea/pea_snx.cpp @@ -17,6 +17,17 @@ using boost::algorithm::to_lower_copy; void getStationsFromSinex(map& receiverMap, KFState& kfState) {} +static string resolvedReceiverType(const Receiver& rec) +{ + return rec.metadata.receiverType.valid ? rec.metadata.receiverType.value : rec.receiverType; +} + +static string resolvedAntennaType(const Receiver& rec) +{ + return rec.metadata.antennaDescriptor.valid ? rec.metadata.antennaDescriptor.value + : rec.antennaType; +} + void sinexPostProcessing(GTime time, map& receiverMap, KFState& netKFState) { theSinex.inputFiles.clear(); @@ -48,7 +59,7 @@ void sinexPostProcessing(GTime time, map& receiverMap, KFState // Add other statistics as they become available... sinexAddStatistic("SAMPLING INTERVAL (SECONDS)", acsConfig.epoch_interval); - char obsCode = 'P'; // GNSS measurements + char obsCode = 'P'; // GNSS measurements // Eugene: SLR? char constCode = ' '; string solcont = "ST"; @@ -58,8 +69,9 @@ void sinexPostProcessing(GTime time, map& receiverMap, KFState string data_agc = ""; PTime startTime; - startTime.bigTime = boost::posix_time::to_time_t(acsConfig.start_epoch - ); // todo aaron, make these constructors for ptime. + startTime.bigTime = boost::posix_time::to_time_t( + acsConfig.start_epoch + ); // todo? make these constructors for ptime. KFState sinexSubstate = mergeFilters({&netKFState}, {KF::ONE, KF::REC_POS, KF::REC_POS_RATE}); @@ -79,107 +91,94 @@ void sinexPostProcessing(GTime time, map& receiverMap, KFState replaceTimes(filename, acsConfig.start_epoch); - writeSinex(filename, sinexSubstate, receiverMap); + writeSinex(filename, sinexSubstate, receiverMap, (GTime)startTime, time); } -void sinexPerEpochPerStation(Trace& trace, GTime time, Receiver& rec) +void updateReceiverMetadata(GTime time, Receiver& rec) { if (rec.id.empty()) { return; } - { - auto& solEpoch = theSinex.solEpochMap[rec.id]; - - solEpoch.sitecode = rec.id; - solEpoch.typecode = '-'; - solEpoch.ptcode = "A"; - solEpoch.solnnum = "0"; - if ((GTime)solEpoch.start == GTime::noTime()) - solEpoch.start = time; - solEpoch.end = time; - solEpoch.mean = - (GTime)solEpoch.start + ((GTime)solEpoch.end - (GTime)solEpoch.start).to_double() / 2; - } - - // check the station data for currency. If later that the end time, refresh Sinex data - UYds yds = time; - UYds defaultStop(-1, -1, -1); + rec.failureEccentricity = true; - if (rec.snx.stop > yds && rec.snx.stop > defaultStop) + // Try config first + auto& recOpts = acsConfig.getRecOpts(rec.id); { - // already have valid data - return; - } - - string snxId = rec.id; + rec.metadata.ingestConfig(recOpts); + syncReceiverMetadata(rec); - if (cdpIdMap.find(rec.id) != cdpIdMap.end()) - { - // need to use CDP ID for SLR stations if possible - int cdpId = cdpIdMap.at(rec.id); - assert(cdpId >= 1000); // if fails, need to consider zero-padding in sinex files - snxId = std::to_string(cdpId); + rec.failureEccentricity = + recOpts.eccentricityModel.enable && rec.metadata.antennaDelta.valid == false; + rec.failureAprioriPos = rec.metadata.stationPosition.valid == false; } - rec.failureEccentricity = rec.antDelta.isZero(); - - auto& recOpts = acsConfig.getRecOpts(rec.id); + // Try sinex if anything not found from config + if (rec.failureEccentricity || resolvedReceiverType(rec).empty() || + resolvedAntennaType(rec).empty() || rec.failureAprioriPos) { - auto& eccModel = recOpts.eccentricityModel; - if (rec.antDelta.isZero() && eccModel.enable) + if ((GTime)rec.snx.stop < time || rec.snx.stop == UYds(0, 0, 0)) { - rec.antDelta = recOpts.eccentricityModel.eccentricity; - rec.failureEccentricity = false; + string snxId = rec.id; + if (cdpIdMap.find(rec.id) != cdpIdMap.end()) + { + // need to use CDP ID for SLR stations if possible + int cdpId = cdpIdMap.at(rec.id); + assert(cdpId >= 1000); // if fails, need to consider zero-padding in sinex files + snxId = std::to_string(cdpId); + } + + auto result = getRecSnx( + snxId, + time, + rec.snx + ); + rec.failureSinex = result.failureSiteId; } - if (rec.antennaType.empty()) - rec.antennaType = recOpts.antenna_type; - if (rec.receiverType.empty()) - rec.receiverType = recOpts.receiver_type; - } - string refSys = "UNE"; - auto result = getRecSnx(snxId, time, rec.snx); - if (!result.failureSiteId) - { - if (rec.antDelta.isZero() && rec.snx.ecc_ptr != nullptr) + if (rec.failureSinex == false) { - rec.antDelta = rec.snx.ecc_ptr->ecc; - refSys = rec.snx.ecc_ptr->rs; - rec.failureEccentricity = false; - } - if (rec.antennaType.empty() && rec.snx.ant_ptr != nullptr) - rec.antennaType = rec.snx.ant_ptr->type; - if (rec.receiverType.empty() && rec.snx.rec_ptr != nullptr) - rec.receiverType = rec.snx.rec_ptr->type; - } + rec.metadata.ingestSinex(rec.snx); + syncReceiverMetadata(rec); - if (result.failureSiteId) - { - rec.failureSinex = true; + rec.failureEccentricity = + recOpts.eccentricityModel.enable && rec.metadata.antennaDelta.valid == false; + rec.failureAprioriPos = rec.metadata.stationPosition.valid == false; + } } +} - if (result.failureEstimate && recOpts.apriori_pos.isZero()) +void sinexPerEpochPerStation(Trace& trace, GTime time, Receiver& rec) +{ + if (rec.id.empty()) { - rec.failureAprioriPos = true; + return; } - if (refSys != "UNE") { - rec.failureEccentricity = true; + // Eugene: Delete this? + auto& solEpoch = theSinex.solEpochMap[rec.id]; - BOOST_LOG_TRIVIAL( - error - ) << "Receiver eccentricity referency system != UNE"; // todo aaron, this needs - // duplication elsewhere, rs - // unchecked + solEpoch.sitecode = rec.id; + solEpoch.ptcode = "A"; + solEpoch.solnnum = "1"; + solEpoch.typecode = 'P'; // GPS by default // Eugene: SLR? + if ((GTime)solEpoch.start == GTime::noTime()) + solEpoch.start = time; + solEpoch.end = time; + solEpoch.mean = + (GTime)solEpoch.start + ((GTime)solEpoch.end - (GTime)solEpoch.start).to_double() / 2; } - if (rec.receiverType.empty() == false) + updateReceiverMetadata(time, rec); + + // Update receiver options + string receiverType = resolvedReceiverType(rec); + if (receiverType.empty() == false) { - string receiverType = to_lower_copy(rec.receiverType); - receiverType = receiverType.substr(0, receiverType.find(" ")); + receiverType = to_lower_copy(receiverType); + receiverType = receiverType.substr(0, receiverType.find(" ")); auto [it, inserted] = acsConfig.customAliasesMap[rec.id].insert(receiverType); if (inserted) @@ -193,19 +192,17 @@ void sinexPerEpochPerStation(Trace& trace, GTime time, Receiver& rec) } } - // Initialise the receiver antenna information - for (bool once : {1}) + // Initialise the antenna information { - string nullstring = ""; - string tmpant = rec.antennaType; + string antennaType = resolvedAntennaType(rec); + string tmpant = antennaType; if (tmpant.empty()) { - trace << "Antenna name not specified" << rec.id << ": Antenna name not specified"; - - rec.failureAntenna = true; + BOOST_LOG_TRIVIAL(warning) << "Antenna name not specified for " << rec.id; + trace << "Antenna name not specified for " << rec.id << "\n"; - break; + return; } bool found; @@ -214,7 +211,7 @@ void sinexPerEpochPerStation(Trace& trace, GTime time, Receiver& rec) { // all good, carry on rec.antennaId = tmpant; - break; + return; } // Try searching under the antenna type with DOME => NONE @@ -223,15 +220,16 @@ void sinexPerEpochPerStation(Trace& trace, GTime time, Receiver& rec) found = findAntenna(tmpant, E_Sys::GPS, time, nav, F1); if (found) { - trace << "Using '" << tmpant << "' instead of: '" << rec.antennaType - << "' for radome of " << rec.id; + BOOST_LOG_TRIVIAL(warning) << "Using '" << tmpant << "' instead of: '" << antennaType + << "' for radome of " << rec.id; + trace << "Using '" << tmpant << "' instead of: '" << antennaType << "' for radome of " + << rec.id << "\n"; rec.antennaId = tmpant; - break; + return; } - trace << "No information for antenna " << rec.antennaType; - - rec.failureAntenna = true; + BOOST_LOG_TRIVIAL(warning) << "No information for antenna " << antennaType; + trace << "No information for antenna " << antennaType << "\n"; } } diff --git a/src/cpp/pea/ppp.cpp b/src/cpp/pea/ppp.cpp index 6b4f36300..bbed5b52f 100644 --- a/src/cpp/pea/ppp.cpp +++ b/src/cpp/pea/ppp.cpp @@ -368,24 +368,64 @@ void updateAprioriRecPos( KFState* remote_ptr ) { + auto useResolvedMetadataStationPosition = [&](E_Source& foundSource) -> bool + { + if (rec.metadata.stationPosition.valid == false) + { + return false; + } + + rec.aprioriPos = rec.metadata.stationPosition.value; + foundSource = E_Source::META; + + if (rec.metadata.stationPosition.winningSource == E_ReceiverMetaSource::SINEX && + rec.snx.pos.isZero() == false) + { + rec.primaryApriori = rec.snx.primary; + rec.aprioriTime = rec.snx.start; + } + + return true; + }; + E_Source foundSource = E_Source::NONE; for (auto source : recOpts.posModel.sources) { switch (source) { + case E_Source::META: + { + if (useResolvedMetadataStationPosition(foundSource)) + { + break; + } + + continue; + } case E_Source::CONFIG: { + if (useResolvedMetadataStationPosition(foundSource)) + { + break; + } + if (recOpts.apriori_pos.isZero()) { continue; } rec.aprioriPos = recOpts.apriori_pos; + foundSource = E_Source::CONFIG; break; } case E_Source::PRECISE: { + if (useResolvedMetadataStationPosition(foundSource)) + { + break; + } + if (rec.snx.pos.isZero()) { continue; @@ -393,10 +433,8 @@ void updateAprioriRecPos( rec.aprioriPos = rec.snx.pos; rec.primaryApriori = rec.snx.primary; - for (int i = 0; i < 3; i++) - { - rec.aprioriTime[i] = rec.snx.start[i]; - } + rec.aprioriTime = rec.snx.start; + foundSource = E_Source::PRECISE; break; } @@ -424,6 +462,7 @@ void updateAprioriRecPos( continue; } + foundSource = E_Source::REMOTE; break; } case E_Source::SPP: @@ -436,7 +475,8 @@ void updateAprioriRecPos( rec.aprioriTime = rec.sol.sppTime; rec.aprioriPos = rec.sol.sppPos; - sppUsed = true; + sppUsed = true; + foundSource = E_Source::SPP; break; } @@ -459,8 +499,10 @@ void updateAprioriRecPos( } } - foundSource = source; - break; + if (foundSource != E_Source::NONE) + { + break; + } } if (foundSource == E_Source::NONE) @@ -477,6 +519,30 @@ void updateAprioriRecPos( rec.aprioriPos.y(), rec.aprioriPos.z() ); + + string receiverType = + rec.metadata.receiverType.valid ? rec.metadata.receiverType.value : ""; + string antennaType = rec.metadata.antennaDescriptor.valid ? rec.metadata.antennaDescriptor.value + : ""; + Vector3d antennaEccentricity = + rec.metadata.antennaDelta.valid ? rec.metadata.antennaDelta.value : Vector3d::Zero(); + + tracepdeex( + 4, + trace, + "\nReceiver metadata:" + " receiver_type[%s]='%s'" + " antenna_type[%s]='%s'" + " antenna_eccentricity[%s]=%f %f %f", + enum_to_string(rec.metadata.receiverType.winningSource), + receiverType.c_str(), + enum_to_string(rec.metadata.antennaDescriptor.winningSource), + antennaType.c_str(), + enum_to_string(rec.metadata.antennaDelta.winningSource), + antennaEccentricity.x(), + antennaEccentricity.y(), + antennaEccentricity.z() + ); } void updateAprioriRecClk( @@ -555,7 +621,7 @@ void updateAprioriRecClk( } rec.aprioriClk = rec.sol.sppClk; - rec.aprioriClkVar = SQR(30); // todo Eugene: use estimated var + rec.aprioriClkVar = SQR(30); // todo? use estimated var break; } @@ -626,7 +692,7 @@ void selectAprioriSource( Matrix3d varianceXYZ = E.transpose() * enuNoise * E; - rec.aprioriPosVar = varianceXYZ; // todo Eugene: use estimated var for SPP + rec.aprioriPosVar = varianceXYZ; // todo? use estimated var for SPP } else { @@ -667,7 +733,7 @@ void addRejectDetails( tracepdeex( 0, trace, - "\n%s\t%-24s\t- %7s\t%s", + "\n%s\t%-24s\t- %-7s\t%s", time.to_string().c_str(), action.c_str(), reason.c_str(), @@ -680,7 +746,7 @@ void addRejectDetails( if (detail.isBool() == false) { - tracepdeex(0, trace, ": %s", detail.value().c_str()); + tracepdeex(0, trace, ": %12s", detail.value().c_str()); } }; @@ -824,7 +890,7 @@ void removeBadAmbiguities( E_FType ft = (E_FType)key.num; preprocSigName = - ft2string(ft); // todo aaron, is this redundant now that network is gone? + ft2string(ft); // todo? is this redundant now that network is gone? sigName = preprocSigName; } @@ -857,8 +923,8 @@ void removeBadAmbiguities( if (preprocSigStat.savedSlip.any && ((acsConfig.ambErrors.resetOnSlip.LLI && preprocSigStat.savedSlip.LLI) || (acsConfig.ambErrors.resetOnSlip.retrack && preprocSigStat.savedSlip.retrack) || - (acsConfig.ambErrors.resetOnSlip.single_freq && preprocSigStat.savedSlip.singleFreq - ) || + (acsConfig.ambErrors.resetOnSlip.single_freq && + preprocSigStat.savedSlip.singleFreq) || (acsConfig.ambErrors.resetOnSlip.GF && preprocSigStat.savedSlip.GF) || (acsConfig.ambErrors.resetOnSlip.MW && preprocSigStat.savedSlip.MW) || (acsConfig.ambErrors.resetOnSlip.SCDIA && preprocSigStat.savedSlip.SCDIA))) @@ -1201,8 +1267,7 @@ void outputPppNmea(Trace& trace, KFState& kfState, string id) // sqrt(phase_biasVar)); // } - if (key.type == KF::TROP // todo aaron needs iteration - && key.str == id) + if (key.type == KF::TROP && key.str == id) // todo? needs iteration { string grad; double trop = 0; diff --git a/src/cpp/pea/ppp_ambres.cpp b/src/cpp/pea/ppp_ambres.cpp index 71fd434c7..855081ee3 100644 --- a/src/cpp/pea/ppp_ambres.cpp +++ b/src/cpp/pea/ppp_ambres.cpp @@ -264,7 +264,7 @@ bool queryBiasUC( kfKey.Sat = Sat; kfKey.num = static_cast(code); - if (Sat.prn == 0) // todo aaron, check if needed and reverse logic + if (Sat.prn == 0) // todo? check if needed and reverse logic { auto& recOpts = acsConfig.getRecOpts(rec, {Sat.sysName(), enum_to_string(code)}); diff --git a/src/cpp/pea/ppp_callbacks.cpp b/src/cpp/pea/ppp_callbacks.cpp index e664df7a3..d9163b486 100644 --- a/src/cpp/pea/ppp_callbacks.cpp +++ b/src/cpp/pea/ppp_callbacks.cpp @@ -22,8 +22,7 @@ bool deweightMeas(RejectCallbackDetails rejectDetails) if (acsConfig.measErrors.enable == false) { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Bad measurement detected but `meas_deweighting` not enabled"; + BOOST_LOG_TRIVIAL(warning) << "Bad measurement detected but `meas_deweighting` not enabled"; return true; } @@ -38,7 +37,7 @@ bool deweightMeas(RejectCallbackDetails rejectDetails) string description; if (stage == E_FilterStage::LSQ) { - description = "Least Squares"; + description = "LeastSquares"; residual = kfMeas.VV(measIndex); } else if (stage == E_FilterStage::PREFIT) @@ -77,7 +76,7 @@ bool deweightMeas(RejectCallbackDetails rejectDetails) { GObs& obs = *obs_ptr; - obs.excludeOutlier = true; // todo Eugene: exclude signal instead of obs + obs.excludeOutlier = true; // todo? exclude signal instead of obs trace << "\n" << obs.Sat.id() << " will be excluded next SPP iteration"; } @@ -136,8 +135,32 @@ bool deweightStationMeas(RejectCallbackDetails rejectDetails) auto& measIndex = rejectDetails.measIndex; auto& stage = rejectDetails.stage; + if (acsConfig.measErrors.enable == false) + { + BOOST_LOG_TRIVIAL(warning) + << "Bad station measurement detected but `meas_deweighting` not enabled"; + + return true; + } + + double deweightFactor = acsConfig.measErrors.deweight_factor; + string id = kfMeas.obsKeys[measIndex].str; + string description; + if (stage == E_FilterStage::LSQ) + { + description = "LeastSquares"; + } + else if (stage == E_FilterStage::PREFIT) + { + description = "Prefit"; + } + else if (stage == E_FilterStage::POSTFIT) + { + description = "Postfit"; + } + for (int i = 0; i < kfMeas.obsKeys.size(); i++) { auto& key = kfMeas.obsKeys[i]; @@ -147,22 +170,6 @@ bool deweightStationMeas(RejectCallbackDetails rejectDetails) continue; } - double deweightFactor = acsConfig.measErrors.deweight_factor; - - string description; - if (stage == E_FilterStage::LSQ) - { - description = "Least Squares"; - } - else if (stage == E_FilterStage::PREFIT) - { - description = "Prefit"; - } - else if (stage == E_FilterStage::POSTFIT) - { - description = "Postfit"; - } - addRejectDetails(kfState.time, trace, kfState, key, "Station Meas Deweighted", description); kfMeas.R.row(i) *= deweightFactor; @@ -450,7 +457,7 @@ bool rejectAllMeasByState(RejectCallbackDetails rejectDetails) auto& stateIndex = rejectDetails.stateIndex; trace << "\n" - << "Bad state detected " << kfKey << " - rejecting all referencing measurements" << "\n"; + << "Bad state detected: " << kfKey << " - rejecting all referencing measurements" << "\n"; for (int measIndex = 0; measIndex < kfMeas.H.rows(); measIndex++) { @@ -596,26 +603,28 @@ bool relaxState(RejectCallbackDetails rejectDetails) if (acsConfig.stateErrors.enable == false) { - BOOST_LOG_TRIVIAL(warning) - << "Warning: Bad state detected but `state_deweighting` not enabled"; + BOOST_LOG_TRIVIAL(warning) << "Bad state detected but `state_deweighting` not enabled"; return true; } + string description; double deweightFactor = 1; if (stage == E_FilterStage::PREFIT) { + description = "Prefit"; deweightFactor = abs(kfState.prefitRatios(stateIndex)); } else if (stage == E_FilterStage::POSTFIT) { + description = "Postfit"; deweightFactor = abs(kfState.postfitRatios(stateIndex)); } // deweightFactor = std::min(abs(deweightFactor), 5000.0); // To avoid breaking // filter, maximum process noise allowed is 5000 times of prefit sigma trace << "\n" - << "Bad state detected " << kfKey << " - relaxing state"; + << "Bad state detected: " << kfKey << " - relaxing state"; kfState.statisticsMap["State rejection"]++; @@ -636,11 +645,21 @@ bool relaxState(RejectCallbackDetails rejectDetails) continue; } - double procNoise = deweightFactor * sqrt(kfState.P(index, index)); - - trace << "\n - Adding " << procNoise << " to sigma of " << key; - - Q(index, index) = SQR(procNoise); + double preSigma = sqrt(kfState.P(index, index)); + Q(index, index) = SQR(deweightFactor * preSigma); + double postSigma = sqrt(kfState.P(index, index) + Q(index, index)); + + addRejectDetails( + kfState.time, + trace, + kfState, + key, + "State Relaxed", + description, + {{"Adjustment", kfState.dx(index)}, + {"preTransitionSigma", preSigma}, + {"postTransitionSigma", postSigma}} + ); if (key.type == KF::ORBIT && key.num >= 3 && acsConfig.satelliteErrors.enable && acsConfig.satelliteErrors.vel_proc_noise_trail && @@ -658,14 +677,8 @@ bool relaxState(RejectCallbackDetails rejectDetails) if (transitionRequired) { - trace << "\n - Pre-transition state sigma for " << kfKey << ": " - << sqrt(kfState.P(stateIndex, stateIndex)); - kfState.manualStateTransition(trace, kfState.time, F, Q); - trace << "\n - Post-transition state sigma for " << kfKey << ": " - << sqrt(kfState.P(stateIndex, stateIndex)); - return false; } diff --git a/src/cpp/pea/ppp_obs.cpp b/src/cpp/pea/ppp_obs.cpp index 446ffd6dd..dfdd67075 100644 --- a/src/cpp/pea/ppp_obs.cpp +++ b/src/cpp/pea/ppp_obs.cpp @@ -338,7 +338,7 @@ inline static void pppSatClocks(COMMON_PPP_ARGS) -obs.satVel.dot(satStat.e) / CLIGHT, init ); // Changes in satellite position or geometric distance calculation due to adjustment of - // satellite clock offset + // satellite clock offset InitialState rateInit = initialStateFromConfig(satOpts.clk_rate, i); @@ -362,8 +362,9 @@ inline static void pppSatClocks(COMMON_PPP_ARGS) inline static void pppRecAntDelta(COMMON_PPP_ARGS) { - Vector3d bodyAntVector = rec.antDelta; - Vector3d bodyLook = ecef2body(rec.attStatus, satStat.e); + Vector3d bodyAntVector = + rec.metadata.antennaDelta.valid ? rec.metadata.antennaDelta.value : rec.antDelta; + Vector3d bodyLook = ecef2body(rec.attStatus, satStat.e); double variance = 0; @@ -399,7 +400,7 @@ inline static void pppRecAntDelta(COMMON_PPP_ARGS) measEntry.addDsgnEntry(kfKey, -bodyLook(i), init); } - // todo aaron needs noise + // todo? needs noise double recAntDelta = -bodyAntVector.dot(bodyLook); @@ -436,7 +437,7 @@ inline static void pppRecPCO(COMMON_PPP_ARGS) acsConfig.interpolate_rec_pco ); Vector3d bodyLook = - ecef2body(attStatus, satStat.e, &dEdQ); // todo aaron, move this to antDelta instead + ecef2body(attStatus, satStat.e, &dEdQ); // todo? move this to antDelta instead for (int i = 0; i < 3; i++) { @@ -484,7 +485,7 @@ inline static void pppRecPCO(COMMON_PPP_ARGS) measEntry.addDsgnEntry(kfKey, -bodyPCO.dot(dEdQ.col(i))); // Eugene: init? } - // todo aaron, needs noise + // todo? needs noise double recPCODelta = -bodyPCO.dot(bodyLook); @@ -560,7 +561,7 @@ inline static void pppSatPCO(COMMON_PPP_ARGS) {"modelYaw", attStatus.modelYaw}} ); - // todo aaron, needs noise + // todo? needs noise double satPCODelta = bodyPCO.dot(bodyLook); @@ -785,7 +786,7 @@ inline static void pppIonStec(COMMON_PPP_ARGS) measEntry.addDsgnEntry(kfKey, factor * alpha, init); } - // todo aaron, needs noise + // todo? needs noise measEntry.componentsMap[E_Component::IONOSPHERIC_COMPONENT] = { ionosphere_m, diff --git a/src/cpp/pea/ppp_pseudoobs.cpp b/src/cpp/pea/ppp_pseudoobs.cpp index 208e5a2a5..62718d392 100644 --- a/src/cpp/pea/ppp_pseudoobs.cpp +++ b/src/cpp/pea/ppp_pseudoobs.cpp @@ -835,7 +835,7 @@ void ambgPseudoObs(Trace& trace, KFState& kfState, KFMeasEntryList& kfMeasEntryL } } -void ionoPseudoObs( // todo aaron, move to model section +void ionoPseudoObs( // todo? move to model section Trace& pppTrace, ReceiverMap& receiverMap, KFState& kfState, @@ -863,7 +863,7 @@ void ionoPseudoObs( // todo aaron, move to model section satStat, extvar, obs.Sat - ); // todo aaron get from other sources too + ); // todo? get from other sources too if (extvar <= 0) continue; @@ -946,7 +946,7 @@ void tropPseudoObs( wetZTD, wetMap, extVar - ); // todo aaron, take this from other places optionally + ); // todo? take this from other places optionally if (extVar <= 0) continue; diff --git a/src/cpp/pea/ppp_slr.cpp b/src/cpp/pea/ppp_slr.cpp index dfb68505c..9502bacc0 100644 --- a/src/cpp/pea/ppp_slr.cpp +++ b/src/cpp/pea/ppp_slr.cpp @@ -93,7 +93,7 @@ inline void slrRelativity2(COMMON_PPP_ARGS) inline void slrSagnac(COMMON_PPP_ARGS) { double dSagnacOut = sagnac(rSat, rRec); - double dSagnacIn = sagnac(rRec, rSat); // todo aaron, is it that simple? look at area + double dSagnacIn = sagnac(rRec, rSat); // todo? is it that simple? look at area measEntry.componentsMap[E_Component::SAGNAC] = {dSagnacOut + dSagnacIn, "+ sag", 0}; } @@ -132,7 +132,9 @@ inline void slrTrop(COMMON_PPP_ARGS) inline void slrRecAntDelta(COMMON_PPP_ARGS) { - Vector3d recAntVector = body2ecef(rec.attStatus, rec.antDelta); + Vector3d antennaDelta = + rec.metadata.antennaDelta.valid ? rec.metadata.antennaDelta.value : rec.antDelta; + Vector3d recAntVector = body2ecef(rec.attStatus, antennaDelta); double recAntDelta = -recAntVector.dot(satStat.e); @@ -144,7 +146,7 @@ inline void slrRecAntDelta(COMMON_PPP_ARGS) inline void slrRecRangeBias(COMMON_PPP_ARGS) { double recRangeBias = obs.rangeBias; - double recRangeBiasVar = DEFAULT_RANG_BIAS_VAR; // todo Eugene: use actual var? + double recRangeBiasVar = DEFAULT_RANG_BIAS_VAR; // todo? use actual var? InitialState init = initialStateFromConfig(recOpts.slr_range_bias); @@ -183,7 +185,7 @@ inline void slrRecTimeBias(COMMON_PPP_ARGS) { // VectorXd recTimeBiasPartial = slrObs.satVel.transpose() * slrObs.e * 0.001; //ms double recTimeBias = obs.timeBias * CLIGHT; - double recTimeBiasVar = DEFAULT_TIME_BIAS_VAR; // todo Eugene: use actual var? + double recTimeBiasVar = DEFAULT_TIME_BIAS_VAR; // todo? use actual var? double recTimeBiasPartial = -obs.satVel.dot(satStat.e) / CLIGHT; InitialState init = initialStateFromConfig(recOpts.slr_time_bias); @@ -427,12 +429,7 @@ void receiverSlr( ) { measEntry.addDsgnEntry(posKey, +eSatInertial[i] * 2, posInit); - measEntry.addDsgnEntry( - velKey, - +eSatInertial[i] * tgap * 2, - velInit - ); // todo aaron, eugene copied this from ppp_obs, but i think it is not - // necessary (bad?) + measEntry.addDsgnEntry(velKey, +eSatInertial[i] * tgap * 2, velInit); } ); @@ -448,8 +445,7 @@ void receiverSlr( obsKey.Sat = obs.Sat; // obsKey.num = i; - measEntry - .addNoiseEntry(obsKey, 1, obs.ephVar); // todo aaron, need many more noise entries + measEntry.addNoiseEntry(obsKey, 1, obs.ephVar); // todo? need many more noise entries } // Range and geometry diff --git a/src/cpp/pea/ppppp.cpp b/src/cpp/pea/ppppp.cpp index c5dcf7387..e04b8f04c 100644 --- a/src/cpp/pea/ppppp.cpp +++ b/src/cpp/pea/ppppp.cpp @@ -21,7 +21,9 @@ #include "common/trace.hpp" #include "inertial/posProp.hpp" #include "iono/ionoModel.hpp" +#if defined(ENABLE_PARALLELISATION) || defined(_OPENMP) #include "omp.h" +#endif #include "orbprop/coordinates.hpp" #include "orbprop/orbitProp.hpp" #include "rtklib/lambda.h" @@ -1198,12 +1200,12 @@ KFState propagateUncertainty(Trace& trace, KFState& kfState) } void chunkFilter( - Trace& trace, - KFState& kfState, - KFMeas& kfMeas, - ReceiverMap& receiverMap, - map& filterChunkMap, - map& traceList + Trace& trace, + KFState& kfState, + KFMeas& kfMeas, + ReceiverMap& receiverMap, + map& filterChunkMap, + map& traceList ) { filterChunkMap.clear(); @@ -1342,7 +1344,7 @@ void chunkFilter( { auto& rec = recIt->second; traceList[str] = getTraceFile(rec); - filterChunk.trace_ptr = &traceList[str]; + filterChunk.trace_ptr = traceList[str].trace; } else { @@ -1574,7 +1576,7 @@ bool isSpecificTimeReset(double epoch, double prev_epoch, const std::vector reset_mod) || - (prev_epoch_mod > epoch_mod && reset_mod == 0); + (prev_epoch_mod > epoch_mod && reset_mod == 0); if (reset_epoch || reset_between_epochs) { @@ -1697,8 +1699,8 @@ void updateFilter( ) { removeBadSatellites(trace, - kfState); // todo Eugene: revisit this as it doesn't work well - removeBadReceivers(trace, kfState, receiverMap); // todo Eugene: revisit this as well + kfState); // todo? revisit this as it doesn't work well + removeBadReceivers(trace, kfState, receiverMap); // todo? revisit this as well removeBadAmbiguities(trace, kfState, receiverMap); removeBadIonospheres(trace, kfState); resetFilterbyConfig(trace, kfState); @@ -1895,8 +1897,8 @@ void ppp( kfState.outputStates(trace, suffix); } - map filterChunkMap; - map traceList; // keep in large scope as we're using pointers + map filterChunkMap; + map traceList; // keep in large scope as we're using pointers chunkFilter(trace, kfState, kfMeas, receiverMap, filterChunkMap, traceList); @@ -1915,6 +1917,7 @@ void ppp( kfState.outputStates( *filterChunk.trace_ptr, (string) "/PPPChunk/" + id, + -1, filterChunk.begX, filterChunk.numX ); diff --git a/src/cpp/pea/preprocessor.cpp b/src/cpp/pea/preprocessor.cpp index 06e9c65af..076c106c7 100644 --- a/src/cpp/pea/preprocessor.cpp +++ b/src/cpp/pea/preprocessor.cpp @@ -604,6 +604,93 @@ void obsVariances(ObsList& obsList) } } +void cleanSignals(ObsList& obsList) +{ + for (auto& obs : only(obsList)) + for (auto& [ftype, sigsList] : obs.sigsLists) + { + E_Sys sys = obs.Sat.sys; + + if (sys == E_Sys::GPS) + { + double dirty_C1W_phase = 0; + for (auto& sig : sigsList) + { + if (sig.code == E_ObsCode::L1C) + dirty_C1W_phase = sig.L; + + if (sig.code == E_ObsCode::L1W && sig.P == 0) + { + sig.L = 0; + } + } + + for (auto& sig : sigsList) + if (sig.code == E_ObsCode::L1W && sig.L == 0 && sig.P != 0) + { + sig.L = dirty_C1W_phase; + break; + } + } + sigsList.remove_if( + [sys](Sig& a) + { + return std::find( + acsConfig.code_priorities[sys].begin(), + acsConfig.code_priorities[sys].end(), + a.code + ) == acsConfig.code_priorities[sys].end(); + } + ); + sigsList.sort( + [sys](Sig& a, Sig& b) + { + auto iterA = std::find( + acsConfig.code_priorities[sys].begin(), + acsConfig.code_priorities[sys].end(), + a.code + ); + auto iterB = std::find( + acsConfig.code_priorities[sys].begin(), + acsConfig.code_priorities[sys].end(), + b.code + ); + + if (a.L == 0) + return false; + if (b.L == 0) + return true; + if (a.P == 0) + return false; + if (b.P == 0) + return true; + if (iterA < iterB) + return true; + else + return false; + } + ); + + if (sigsList.empty()) + { + continue; + } + + Sig firstOfType = sigsList.front(); + + // use first of type as representative if its in the priority list + auto iter = std::find( + acsConfig.code_priorities[sys].begin(), + acsConfig.code_priorities[sys].end(), + firstOfType.code + ); + if (iter != acsConfig.code_priorities[sys].end()) + { + obs.sigs[ftype] = Sig(firstOfType); + } + } +} + void excludeUnprocessed(ObsList& obsList) { for (auto& obs : only(obsList)) @@ -665,17 +752,19 @@ void preprocessor( { DOCS_REFERENCE(Preprocessing__); - if ((acsConfig.process_preprocessor == false) || - (acsConfig.preprocOpts.preprocess_all_data == true && realEpoch == true) || - (acsConfig.preprocOpts.preprocess_all_data == false && realEpoch == false)) + // Only preprocess once: either while reading data, or in main processing. + const bool handledByPreprocessor = + acsConfig.process_preprocessor && (acsConfig.preprocOpts.preprocess_all_data == realEpoch); + + // Without the preprocessor, still do basic preparation for real epochs. + const bool outsideProcessingEpoch = + (acsConfig.process_preprocessor == false && realEpoch == false); + + if (handledByPreprocessor || outsideProcessingEpoch) { return; } - auto jsonTrace = getTraceFile(rec, true); - - auto& recOpts = acsConfig.getRecOpts(rec.id); - auto& obsList = rec.obsList; if (obsList.empty()) @@ -683,22 +772,16 @@ void preprocessor( return; } - PTime startTime; - startTime.bigTime = boost::posix_time::to_time_t(acsConfig.start_epoch); - - double tol; - if (acsConfig.assign_closest_epoch) - tol = acsConfig.epoch_interval / 2; // todo aaron this should be the epoch_tolerance? - else - tol = 0.5; - GTime time = obsList.front()->time; - if (acsConfig.start_epoch.is_not_a_date_time() == false && time < (GTime)startTime - tol) + if (acsConfig.start_epoch.is_not_a_date_time() == false && + time < GTime(acsConfig.start_epoch) - acsConfig.epoch_tolerance) { return; } - getRecSnx(rec.id, time, rec.snx); + updateReceiverMetadata(time, rec); + + auto& recOpts = acsConfig.getRecOpts(rec.id); bool dummy; updateAprioriRecPos(trace, rec, recOpts, dummy, remote_ptr); @@ -760,12 +843,18 @@ void preprocessor( satazel(pos, satStat.e, satStat); } + if (acsConfig.process_preprocessor == false) + { + return; + } + clearSlips(obsList); excludeUnprocessed(obsList); if (acsConfig.output_observations) { + auto jsonTrace = getTraceFile(rec, true); outputObservations(trace, jsonTrace, obsList, rec, pos); } diff --git a/src/cpp/pea/preprocessor.hpp b/src/cpp/pea/preprocessor.hpp index bb1539ac3..9a44883e1 100644 --- a/src/cpp/pea/preprocessor.hpp +++ b/src/cpp/pea/preprocessor.hpp @@ -7,4 +7,5 @@ void preprocessor( KFState* kfState_ptr = nullptr, KFState* remote_ptr = nullptr ); -void obsVariances(ObsList& obsList); \ No newline at end of file +void obsVariances(ObsList& obsList); +void cleanSignals(ObsList& obsList); \ No newline at end of file diff --git a/src/cpp/pea/spp.cpp b/src/cpp/pea/spp.cpp index 366dfd592..d88e1dbcd 100644 --- a/src/cpp/pea/spp.cpp +++ b/src/cpp/pea/spp.cpp @@ -214,10 +214,10 @@ bool prange( bias = bias_A; biasVar = varBias_A; - bool dualFreq = (ionoMode == E_IonoMode::IONO_FREE_LINEAR_COMBO) || - (ionoMode == E_IonoMode::SBAS && acsConfig.sbsInOpts.freq == 5); - double c1 = 1; - double c2 = 0; + bool dualFreq = (ionoMode == E_IonoMode::IONO_FREE_LINEAR_COMBO) || + (ionoMode == E_IonoMode::SBAS && acsConfig.sbsInOpts.freq == 5); + double c1 = 1; + double c2 = 0; if (dualFreq) { @@ -688,7 +688,7 @@ E_Solution estpos( double dtSat = -obs.satClk * CLIGHT; double varSatClk = obs.satClkVar * SQR(CLIGHT); auto& satOpts = acsConfig.getSatOpts(obs.Sat); - if (satOpts.posModel.sources[0] == E_Source::SBAS) + if (satOpts.posModel.sources[0] == E_Source::SBAS && !acsConfig.sbsInOpts.pvs_on_dfmc) { double sbasVar = checkSBASVar(trace, obs.time, obs.Sat, rRec, rSat, obs.satNav_ptr->currentSBAS); @@ -810,7 +810,7 @@ E_Solution estpos( codeMeas.obsKey.Sat = obs.Sat; codeMeas.obsKey.str = id; codeMeas.obsKey.num = ft2 ? (static_cast(obs.sigs[ft1].code) * 100 + - static_cast(obs.sigs[ft2].code)) + static_cast(obs.sigs[ft2].code)) : static_cast(obs.sigs[ft1].code); codeMeas.obsKey.type = KF::CODE_MEAS; codeMeas.obsKey.comment = ""; @@ -822,7 +822,7 @@ E_Solution estpos( kfMeasEntryList.push_back(codeMeas); - obs.sppValid = true; // todo aaron, this is messy, lots of excludes dont work if spp + obs.sppValid = true; // todo? this is messy, lots of excludes dont work if spp // not run, harmonise the spp/ppp exclusion methods. obs.sppCodeResidual = res; } @@ -906,7 +906,7 @@ E_Solution estpos( kfState.outputStates(trace, suffix); } - if (kfState.chiSquareTest.enable) // todo Eugene: use meas chi-square test in algebra + if (kfState.chiSquareTest.enable) // todo? use meas chi-square test in algebra { double a = sqrt(kfState.P(1, 1) + kfState.P(2, 2) + kfState.P(3, 3)) * kfState.chi2PerDof; @@ -1258,8 +1258,8 @@ void spp( ); // Estimate receiver position with pseudorange - sol.status = estpos(trace, obsList, sol, id, kfState_ptr, (string) "SPP/" + id); // todo aaron, - // remote too? + sol.status = + estpos(trace, obsList, sol, id, kfState_ptr, (string) "SPP/" + id); // todo? // remote too? auto& sppState = sol.sppState; diff --git a/src/cpp/rtklib/rtkcmn.cpp b/src/cpp/rtklib/rtkcmn.cpp index 1ba44060d..1a2419ce1 100644 --- a/src/cpp/rtklib/rtkcmn.cpp +++ b/src/cpp/rtklib/rtkcmn.cpp @@ -120,7 +120,7 @@ double sagnac( vel = omega.cross(rDest); } - // todo aaron, check which vel is required for slr things, still dest on outward journey? + // todo? check which vel is required for slr things, still dest on outward journey? return (rDest - rSource).dot(vel) / CLIGHT; } diff --git a/src/cpp/sbas/decodeL1.cpp b/src/cpp/sbas/decodeL1.cpp index 725712430..000f5c985 100644 --- a/src/cpp/sbas/decodeL1.cpp +++ b/src/cpp/sbas/decodeL1.cpp @@ -617,8 +617,8 @@ void decodeL1GEO_Navg(Trace& trace, GTime frameTime, Navigation& nav, unsigned c seph.acc[0] = getbitsInc(data, ind, 10) * 0.0000125; seph.acc[1] = getbitsInc(data, ind, 10) * 0.0000125; seph.acc[2] = getbitsInc(data, ind, 10) * 0.000625; - seph.af0 = getbitsInc(data, ind, 12) * P2_31; - seph.af0 = getbitsInc(data, ind, 8) * P2_40; + seph.af0 = getbitsInc(data, ind, 12) * P2_31; + seph.af0 = getbitsInc(data, ind, 8) * P2_40; nav.sephMap[seph.Sat][seph.type][seph.t0] = seph; } diff --git a/src/cpp/sbas/sbas.cpp b/src/cpp/sbas/sbas.cpp index ec20d8c79..b7be78488 100644 --- a/src/cpp/sbas/sbas.cpp +++ b/src/cpp/sbas/sbas.cpp @@ -74,7 +74,7 @@ void writeEMSdata( { if (frameTime > lastEMSWritten) { - // todo aaron, use the standard file rotations + // todo? use the standard file rotations PTime pTime = frameTime; boost::posix_time::ptime otherPTime = boost::posix_time::from_time_t((time_t)pTime.bigTime); @@ -191,7 +191,7 @@ void loadSBASdata(Trace& trace, GTime time, Navigation& nav) auto& sbs = satDat.currentSBAS; for (auto it = sbs.slowUpdt.begin(); it != sbs.slowUpdt.end();) { - auto slowUpdt = it->second; + auto& slowUpdt = it->second; for (auto it2 = slowUpdt.begin(); it2 != slowUpdt.end();) { auto teph = it2->first; @@ -209,7 +209,7 @@ void loadSBASdata(Trace& trace, GTime time, Navigation& nav) for (auto it = sbs.fastUpdt.begin(); it != sbs.fastUpdt.end();) { - auto fastUpdt = it->second; + auto& fastUpdt = it->second; for (auto it2 = fastUpdt.begin(); it2 != fastUpdt.end();) { auto teph = it2->first; @@ -292,7 +292,7 @@ void writeSPP(string filename, Receiver& rec) std::ofstream output(filename, std::fstream::out | std::fstream::app); if (!output.is_open()) { - BOOST_LOG_TRIVIAL(warning) << "Warning: Error opening POS file '" << filename; + BOOST_LOG_TRIVIAL(warning) << "Error opening POS file '" << filename; return; } @@ -469,4 +469,4 @@ void checkForType0(GTime time, int type) } else if ((time - lastMessType0).to_double() > 10) sbasAlertNoSoL = false; -} \ No newline at end of file +} diff --git a/src/cpp/slr/slrObs.cpp b/src/cpp/slr/slrObs.cpp index d45d76f83..1ce3bcf4f 100644 --- a/src/cpp/slr/slrObs.cpp +++ b/src/cpp/slr/slrObs.cpp @@ -50,9 +50,9 @@ void readSatId(string filepath) ///< Filepath to sat ID file newSat.satName = satName; string satId = line.substr(25, 9); boost::algorithm::trim(satId); - newSat.satId = satId; - newSat.ilrsId = std::stoi(line.substr(34, 9) - ); // todo: check input strings are compatible with stoi() and stod(), e.g. white spaces + newSat.satId = satId; + // todo: check input strings are compatible with stoi() and stod(), e.g. white spaces + newSat.ilrsId = std::stoi(line.substr(34, 9)); newSat.noradId = std::stoi(line.substr(43, 9)); newSat.altitude[0] = std::stod(line.substr(52, 9)); newSat.altitude[1] = std::stod(line.substr(61, 9)); diff --git a/src/cpp/trop/tropCSSR.cpp b/src/cpp/trop/tropCSSR.cpp index 708f92552..452422372 100644 --- a/src/cpp/trop/tropCSSR.cpp +++ b/src/cpp/trop/tropCSSR.cpp @@ -71,7 +71,7 @@ double tropCSSR( break; case 3: dryZTD += val * dLat * dLon; - break; // todo aaron magic numbers + break; // todo? magic numbers } tracepdeex(2, trace, ", %.4f\n", val); } diff --git a/src/cpp/trop/tropModels.cpp b/src/cpp/trop/tropModels.cpp index e46cb20e6..92669f012 100644 --- a/src/cpp/trop/tropModels.cpp +++ b/src/cpp/trop/tropModels.cpp @@ -114,9 +114,8 @@ double tropModelCoef(int ind, VectorPos& pos) if (dlonDeg > atmReg.intLonDeg || atmReg.intLonDeg == 0) return 0; - return (1 - dlatDeg / atmReg.intLatDeg) * - (1 - dlonDeg / atmReg.intLonDeg - ); // todo aaron use bilinear interpolation function? + return (1 - dlatDeg / atmReg.intLatDeg) * (1 - dlonDeg / atmReg.intLonDeg); + // todo? use bilinear interpolation function? } default: { @@ -251,7 +250,7 @@ double tropModel( break; } - // todo aaron var might still be < 0 if everything in models failed + // todo? var might still be < 0 if everything in models failed if (tropStates.zenith == 0) // initialization { @@ -312,7 +311,7 @@ double tropDryZTD(Trace& trace, vector models, GTime time, VectorPo break; } - // todo aaron var might still be < 0 if everything in models failed + // todo? var might still be < 0 if everything in models failed return dryZTD; } diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt new file mode 100644 index 000000000..6b2cfa1da --- /dev/null +++ b/src/tests/CMakeLists.txt @@ -0,0 +1,107 @@ +set(UNIT_TEST_INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/../cpp + ${EIGEN3_INCLUDE_DIRS} + ${Boost_INCLUDE_DIRS} +) + +function(configure_unit_test target) + target_include_directories(${target} PRIVATE + ${UNIT_TEST_INCLUDE_DIRS} + ) + + if(OpenMP_CXX_FOUND) + target_link_libraries(${target} PRIVATE OpenMP::OpenMP_CXX) + endif() + + add_test(NAME ${target} COMMAND ${target}) +endfunction() + +set(RECEIVER_METADATA_TEST_SOURCES + ../cpp/common/receiverMetadata.cpp + unit/test_ReceiverMetaData.cpp +) + +set(SANITY_CHECKER_TEST_SOURCES + ../cpp/common/sanityCheckers/ConfigSanityManager.cpp + ../cpp/common/sanityCheckers/EphemerisTimeDelayChecker.cpp + ../cpp/common/sanityCheckers/EpochToleranceChecker.cpp + ../cpp/common/sanityCheckers/IonosphericFreeComboChecker.cpp + ../cpp/common/sanityCheckers/IonosphericOutageChecker.cpp + ../cpp/common/sanityCheckers/RequiredSiteEccentricityChecker.cpp + ../cpp/common/sanityCheckers/SbasSanityChecker.cpp +) + +function(add_sanity_checker_test target test_source) + add_executable(${target} + ${SANITY_CHECKER_TEST_SOURCES} + ${test_source} + ) + + target_link_libraries(${target} PRIVATE + Boost::log + Boost::log_setup + Boost::program_options + Boost::thread + Boost::unit_test_framework + ) + + configure_unit_test(${target}) +endfunction() + +add_executable(receiver_metadata_tests + ${RECEIVER_METADATA_TEST_SOURCES} +) + +add_executable(trace_file_cache_tests + unit/test_TraceFileCache.cpp +) + +add_sanity_checker_test(config_sanity_manager_tests + unit/sanityCheckers/test_ConfigSanityManager.cpp +) +add_sanity_checker_test(epoch_tolerance_checker_tests + unit/sanityCheckers/test_EpochToleranceChecker.cpp +) +add_sanity_checker_test(ephemeris_time_delay_checker_tests + unit/sanityCheckers/test_EphemerisTimeDelayChecker.cpp +) +add_sanity_checker_test(ionospheric_free_combo_checker_tests + unit/sanityCheckers/test_IonosphericFreeComboChecker.cpp +) +add_sanity_checker_test(ionospheric_outage_checker_tests + unit/sanityCheckers/test_IonosphericOutageChecker.cpp +) +add_sanity_checker_test(required_site_eccentricity_checker_tests + unit/sanityCheckers/test_RequiredSiteEccentricityChecker.cpp +) +add_sanity_checker_test(sbas_sanity_checker_tests + unit/sanityCheckers/test_SbasSanityChecker.cpp +) + +add_custom_target(unit_tests + DEPENDS receiver_metadata_tests + trace_file_cache_tests + config_sanity_manager_tests + epoch_tolerance_checker_tests + ephemeris_time_delay_checker_tests + ionospheric_free_combo_checker_tests + ionospheric_outage_checker_tests + required_site_eccentricity_checker_tests + sbas_sanity_checker_tests +) + +target_link_libraries(receiver_metadata_tests PRIVATE + Boost::unit_test_framework +) + +target_link_libraries(trace_file_cache_tests PRIVATE + Boost::log + Boost::unit_test_framework +) + +if(WIN32) + target_link_libraries(receiver_metadata_tests PRIVATE ws2_32 wsock32) +endif() + +configure_unit_test(receiver_metadata_tests) +configure_unit_test(trace_file_cache_tests) diff --git a/src/tests/unit/sanityCheckers/test_ConfigSanityManager.cpp b/src/tests/unit/sanityCheckers/test_ConfigSanityManager.cpp new file mode 100644 index 000000000..17854dc60 --- /dev/null +++ b/src/tests/unit/sanityCheckers/test_ConfigSanityManager.cpp @@ -0,0 +1,17 @@ +#define BOOST_TEST_MODULE ConfigSanityManagerTests +#include +#include "common/sanityCheckers/ConfigSanityManager.hpp" + +BOOST_AUTO_TEST_CASE(default_manager_registers_expected_checkers) +{ + auto manager = ConfigSanityManager::defaultManager(); + auto names = manager.checkerNames(); + + BOOST_CHECK_EQUAL(manager.checkerCount(), 6); + BOOST_CHECK_EQUAL(names[0], "EpochToleranceChecker"); + BOOST_CHECK_EQUAL(names[1], "RequiredSiteEccentricityChecker"); + BOOST_CHECK_EQUAL(names[2], "IonosphericOutageChecker"); + BOOST_CHECK_EQUAL(names[3], "EphemerisTimeDelayChecker"); + BOOST_CHECK_EQUAL(names[4], "IonosphericFreeComboChecker"); + BOOST_CHECK_EQUAL(names[5], "SbasSanityChecker"); +} diff --git a/src/tests/unit/sanityCheckers/test_EphemerisTimeDelayChecker.cpp b/src/tests/unit/sanityCheckers/test_EphemerisTimeDelayChecker.cpp new file mode 100644 index 000000000..2ca35d2c2 --- /dev/null +++ b/src/tests/unit/sanityCheckers/test_EphemerisTimeDelayChecker.cpp @@ -0,0 +1,46 @@ +#define BOOST_TEST_MODULE EphemerisTimeDelayCheckerTests +#include +#include "common/acsConfig.hpp" +#include "common/sanityCheckers/EphemerisTimeDelayChecker.hpp" + +BOOST_AUTO_TEST_CASE(resets_ephemeris_time_delay_outside_real_time) +{ + ACSConfig config; + config.simulate_real_time = false; + + for (E_Sys sys : magic_enum::enum_values()) + { + config.default_eph_time_delay[sys] = 12.5; + config.eph_time_delay[sys] = 99.0; + } + + EphemerisTimeDelayChecker checker; + + BOOST_CHECK(checker.check(config)); + + for (E_Sys sys : magic_enum::enum_values()) + { + BOOST_CHECK_EQUAL(config.eph_time_delay[sys], config.default_eph_time_delay[sys]); + } +} + +BOOST_AUTO_TEST_CASE(does_not_reset_ephemeris_time_delay_in_real_time) +{ + ACSConfig config; + config.simulate_real_time = true; + + for (E_Sys sys : magic_enum::enum_values()) + { + config.default_eph_time_delay[sys] = 12.5; + config.eph_time_delay[sys] = 99.0; + } + + EphemerisTimeDelayChecker checker; + + BOOST_CHECK(checker.check(config)); + + for (E_Sys sys : magic_enum::enum_values()) + { + BOOST_CHECK_EQUAL(config.eph_time_delay[sys], 99.0); + } +} diff --git a/src/tests/unit/sanityCheckers/test_EpochToleranceChecker.cpp b/src/tests/unit/sanityCheckers/test_EpochToleranceChecker.cpp new file mode 100644 index 000000000..e95df5f71 --- /dev/null +++ b/src/tests/unit/sanityCheckers/test_EpochToleranceChecker.cpp @@ -0,0 +1,28 @@ +#define BOOST_TEST_MODULE EpochToleranceCheckerTests +#include +#include "common/acsConfig.hpp" +#include "common/sanityCheckers/EpochToleranceChecker.hpp" + +BOOST_AUTO_TEST_CASE(limits_epoch_tolerance_to_half_epoch_interval) +{ + ACSConfig config; + config.epoch_interval = 30; + config.epoch_tolerance = 20; + + EpochToleranceChecker checker; + + BOOST_CHECK(!checker.check(config)); + BOOST_CHECK_EQUAL(config.epoch_tolerance, 15); +} + +BOOST_AUTO_TEST_CASE(passes_when_epoch_tolerance_is_within_limit) +{ + ACSConfig config; + config.epoch_interval = 30; + config.epoch_tolerance = 10; + + EpochToleranceChecker checker; + + BOOST_CHECK(checker.check(config)); + BOOST_CHECK_EQUAL(config.epoch_tolerance, 10); +} diff --git a/src/tests/unit/sanityCheckers/test_IonosphericFreeComboChecker.cpp b/src/tests/unit/sanityCheckers/test_IonosphericFreeComboChecker.cpp new file mode 100644 index 000000000..2d1e96f9d --- /dev/null +++ b/src/tests/unit/sanityCheckers/test_IonosphericFreeComboChecker.cpp @@ -0,0 +1,34 @@ +#define BOOST_TEST_MODULE IonosphericFreeComboCheckerTests +#include +#include "common/acsConfig.hpp" +#include "common/sanityCheckers/IonosphericFreeComboChecker.hpp" + +BOOST_AUTO_TEST_CASE(disables_higher_order_ionospheric_components_when_if_combo_is_enabled) +{ + ACSConfig config; + config.pppOpts.ionoOpts.use_if_combo = true; + config.recOptsMap["TEST"].ionospheric_component2 = true; + config.recOptsMap["TEST"].ionospheric_component3 = true; + + IonosphericFreeComboChecker checker; + + BOOST_CHECK(!checker.check(config)); + BOOST_CHECK(!config.recOptsMap["TEST"].ionospheric_component2); + BOOST_CHECK(!config.recOptsMap["TEST"].ionospheric_component3); + BOOST_CHECK(isInited(config.recOptsMap["TEST"], config.recOptsMap["TEST"].ionospheric_component2)); + BOOST_CHECK(isInited(config.recOptsMap["TEST"], config.recOptsMap["TEST"].ionospheric_component3)); +} + +BOOST_AUTO_TEST_CASE(is_noop_when_if_combo_is_disabled) +{ + ACSConfig config; + config.pppOpts.ionoOpts.use_if_combo = false; + config.recOptsMap["TEST"].ionospheric_component2 = true; + config.recOptsMap["TEST"].ionospheric_component3 = true; + + IonosphericFreeComboChecker checker; + + BOOST_CHECK(checker.check(config)); + BOOST_CHECK(config.recOptsMap["TEST"].ionospheric_component2); + BOOST_CHECK(config.recOptsMap["TEST"].ionospheric_component3); +} diff --git a/src/tests/unit/sanityCheckers/test_IonosphericOutageChecker.cpp b/src/tests/unit/sanityCheckers/test_IonosphericOutageChecker.cpp new file mode 100644 index 000000000..d2af6bbf9 --- /dev/null +++ b/src/tests/unit/sanityCheckers/test_IonosphericOutageChecker.cpp @@ -0,0 +1,26 @@ +#define BOOST_TEST_MODULE IonosphericOutageCheckerTests +#include +#include "common/acsConfig.hpp" +#include "common/sanityCheckers/IonosphericOutageChecker.hpp" + +BOOST_AUTO_TEST_CASE(warns_when_reset_limit_is_less_than_epoch_interval) +{ + ACSConfig config; + config.epoch_interval = 30; + config.ionErrors.outage_reset_limit = 10; + + IonosphericOutageChecker checker; + + BOOST_CHECK(!checker.check(config)); +} + +BOOST_AUTO_TEST_CASE(passes_when_reset_limit_is_at_least_epoch_interval) +{ + ACSConfig config; + config.epoch_interval = 30; + config.ionErrors.outage_reset_limit = 30; + + IonosphericOutageChecker checker; + + BOOST_CHECK(checker.check(config)); +} diff --git a/src/tests/unit/sanityCheckers/test_RequiredSiteEccentricityChecker.cpp b/src/tests/unit/sanityCheckers/test_RequiredSiteEccentricityChecker.cpp new file mode 100644 index 000000000..73fdce8a5 --- /dev/null +++ b/src/tests/unit/sanityCheckers/test_RequiredSiteEccentricityChecker.cpp @@ -0,0 +1,29 @@ +#define BOOST_TEST_MODULE RequiredSiteEccentricityCheckerTests +#include +#include "common/acsConfig.hpp" +#include "common/sanityCheckers/RequiredSiteEccentricityChecker.hpp" + +BOOST_AUTO_TEST_CASE(enables_receiver_eccentricity_model_when_required) +{ + ACSConfig config; + config.require_site_eccentricity = true; + config.recOptsMap["TEST"].eccentricityModel.enable = false; + + RequiredSiteEccentricityChecker checker; + + BOOST_CHECK(!checker.check(config)); + BOOST_CHECK(config.recOptsMap["TEST"].eccentricityModel.enable); + BOOST_CHECK(isInited(config.recOptsMap["TEST"], config.recOptsMap["TEST"].eccentricityModel.enable)); +} + +BOOST_AUTO_TEST_CASE(is_noop_when_site_eccentricity_is_not_required) +{ + ACSConfig config; + config.require_site_eccentricity = false; + config.recOptsMap["TEST"].eccentricityModel.enable = false; + + RequiredSiteEccentricityChecker checker; + + BOOST_CHECK(checker.check(config)); + BOOST_CHECK(!config.recOptsMap["TEST"].eccentricityModel.enable); +} diff --git a/src/tests/unit/sanityCheckers/test_SbasSanityChecker.cpp b/src/tests/unit/sanityCheckers/test_SbasSanityChecker.cpp new file mode 100644 index 000000000..baaad2cc5 --- /dev/null +++ b/src/tests/unit/sanityCheckers/test_SbasSanityChecker.cpp @@ -0,0 +1,109 @@ +#define BOOST_TEST_MODULE SbasSanityCheckerTests +#include +#include "common/acsConfig.hpp" +#include "common/sanityCheckers/SbasSanityChecker.hpp" + +BOOST_AUTO_TEST_CASE(disables_l1_incompatible_input_flags_even_when_sbas_processing_is_off) +{ + ACSConfig config; + config.process_sbas = false; + config.sbsInOpts.freq = 1; + config.sbsInOpts.use_do259 = true; + config.sbsInOpts.pvs_on_dfmc = true; + + SbasSanityChecker checker; + + BOOST_CHECK(!checker.check(config)); + BOOST_CHECK(!config.sbsInOpts.use_do259); + BOOST_CHECK(!config.sbsInOpts.pvs_on_dfmc); +} + +BOOST_AUTO_TEST_CASE(configures_l1_sbas_processing) +{ + ACSConfig config; + config.process_sbas = true; + config.process_preprocessor = false; + config.process_spp = false; + config.sbsOpts.mode = E_SbasMode::L1; + config.sppOpts.smooth_window = 30; + config.sppOpts.use_smooth_only = false; + config.sbsOpts.use_sbas_rec_var = false; + config.process_sys[E_Sys::GPS] = true; + config.process_sys[E_Sys::GAL] = true; + config.process_sys[E_Sys::SBS] = true; + config.satOptsMap["G01"].posModel.enable = false; + + SbasSanityChecker checker; + + BOOST_CHECK(!checker.check(config)); + BOOST_CHECK(config.process_preprocessor); + BOOST_CHECK(config.process_spp); + BOOST_CHECK_EQUAL(config.sbsInOpts.freq, 1); + BOOST_CHECK_EQUAL(config.sppOpts.smooth_window, 100); + BOOST_CHECK(config.sppOpts.use_smooth_only); + BOOST_CHECK(config.sbsOpts.use_sbas_rec_var); + BOOST_CHECK(config.process_sys[E_Sys::GPS]); + BOOST_CHECK(!config.process_sys[E_Sys::GAL]); + BOOST_CHECK(config.process_sys[E_Sys::SBS]); + BOOST_CHECK_EQUAL(config.code_priorities[E_Sys::GPS].front(), E_ObsCode::L1C); + BOOST_CHECK(config.satOptsMap["G01"].posModel.enable); + BOOST_CHECK_EQUAL(config.satOptsMap["G01"].posModel.sources.front(), E_Source::SBAS); +} + +BOOST_AUTO_TEST_CASE(configures_dfmc_sbas_processing) +{ + ACSConfig config; + config.process_sbas = true; + config.sbsOpts.mode = E_SbasMode::DFMC; + config.sbsInOpts.freq = 5; + config.sbsInOpts.pvs_on_dfmc = true; + config.process_sys[E_Sys::GPS] = true; + config.process_sys[E_Sys::GLO] = true; + config.process_sys[E_Sys::BDS] = true; + + SbasSanityChecker checker; + + BOOST_CHECK(checker.check(config)); + BOOST_CHECK_EQUAL(config.sbsInOpts.freq, 5); + BOOST_CHECK(!config.sbsInOpts.pvs_on_dfmc); + BOOST_CHECK(config.process_sys[E_Sys::GPS]); + BOOST_CHECK(!config.process_sys[E_Sys::GLO]); + BOOST_CHECK(config.process_sys[E_Sys::BDS]); + BOOST_CHECK_EQUAL(config.sppOpts.iono_mode, E_IonoMode::SBAS); + BOOST_CHECK_EQUAL(config.sppOpts.trop_models.front(), E_TropModel::SBAS); +} + +BOOST_AUTO_TEST_CASE(configures_pvs_processing) +{ + ACSConfig config; + config.process_sbas = true; + config.process_ppp = false; + config.sbsOpts.mode = E_SbasMode::PVS; + config.process_sys[E_Sys::GPS] = false; + config.process_sys[E_Sys::GAL] = false; + config.process_sys[E_Sys::GLO] = true; + config.recOptsMap["TEST"].tideModels.solid = true; + + SbasSanityChecker checker; + + BOOST_CHECK(checker.check(config)); + BOOST_CHECK(config.process_ppp); + BOOST_CHECK_EQUAL(config.sbsInOpts.freq, 5); + BOOST_CHECK(config.sbsInOpts.pvs_on_dfmc); + BOOST_CHECK(config.process_sys[E_Sys::GPS]); + BOOST_CHECK(config.process_sys[E_Sys::GAL]); + BOOST_CHECK(!config.process_sys[E_Sys::GLO]); + BOOST_CHECK_EQUAL(config.recOptsMap["TEST"].receiver_reference_system, E_Sys::GPS); + BOOST_CHECK(config.recOptsMap["TEST"].tropModel.enable); + BOOST_CHECK_EQUAL(config.recOptsMap["TEST"].tropModel.models.front(), E_TropModel::STANDARD); + BOOST_CHECK(config.recOptsMap["TEST"].tideModels.otl); + BOOST_CHECK(!config.recOptsMap["TEST"].tideModels.atl); + BOOST_CHECK(!config.recOptsMap["TEST"].tideModels.spole); + BOOST_CHECK(!config.recOptsMap["TEST"].tideModels.opole); + BOOST_CHECK(config.sppOpts.always_reinitialise); + BOOST_CHECK(config.pppOpts.use_primary_signals); + BOOST_CHECK(config.errorAccumulation.enable); + BOOST_CHECK_EQUAL(config.ambErrors.phase_reject_limit, 2); + BOOST_CHECK(config.ambErrors.resetOnSlip.LLI); + BOOST_CHECK(config.ambErrors.resetOnSlip.retrack); +} diff --git a/src/tests/unit/test_ReceiverMetaData.cpp b/src/tests/unit/test_ReceiverMetaData.cpp new file mode 100644 index 000000000..088f43655 --- /dev/null +++ b/src/tests/unit/test_ReceiverMetaData.cpp @@ -0,0 +1,493 @@ +#define BOOST_TEST_MODULE receiver_metadata_tests +#include + +#include "common/acsConfig.hpp" +#include "common/enumHelpers.hpp" +#include "common/receiver.hpp" +#include "common/rtcmDecoder.hpp" +#include "common/sinex.hpp" + +static void expectVector3dEq(const Vector3d& actual, const Vector3d& expected) +{ + BOOST_TEST(actual.x() == expected.x()); + BOOST_TEST(actual.y() == expected.y()); + BOOST_TEST(actual.z() == expected.z()); +} + +static void expectPriorityEq( + const vector& actual, + const vector& expected) +{ + BOOST_REQUIRE(actual.size() == expected.size()); + + for (size_t i = 0; i < expected.size(); i++) + { + BOOST_TEST(static_cast(actual[i]) == static_cast(expected[i])); + } +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_source_priority_prefers_higher_priority_source) +{ + ReceiverMetaField field; + auto priority = vector{ + E_ReceiverMetaSource::CONFIG, + E_ReceiverMetaSource::SINEX, + E_ReceiverMetaSource::RINEX, + E_ReceiverMetaSource::RTCM}; + + ingestReceiverMetaField( + field, + string("rtcm-value"), + true, + E_ReceiverMetaSource::RTCM, + priority + ); + ingestReceiverMetaField( + field, + string("config-value"), + true, + E_ReceiverMetaSource::CONFIG, + priority + ); + + BOOST_TEST(field.valid); + BOOST_TEST(field.value == "config-value"); + BOOST_TEST(static_cast(field.winningSource) == static_cast(E_ReceiverMetaSource::CONFIG)); + BOOST_TEST(field.hasSource(E_ReceiverMetaSource::CONFIG)); + BOOST_TEST(field.hasSource(E_ReceiverMetaSource::RTCM)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_source_priority_keeps_existing_for_empty_update) +{ + ReceiverMetaField field; + auto priority = defaultReceiverMetaSourcePriority(); + + ingestReceiverMetaField( + field, + string("rinex-value"), + true, + E_ReceiverMetaSource::RINEX, + priority + ); + ingestReceiverMetaField( + field, + string(""), + false, + E_ReceiverMetaSource::CONFIG, + priority + ); + + BOOST_TEST(field.valid); + BOOST_TEST(field.value == "rinex-value"); + BOOST_TEST(static_cast(field.winningSource) == static_cast(E_ReceiverMetaSource::RINEX)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_priority_index_orders_known_sources_first) +{ + auto priority = vector{ + E_ReceiverMetaSource::SINEX, + E_ReceiverMetaSource::RINEX, + E_ReceiverMetaSource::RTCM}; + + BOOST_TEST( + receiverMetaPriorityIndex(E_ReceiverMetaSource::SINEX, priority) < + receiverMetaPriorityIndex(E_ReceiverMetaSource::RTCM, priority)); + BOOST_TEST( + receiverMetaPriorityIndex(E_ReceiverMetaSource::CONFIG, priority) > + receiverMetaPriorityIndex(E_ReceiverMetaSource::RTCM, priority)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_receiver_options_default_priority_matches_metadata_default) +{ + ReceiverOptions recOpts; + + expectPriorityEq(recOpts.meta_priority, defaultReceiverMetaSourcePriority()); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_receiver_options_default_pos_sources_use_meta_layering) +{ + ReceiverOptions recOpts; + + BOOST_REQUIRE(recOpts.posModel.sources.size() == 4); + BOOST_TEST(static_cast(recOpts.posModel.sources[0]) == static_cast(E_Source::KALMAN)); + BOOST_TEST(static_cast(recOpts.posModel.sources[1]) == static_cast(E_Source::META)); + BOOST_TEST(static_cast(recOpts.posModel.sources[2]) == static_cast(E_Source::SPP)); + BOOST_TEST(static_cast(recOpts.posModel.sources[3]) == static_cast(E_Source::REMOTE)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_source_priority_strings_parse_case_insensitively) +{ + vector sourceStrings = {"rtcm", "RINEX", "Sinex", "CONFIG"}; + + vector priority; + for (const auto& sourceString : sourceStrings) + { + priority.push_back(string_to_enum_nocase_throw(sourceString)); + } + + expectPriorityEq( + priority, + { + E_ReceiverMetaSource::RTCM, + E_ReceiverMetaSource::RINEX, + E_ReceiverMetaSource::SINEX, + E_ReceiverMetaSource::CONFIG}); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_ingest_config_applies_meta_priority) +{ + ReceiverMetadata metadata; + + ReceiverOptions recOpts; + recOpts.meta_priority = { + E_ReceiverMetaSource::RTCM, + E_ReceiverMetaSource::CONFIG, + E_ReceiverMetaSource::SINEX, + E_ReceiverMetaSource::RINEX}; + recOpts.receiver_type = "CONFIG TYPE"; + + RtcmStationInfo rtcmInfo; + rtcmInfo.receiverType = "RTCM TYPE"; + + metadata.ingestConfig(recOpts); + metadata.ingestRtcm(rtcmInfo); + + BOOST_TEST(metadata.receiverType.value == "RTCM TYPE"); + BOOST_TEST(static_cast(metadata.receiverType.winningSource) == + static_cast(E_ReceiverMetaSource::RTCM)); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::CONFIG)); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::RTCM)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_config_enable_without_offset_does_not_mask_sinex_delta) +{ + ReceiverMetadata metadata; + + ReceiverOptions recOpts; + recOpts.eccentricityModel.enable = true; + + SinexSiteEcc sinexEcc; + sinexEcc.ecc = VectorEnu(Vector3d(0.4, 0.5, 0.6)); + + SinexRecData recSnx; + recSnx.ecc_ptr = &sinexEcc; + + metadata.ingestConfig(recOpts); + metadata.ingestSinex(recSnx); + + BOOST_TEST(metadata.antennaDelta.valid); + expectVector3dEq(metadata.antennaDelta.value, Vector3d(0.4, 0.5, 0.6)); + BOOST_TEST(static_cast(metadata.antennaDelta.winningSource) == + static_cast(E_ReceiverMetaSource::SINEX)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_source_priority_participates_in_option_inheritance) +{ + ReceiverOptions inheritedOpts; + ReceiverOptions baseOpts; + + vector basePriority = { + E_ReceiverMetaSource::RTCM, + E_ReceiverMetaSource::RINEX, + E_ReceiverMetaSource::SINEX, + E_ReceiverMetaSource::CONFIG}; + + setOption(baseOpts, baseOpts.meta_priority, basePriority); + + bool inherited = + initIfNeeded(inheritedOpts, baseOpts, inheritedOpts.meta_priority); + + BOOST_TEST(inherited); + BOOST_TEST(isInited(inheritedOpts, inheritedOpts.meta_priority)); + expectPriorityEq(inheritedOpts.meta_priority, basePriority); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_ingest_rtcm_maps_station_fields) +{ + ReceiverMetadata metadata; + + RtcmStationInfo rtcmInfo; + rtcmInfo.receiverType = "TRIMBLE ALLOY"; + rtcmInfo.receiverFirmware = "6.45"; + rtcmInfo.receiverSerial = "RTCM-REC"; + rtcmInfo.antennaDesc = "TRM57971.00 NONE"; + rtcmInfo.antennaSerial = "RTCM-ANT"; + rtcmInfo.antennaHeight = 1.2345; + rtcmInfo.hasAntennaHeight = true; + rtcmInfo.physicalStationId = 7; + rtcmInfo.physEcefX = 1111.1; + rtcmInfo.physEcefY = 2222.2; + rtcmInfo.physEcefZ = 3333.3; + + metadata.ingestRtcm(rtcmInfo); + + BOOST_TEST(metadata.receiverType.valid); + BOOST_TEST(metadata.receiverType.value == "TRIMBLE ALLOY"); + BOOST_TEST(metadata.receiverFirmware.value == "6.45"); + BOOST_TEST(metadata.receiverSerial.value == "RTCM-REC"); + BOOST_TEST(metadata.antennaDescriptor.value == "TRM57971.00 NONE"); + BOOST_TEST(metadata.antennaSerial.value == "RTCM-ANT"); + expectVector3dEq(metadata.antennaDelta.value, Vector3d(0, 0, 1.2345)); + expectVector3dEq(metadata.stationPosition.value, Vector3d(1111.1, 2222.2, 3333.3)); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::RTCM)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_ingest_rtcm_ignores_empty_physical_station_position) +{ + ReceiverMetadata metadata; + + ReceiverOptions recOpts; + recOpts.apriori_pos = Vector3d(123.0, 456.0, 789.0); + + RtcmStationInfo rtcmInfo; + rtcmInfo.physicalStationId = 7; + + metadata.ingestConfig(recOpts); + metadata.ingestRtcm(rtcmInfo); + + BOOST_TEST(metadata.stationPosition.valid); + expectVector3dEq(metadata.stationPosition.value, Vector3d(123.0, 456.0, 789.0)); + BOOST_TEST(static_cast(metadata.stationPosition.winningSource) == + static_cast(E_ReceiverMetaSource::CONFIG)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_ingest_rtcm_accepts_direct_station_position_without_physical_id) +{ + ReceiverMetadata metadata; + + RtcmStationInfo rtcmInfo; + rtcmInfo.ecefX = 4444.4; + rtcmInfo.ecefY = 5555.5; + rtcmInfo.ecefZ = 6666.6; + + metadata.ingestRtcm(rtcmInfo); + + BOOST_TEST(metadata.stationPosition.valid); + expectVector3dEq(metadata.stationPosition.value, Vector3d(4444.4, 5555.5, 6666.6)); + BOOST_TEST(static_cast(metadata.stationPosition.winningSource) == + static_cast(E_ReceiverMetaSource::RTCM)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_ingest_rinex_maps_header_fields) +{ + ReceiverMetadata metadata; + + RinexStation rnx; + rnx.id = "ABCD"; + rnx.marker = "MARKER-01"; + rnx.antDesc = "LEIAR25.R4 NONE"; + rnx.antSerial = "RINEX-ANT"; + rnx.recType = "SEPT POLARX5"; + rnx.recFWVersion = "5.4.0"; + rnx.recSerial = "RINEX-REC"; + rnx.del = Vector3d(0.1, 0.2, 0.3); + rnx.pos = Vector3d(4444.4, 5555.5, 6666.6); + + metadata.ingestRinex(rnx); + + BOOST_TEST(metadata.receiverType.value == "SEPT POLARX5"); + BOOST_TEST(metadata.receiverFirmware.value == "5.4.0"); + BOOST_TEST(metadata.receiverSerial.value == "RINEX-REC"); + BOOST_TEST(metadata.antennaDescriptor.value == "LEIAR25.R4 NONE"); + BOOST_TEST(metadata.antennaSerial.value == "RINEX-ANT"); + BOOST_TEST(metadata.markerName.value == "ABCD"); + BOOST_TEST(metadata.markerNumber.value == "MARKER-01"); + expectVector3dEq(metadata.antennaDelta.value, Vector3d(0.1, 0.2, 0.3)); + expectVector3dEq(metadata.stationPosition.value, Vector3d(4444.4, 5555.5, 6666.6)); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::RINEX)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_ingest_sinex_maps_lookup_fields) +{ + ReceiverMetadata metadata; + + SinexReceiver sinexReceiver; + sinexReceiver.type = "JAVAD TRE_3"; + sinexReceiver.firm = "3.7.9"; + sinexReceiver.sn = "SNXREC"; + + SinexAntenna sinexAntenna; + sinexAntenna.type = "JAVRINGANT_DM SCIS"; + sinexAntenna.sn = "SNXANT"; + + SinexSiteEcc sinexEcc; + sinexEcc.ecc = VectorEnu(Vector3d(0.4, 0.5, 0.6)); + + SinexRecData recSnx; + recSnx.rec_ptr = &sinexReceiver; + recSnx.ant_ptr = &sinexAntenna; + recSnx.ecc_ptr = &sinexEcc; + recSnx.pos = VectorEcef(Vector3d(7777.7, 8888.8, 9999.9)); + + metadata.ingestSinex(recSnx); + + BOOST_TEST(metadata.receiverType.value == "JAVAD TRE_3"); + BOOST_TEST(metadata.receiverFirmware.value == "3.7.9"); + BOOST_TEST(metadata.receiverSerial.value == "SNXREC"); + BOOST_TEST(metadata.antennaDescriptor.value == "JAVRINGANT_DM SCIS"); + BOOST_TEST(metadata.antennaSerial.value == "SNXANT"); + expectVector3dEq(metadata.antennaDelta.value, Vector3d(0.4, 0.5, 0.6)); + expectVector3dEq(metadata.stationPosition.value, Vector3d(7777.7, 8888.8, 9999.9)); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::SINEX)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_ingest_config_maps_receiver_options) +{ + ReceiverMetadata metadata; + + ReceiverOptions recOpts; + recOpts.receiver_type = " TRIMBLE NETR9 "; + recOpts.antenna_type = " TRM59800.00 NONE "; + recOpts.apriori_pos = Vector3d(123.0, 456.0, 789.0); + recOpts.eccentricityModel.enable = true; + setOption(recOpts, recOpts.eccentricityModel.eccentricity, Vector3d(1.0, 2.0, 3.0)); + + metadata.ingestConfig(recOpts); + + BOOST_TEST(metadata.receiverType.value == "TRIMBLE NETR9"); + BOOST_TEST(metadata.antennaDescriptor.value == "TRM59800.00 NONE"); + expectVector3dEq(metadata.antennaDelta.value, Vector3d(1.0, 2.0, 3.0)); + expectVector3dEq(metadata.stationPosition.value, Vector3d(123.0, 456.0, 789.0)); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::CONFIG)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_ingest_config_ignores_whitespace_strings) +{ + ReceiverMetadata metadata; + + ReceiverOptions recOpts; + recOpts.receiver_type = " "; + recOpts.antenna_type = "\t"; + + metadata.ingestConfig(recOpts); + + BOOST_TEST(metadata.receiverType.valid == false); + BOOST_TEST(metadata.antennaDescriptor.valid == false); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_ingest_methods_respect_configured_priority) +{ + ReceiverMetadata metadata; + metadata.setPriority({ + E_ReceiverMetaSource::SINEX, + E_ReceiverMetaSource::RTCM, + E_ReceiverMetaSource::RINEX, + E_ReceiverMetaSource::CONFIG}); + + RtcmStationInfo rtcmInfo; + rtcmInfo.receiverType = "RTCM TYPE"; + + RinexStation rnx; + rnx.recType = "RINEX TYPE"; + + SinexReceiver sinexReceiver; + sinexReceiver.type = "SINEX TYPE"; + + SinexRecData recSnx; + recSnx.rec_ptr = &sinexReceiver; + + metadata.ingestRtcm(rtcmInfo); + metadata.ingestRinex(rnx); + metadata.ingestSinex(recSnx); + + BOOST_TEST(metadata.receiverType.value == "SINEX TYPE"); + BOOST_TEST(static_cast(metadata.receiverType.winningSource) == + static_cast(E_ReceiverMetaSource::SINEX)); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::RTCM)); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::RINEX)); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::SINEX)); +} + +BOOST_AUTO_TEST_CASE(receiver_metadata_unlisted_sources_are_ignored) +{ + ReceiverMetadata metadata; + metadata.setPriority({E_ReceiverMetaSource::RTCM}); + + RinexStation rnx; + rnx.recType = "RINEX TYPE"; + rnx.pos = Vector3d(4444.4, 5555.5, 6666.6); + + SinexReceiver sinexReceiver; + sinexReceiver.type = "SINEX TYPE"; + + SinexRecData recSnx; + recSnx.rec_ptr = &sinexReceiver; + recSnx.pos = VectorEcef(Vector3d(7777.7, 8888.8, 9999.9)); + + metadata.ingestRinex(rnx); + metadata.ingestSinex(recSnx); + + BOOST_TEST(metadata.receiverType.valid == false); + BOOST_TEST(metadata.stationPosition.valid == false); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::RINEX) == false); + BOOST_TEST(metadata.receiverType.hasSource(E_ReceiverMetaSource::SINEX) == false); +} + +BOOST_AUTO_TEST_CASE(rtcm_metadata_selection_prefers_last_reference_station) +{ + map stationInfoMap; + stationInfoMap[10].receiverType = "STATION 10"; + stationInfoMap[20].receiverType = "STATION 20"; + + auto* info = selectRtcmStationInfoForMetadata(stationInfoMap, 20); + + BOOST_REQUIRE(info != nullptr); + BOOST_TEST(info->receiverType == "STATION 20"); +} + +BOOST_AUTO_TEST_CASE(rtcm_metadata_selection_uses_single_station_before_msm_reference) +{ + map stationInfoMap; + stationInfoMap[10].receiverType = "ONLY STATION"; + + auto* info = selectRtcmStationInfoForMetadata(stationInfoMap, -1); + + BOOST_REQUIRE(info != nullptr); + BOOST_TEST(info->receiverType == "ONLY STATION"); +} + +BOOST_AUTO_TEST_CASE(rtcm_metadata_selection_does_not_guess_with_multiple_stations) +{ + map stationInfoMap; + stationInfoMap[10].receiverType = "STATION 10"; + stationInfoMap[20].receiverType = "STATION 20"; + + auto* info = selectRtcmStationInfoForMetadata(stationInfoMap, -1); + + BOOST_TEST(info == nullptr); +} + +BOOST_AUTO_TEST_CASE(rtcm_metadata_selection_does_not_fallback_when_known_station_missing) +{ + map stationInfoMap; + stationInfoMap[10].receiverType = "STATION 10"; + + auto* info = selectRtcmStationInfoForMetadata(stationInfoMap, 20); + + BOOST_TEST(info == nullptr); +} + +BOOST_AUTO_TEST_CASE(rtcm_metadata_selection_keeps_non_physical_station_metadata_together) +{ + map stationInfoMap; + stationInfoMap[100].receiverType = "VIRTUAL RECEIVER"; + stationInfoMap[100].antennaDesc = "VIRTUAL ANTENNA"; + stationInfoMap[100].physicalStationId = 7; + stationInfoMap[100].physEcefX = 1111.1; + stationInfoMap[100].physEcefY = 2222.2; + stationInfoMap[100].physEcefZ = 3333.3; + stationInfoMap[7].ecefX = 4444.4; + stationInfoMap[7].ecefY = 5555.5; + stationInfoMap[7].ecefZ = 6666.6; + + auto* info = selectRtcmStationInfoForMetadata(stationInfoMap, 100); + + BOOST_REQUIRE(info != nullptr); + BOOST_TEST(info->receiverType == "VIRTUAL RECEIVER"); + BOOST_TEST(info->antennaDesc == "VIRTUAL ANTENNA"); + + ReceiverMetadata metadata; + metadata.ingestRtcm(*info); + + expectVector3dEq(metadata.stationPosition.value, Vector3d(1111.1, 2222.2, 3333.3)); +} diff --git a/src/tests/unit/test_TraceFileCache.cpp b/src/tests/unit/test_TraceFileCache.cpp new file mode 100644 index 000000000..e81eea0d6 --- /dev/null +++ b/src/tests/unit/test_TraceFileCache.cpp @@ -0,0 +1,56 @@ +#define BOOST_TEST_MODULE trace_file_cache_tests +#include +#include +#include +#include +#include + +#include "common/trace.hpp" + +thread_local boost::iostreams::stream nullStream{ + boost::iostreams::null_sink{} +}; + +namespace +{ +std::filesystem::path makeTempDir() +{ + auto suffix = std::chrono::steady_clock::now().time_since_epoch().count(); + auto path = std::filesystem::temp_directory_path() / + ("ginan_trace_cache_test_" + std::to_string(suffix)); + + std::filesystem::create_directories(path); + return path; +} +} // namespace + +BOOST_AUTO_TEST_CASE(retain_trace_files_prunes_rotated_trace_streams) +{ + auto tempDir = makeTempDir(); + auto fileA = (tempDir / "trace-a.trace").string(); + auto fileB = (tempDir / "trace-b.trace").string(); + + retainTraceFiles({}); + + { + auto traceA = getTraceFile(fileA, string("A")); + traceA << "first\n"; + traceA.flush(); + } + + auto traceB = getTraceFile(fileB, string("B")); + traceB << "second\n"; + traceB.flush(); + + BOOST_REQUIRE(traceFileCache().find(fileA) != traceFileCache().end()); + BOOST_REQUIRE(traceFileCache().find(fileB) != traceFileCache().end()); + + retainTraceFiles({fileB}); + + BOOST_CHECK(traceFileCache().find(fileA) == traceFileCache().end()); + BOOST_CHECK(traceFileCache().find(fileB) != traceFileCache().end()); + + traceB.flush(); + retainTraceFiles({}); + std::filesystem::remove_all(tempDir); +} diff --git a/vcpkg.json b/vcpkg.json index 4b93f504a..0942e3d8a 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -10,6 +10,7 @@ "boost-thread", "boost-program-options", "boost-serialization", + "boost-test", "boost-timer", "boost-bimap", "boost-format", @@ -38,7 +39,9 @@ "name": "mongo-cxx-driver", "$comment": "Optional: MongoDB support - can be disabled with ENABLE_MONGODB=OFF", "platform": "!windows" - } + }, + "netcdf-c", + "netcdf-cxx4" ], "builtin-baseline": "4c5ae6b55f3e3e39d291679f89822f496cf190ee" }